@redpanda-data/docs-extensions-and-macros 5.13.5 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools-mcp.js +88 -0
- package/bin/doc-tools.js +349 -102
- package/bin/mcp-tools/index.js +20 -0
- package/bin/mcp-tools/lint-doc-strings.js +75 -0
- package/bin/mcp-tools/overrides-audit.js +100 -0
- package/bin/mcp-tools/preview-doc-string.js +71 -0
- package/cli-utils/diff-utils.js +77 -0
- package/cli-utils/helm-commented-values.js +172 -23
- package/cli-utils/sync-schemas.js +144 -0
- package/docs-data/rpk-overrides.schema.json +4 -0
- package/package.json +2 -1
- package/tools/generate-cli-docs.js +28 -67
- package/tools/lint-strings/diff.js +137 -0
- package/tools/lint-strings/engine.js +163 -0
- package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
- package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
- package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
- package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
- package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
- package/tools/lint-strings/go-source.js +327 -0
- package/tools/lint-strings/index.js +229 -0
- package/tools/lint-strings/rules/common.js +148 -0
- package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
- package/tools/lint-strings/source-text.js +41 -0
- package/tools/lint-strings/surfaces/connect.js +233 -0
- package/tools/lint-strings/surfaces/crd.js +239 -0
- package/tools/lint-strings/surfaces/helm.js +159 -0
- package/tools/lint-strings/surfaces/metrics.js +368 -0
- package/tools/lint-strings/surfaces/properties.js +216 -0
- package/tools/lint-strings/surfaces/rpk.js +377 -0
- package/tools/overrides-audit/README.adoc +152 -0
- package/tools/overrides-audit/adapters/connect.js +144 -0
- package/tools/overrides-audit/adapters/properties.js +117 -0
- package/tools/overrides-audit/adapters/rpk.js +192 -0
- package/tools/overrides-audit/classify.js +546 -0
- package/tools/overrides-audit/index.js +185 -0
- package/tools/preview-string/index.js +245 -0
- package/tools/property-extractor/Makefile +9 -4
- package/tools/property-extractor/parser.py +26 -0
- package/tools/property-extractor/property_extractor.py +41 -0
- package/tools/property-extractor/transformers.py +15 -2
- package/tools/rpk-docs/detect-platform-commands.js +8 -1
- package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { isDeepStrictEqual } = require('util')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Schemas that document docs-data/*.json files (rpk-overrides.schema.json,
|
|
9
|
+
* property-overrides.schema.json, ...) are meant to live inside each content
|
|
10
|
+
* repo's own docs-data/, alongside the file they document — a writer can
|
|
11
|
+
* change both the override data and its schema in one PR, without waiting on
|
|
12
|
+
* a new @redpanda-data/docs-extensions-and-macros release first.
|
|
13
|
+
*
|
|
14
|
+
* That's exactly why this package's copy is not automatically authoritative:
|
|
15
|
+
* a content repo's copy can legitimately be AHEAD of this package's, when a
|
|
16
|
+
* writer documented a real override field here before anyone remembered to
|
|
17
|
+
* update the schema in this package. Confirmed happening in practice —
|
|
18
|
+
* docs-data/rpk-overrides.schema.json in this package is missing `asPartial`,
|
|
19
|
+
* a real field the generator (tools/rpk-docs/generate-rpk-docs.js) already
|
|
20
|
+
* reads, that the docs repo's copy documents correctly. A naive "package
|
|
21
|
+
* always wins" sync would have silently deleted that the first time someone
|
|
22
|
+
* ran it. So syncSchemas() only ever overwrites a destination file when this
|
|
23
|
+
* package's copy is a superset of it (every object key the destination has,
|
|
24
|
+
* the source also has) — otherwise it reports the destination-only keys and
|
|
25
|
+
* refuses, same as `check` mode, unless the caller passes `force: true`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const PACKAGE_SCHEMA_DIR = path.resolve(__dirname, '..', 'docs-data')
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* List the *.schema.json files this package ships.
|
|
32
|
+
* @returns {Array<{name: string, sourcePath: string}>}
|
|
33
|
+
*/
|
|
34
|
+
function listPackageSchemas () {
|
|
35
|
+
if (!fs.existsSync(PACKAGE_SCHEMA_DIR)) return []
|
|
36
|
+
return fs.readdirSync(PACKAGE_SCHEMA_DIR)
|
|
37
|
+
.filter((name) => name.endsWith('.schema.json'))
|
|
38
|
+
.sort()
|
|
39
|
+
.map((name) => ({ name, sourcePath: path.join(PACKAGE_SCHEMA_DIR, name) }))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find object keys present in `dest` but absent from the same path in
|
|
44
|
+
* `source` — the shape of "the destination knows something the source
|
|
45
|
+
* doesn't". Arrays are compared as opaque leaves (JSON Schema's own
|
|
46
|
+
* structure keeps named, distinguishable content in objects — `properties`,
|
|
47
|
+
* `$defs` — not in arrays like `required`/`enum`, so this is where a
|
|
48
|
+
* destination-only capability would actually show up).
|
|
49
|
+
*
|
|
50
|
+
* @param {*} source
|
|
51
|
+
* @param {*} dest
|
|
52
|
+
* @param {string} [pathPrefix]
|
|
53
|
+
* @returns {string[]} Dotted paths that exist in dest but not source.
|
|
54
|
+
*/
|
|
55
|
+
function findDestOnlyPaths (source, dest, pathPrefix = '') {
|
|
56
|
+
if (
|
|
57
|
+
dest === null || typeof dest !== 'object' || Array.isArray(dest) ||
|
|
58
|
+
source === null || typeof source !== 'object' || Array.isArray(source)
|
|
59
|
+
) {
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const onlyInDest = []
|
|
64
|
+
for (const [key, destValue] of Object.entries(dest)) {
|
|
65
|
+
const keyPath = pathPrefix ? `${pathPrefix}.${key}` : key
|
|
66
|
+
if (!Object.hasOwn(source, key)) {
|
|
67
|
+
onlyInDest.push(keyPath)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
onlyInDest.push(...findDestOnlyPaths(source[key], destValue, keyPath))
|
|
71
|
+
}
|
|
72
|
+
return onlyInDest
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compare this package's schemas against the copies in a destination
|
|
77
|
+
* directory (a content repo's docs-data/), and optionally write updates.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} [opts]
|
|
80
|
+
* @param {string} [opts.destDir] - Directory to sync into. Defaults to
|
|
81
|
+
* docs-data/ under the current repo root.
|
|
82
|
+
* @param {boolean} [opts.check=false] - When true, never writes — only
|
|
83
|
+
* reports what's missing or out of date. Use this in CI.
|
|
84
|
+
* @param {boolean} [opts.force=false] - Overwrite a destination file even
|
|
85
|
+
* when it has content this package's copy lacks. Rarely correct — see the
|
|
86
|
+
* module doc comment. Ignored when `check` is true.
|
|
87
|
+
* @returns {{
|
|
88
|
+
* destDir: string,
|
|
89
|
+
* results: Array<{
|
|
90
|
+
* name: string,
|
|
91
|
+
* status: ('created'|'updated'|'unchanged'|'diverged'),
|
|
92
|
+
* sourcePath: string,
|
|
93
|
+
* destPath: string,
|
|
94
|
+
* destOnlyPaths?: string[]
|
|
95
|
+
* }>,
|
|
96
|
+
* drift: boolean
|
|
97
|
+
* }} drift is true when any schema is missing, differs, or has diverged.
|
|
98
|
+
* status 'diverged' means the destination has content this package's copy
|
|
99
|
+
* doesn't — nothing was written for that file unless `force` was set.
|
|
100
|
+
*/
|
|
101
|
+
function syncSchemas ({ destDir, check = false, force = false } = {}) {
|
|
102
|
+
const resolvedDest = path.resolve(destDir || 'docs-data')
|
|
103
|
+
const schemas = listPackageSchemas()
|
|
104
|
+
|
|
105
|
+
const results = schemas.map(({ name, sourcePath }) => {
|
|
106
|
+
const destPath = path.join(resolvedDest, name)
|
|
107
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf8')
|
|
108
|
+
const destExists = fs.existsSync(destPath)
|
|
109
|
+
const destContent = destExists ? fs.readFileSync(destPath, 'utf8') : null
|
|
110
|
+
|
|
111
|
+
let status
|
|
112
|
+
let destOnlyPaths
|
|
113
|
+
if (!destExists) {
|
|
114
|
+
status = 'created'
|
|
115
|
+
} else {
|
|
116
|
+
// Compare parsed content, not raw text — a destination reformatted by a
|
|
117
|
+
// content repo's own prettier/editorconfig (different indent, key
|
|
118
|
+
// order, trailing newline) with no real data change must not read as
|
|
119
|
+
// drift.
|
|
120
|
+
const sourceParsed = JSON.parse(sourceContent)
|
|
121
|
+
const destParsed = JSON.parse(destContent)
|
|
122
|
+
if (isDeepStrictEqual(sourceParsed, destParsed)) {
|
|
123
|
+
status = 'unchanged'
|
|
124
|
+
} else {
|
|
125
|
+
destOnlyPaths = findDestOnlyPaths(sourceParsed, destParsed)
|
|
126
|
+
status = destOnlyPaths.length > 0 ? 'diverged' : 'updated'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const shouldWrite = !check && (status === 'created' || status === 'updated' || (status === 'diverged' && force))
|
|
131
|
+
if (shouldWrite) {
|
|
132
|
+
fs.mkdirSync(resolvedDest, { recursive: true })
|
|
133
|
+
fs.writeFileSync(destPath, sourceContent)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { name, status, sourcePath, destPath, ...(destOnlyPaths ? { destOnlyPaths } : {}) }
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const drift = results.some((r) => r.status !== 'unchanged')
|
|
140
|
+
|
|
141
|
+
return { destDir: resolvedDest, results, drift }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { listPackageSchemas, syncSchemas, findDestOnlyPaths, PACKAGE_SCHEMA_DIR }
|
|
@@ -137,6 +137,10 @@
|
|
|
137
137
|
"type": "boolean",
|
|
138
138
|
"description": "If true, exclude this command from documentation entirely. Use for internal or hidden commands that shouldn't be documented."
|
|
139
139
|
},
|
|
140
|
+
"asPartial": {
|
|
141
|
+
"type": "boolean",
|
|
142
|
+
"description": "If true, write this command's .adoc file to the partials directory instead of the pages directory. Inherited by child commands — set on a parent command (e.g., 'rpk ai') to route the entire subtree to partials."
|
|
143
|
+
},
|
|
140
144
|
"excludeFlags": {
|
|
141
145
|
"type": "array",
|
|
142
146
|
"description": "List of flag names to exclude from documentation (without dashes)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.17.0",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"test:python": "./__tests__/tools/property-extractor/setup-and-test.sh",
|
|
33
33
|
"test:cgo": "node __tests__/tools/cgo-detection.test.js",
|
|
34
34
|
"test:all": "npm run test && npm run test:python && npm run test:cgo",
|
|
35
|
+
"eval:doc-strings": "node evals/doc-strings/run-evals.js",
|
|
35
36
|
"generate:cli-docs": "node tools/generate-cli-docs.js",
|
|
36
37
|
"bundle:admin": "doc-tools generate bundle-openapi --surface admin",
|
|
37
38
|
"bundle:connect": "doc-tools generate bundle-openapi --surface connect",
|
|
@@ -277,74 +277,35 @@ IMPORTANT: This documentation is auto-generated. Do not edit manually. Run \`npm
|
|
|
277
277
|
const mainData = parseHelp(mainHelp);
|
|
278
278
|
doc += generateCommandDoc('', mainData, null, 2);
|
|
279
279
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
'
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const generateHelp = getHelpOutput('generate');
|
|
304
|
-
const generateData = parseHelp(generateHelp);
|
|
305
|
-
doc += generateCommandDoc('generate', generateData, null, 2);
|
|
306
|
-
|
|
307
|
-
// Generate subcommands
|
|
308
|
-
const generateSubcommands = [
|
|
309
|
-
'property-docs',
|
|
310
|
-
'metrics-docs',
|
|
311
|
-
'rpk-docs',
|
|
312
|
-
'rpk-env-partial',
|
|
313
|
-
'rpcn-connector-docs',
|
|
314
|
-
'helm-spec',
|
|
315
|
-
'cloud-regions',
|
|
316
|
-
'crd-spec',
|
|
317
|
-
'bundle-openapi',
|
|
318
|
-
'update-connect-version',
|
|
319
|
-
'migrate-rpcn-metadata',
|
|
320
|
-
'migrate-rpcn-descriptions'
|
|
321
|
-
];
|
|
322
|
-
|
|
323
|
-
generateSubcommands.forEach(subcmd => {
|
|
324
|
-
console.log(` Generating docs for: generate ${subcmd}`);
|
|
325
|
-
const help = getHelpOutput(`generate ${subcmd}`);
|
|
326
|
-
const data = parseHelp(help);
|
|
327
|
-
const jsdoc = jsdocs[subcmd];
|
|
328
|
-
doc += generateCommandDoc(`generate ${subcmd}`, data, jsdoc, 3);
|
|
329
|
-
});
|
|
280
|
+
// Walk the command tree the CLI itself reports, rather than three
|
|
281
|
+
// hardcoded lists. Those lists had drifted both ways at once: lint-strings,
|
|
282
|
+
// preview-string and overrides were undocumented while README.adoc points
|
|
283
|
+
// users here as the complete reference, and `preview-prompt` was still
|
|
284
|
+
// getting a section even though the command no longer exists (the generator
|
|
285
|
+
// fell back to the top-level help for it, so the section was real-looking
|
|
286
|
+
// and wrong). A command group is anything whose help lists Commands: of its
|
|
287
|
+
// own, so a new command or subcommand is documented the moment it is
|
|
288
|
+
// registered.
|
|
289
|
+
const documentCommand = (commandPath, level) => {
|
|
290
|
+
const label = commandPath.join(' ');
|
|
291
|
+
console.log(`${' '.repeat(level - 1)}Generating docs for: ${label}`);
|
|
292
|
+
const data = parseHelp(getHelpOutput(label));
|
|
293
|
+
const jsdoc = jsdocs[commandPath[commandPath.length - 1]];
|
|
294
|
+
doc += generateCommandDoc(label, data, jsdoc, level);
|
|
295
|
+
for (const child of data.commands) {
|
|
296
|
+
// Commander lists `help [command]` in every group; it is not a command
|
|
297
|
+
// anyone documents.
|
|
298
|
+
const childName = child.name.split(/[\s[<]/)[0];
|
|
299
|
+
if (childName === 'help') continue;
|
|
300
|
+
documentCommand([...commandPath, childName], level + 1);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
330
303
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const validateSubcommands = [
|
|
338
|
-
'enterprise-features'
|
|
339
|
-
];
|
|
340
|
-
|
|
341
|
-
validateSubcommands.forEach(subcmd => {
|
|
342
|
-
console.log(` Generating docs for: validate ${subcmd}`);
|
|
343
|
-
const help = getHelpOutput(`validate ${subcmd}`);
|
|
344
|
-
const data = parseHelp(help);
|
|
345
|
-
const jsdoc = jsdocs[subcmd];
|
|
346
|
-
doc += generateCommandDoc(`validate ${subcmd}`, data, jsdoc, 3);
|
|
347
|
-
});
|
|
304
|
+
for (const child of mainData.commands) {
|
|
305
|
+
const childName = child.name.split(/[\s[<]/)[0];
|
|
306
|
+
if (childName === 'help') continue;
|
|
307
|
+
documentCommand([childName], 2);
|
|
308
|
+
}
|
|
348
309
|
|
|
349
310
|
// Write to file
|
|
350
311
|
const outputPath = path.join(__dirname, '..', 'CLI_REFERENCE.adoc');
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Diff support for declaration-anchored linting.
|
|
7
|
+
*
|
|
8
|
+
* Diff mode never lints hunks directly: descriptions are clang-format-wrapped
|
|
9
|
+
* adjacent string literals, so a PR touching one wrapped line must surface the
|
|
10
|
+
* FULL declaration. The flow is: parse `git diff <base>...HEAD` into changed
|
|
11
|
+
* (post-image) line numbers per file, route each file to a surface by path,
|
|
12
|
+
* extract declarations from those files at HEAD, and keep declarations whose
|
|
13
|
+
* line span intersects the changed lines.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Path -> surface routing table. First match wins. A surface routed here
|
|
18
|
+
* without an extractor in the index.js SURFACES registry is reported as
|
|
19
|
+
* not-yet-supported instead of silently dropping the files.
|
|
20
|
+
*/
|
|
21
|
+
const SURFACE_ROUTES = [
|
|
22
|
+
{ surface: 'properties', pattern: /^src\/v\/config\// },
|
|
23
|
+
{ surface: 'metrics', pattern: /(^|\/)[^/]*probe\.cc$|^src\/v\/metrics\// },
|
|
24
|
+
{ surface: 'rpk', pattern: /^src\/go\/rpk\/pkg\/cli\// },
|
|
25
|
+
// Both chart layouts ship: charts/<name>/chart/values.yaml (redpanda,
|
|
26
|
+
// console) and charts/<name>/values.yaml (connectors).
|
|
27
|
+
{ surface: 'helm', pattern: /^charts\/[^/]+\/(chart\/)?values\.yaml$/ },
|
|
28
|
+
{ surface: 'crd', pattern: /^operator\/api\// },
|
|
29
|
+
{ surface: 'connect', pattern: /^internal\/impl\// }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Route a repo-relative file path to a surface id, or null when the file is
|
|
34
|
+
* not a doc-string surface.
|
|
35
|
+
*/
|
|
36
|
+
function routeFile (file) {
|
|
37
|
+
for (const route of SURFACE_ROUTES) {
|
|
38
|
+
if (route.pattern.test(file)) return route.surface
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run `git diff <base>...HEAD` in the repo and parse it into a map of
|
|
45
|
+
* file -> Set of changed line numbers in the post-image (HEAD) version.
|
|
46
|
+
* Deleted files (no post-image) are omitted.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} repo - Path to the git checkout
|
|
49
|
+
* @param {string} base - Base ref (`git diff base...HEAD`)
|
|
50
|
+
* @returns {Map<string, Set<number>>}
|
|
51
|
+
*/
|
|
52
|
+
function getChangedLines (repo, base) {
|
|
53
|
+
const result = spawnSync(
|
|
54
|
+
'git',
|
|
55
|
+
['diff', '--unified=0', '--no-color', `${base}...HEAD`],
|
|
56
|
+
{ cwd: repo, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 }
|
|
57
|
+
)
|
|
58
|
+
if (result.error) throw result.error
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
throw new Error(`git diff ${base}...HEAD failed in ${repo}: ${result.stderr || result.stdout}`)
|
|
61
|
+
}
|
|
62
|
+
return parseUnifiedDiff(result.stdout)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse unified diff text (produced with --unified=0) into
|
|
67
|
+
* file -> Set(changed post-image line numbers).
|
|
68
|
+
*/
|
|
69
|
+
function parseUnifiedDiff (diffText) {
|
|
70
|
+
const changed = new Map()
|
|
71
|
+
let currentFile = null
|
|
72
|
+
|
|
73
|
+
for (const line of diffText.split('\n')) {
|
|
74
|
+
// Only git's file headers count: "+++ b/<path>" or "+++ /dev/null".
|
|
75
|
+
// (An added content line that itself starts with "++ " would otherwise
|
|
76
|
+
// look like a header under --unified=0.)
|
|
77
|
+
if (line.startsWith('+++ b/')) {
|
|
78
|
+
currentFile = line.slice(6).trim()
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
if (line.startsWith('+++ /dev/null')) {
|
|
82
|
+
currentFile = null
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (!line.startsWith('@@') || currentFile === null) continue
|
|
86
|
+
|
|
87
|
+
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/)
|
|
88
|
+
if (!hunk) continue
|
|
89
|
+
const start = parseInt(hunk[1], 10)
|
|
90
|
+
const count = hunk[2] === undefined ? 1 : parseInt(hunk[2], 10)
|
|
91
|
+
if (count === 0) continue // pure deletion: no post-image lines
|
|
92
|
+
|
|
93
|
+
if (!changed.has(currentFile)) changed.set(currentFile, new Set())
|
|
94
|
+
const lines = changed.get(currentFile)
|
|
95
|
+
for (let i = 0; i < count; i++) lines.add(start + i)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return changed
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Group changed files by surface.
|
|
103
|
+
*
|
|
104
|
+
* @param {Map<string, Set<number>>} changedLines - From getChangedLines
|
|
105
|
+
* @returns {Object} surface -> Map(file -> Set(lines))
|
|
106
|
+
*/
|
|
107
|
+
function classifyDiff (changedLines) {
|
|
108
|
+
const bySurface = {}
|
|
109
|
+
for (const [file, lines] of changedLines) {
|
|
110
|
+
const surface = routeFile(file)
|
|
111
|
+
if (!surface) continue
|
|
112
|
+
if (!bySurface[surface]) bySurface[surface] = new Map()
|
|
113
|
+
bySurface[surface].set(file, lines)
|
|
114
|
+
}
|
|
115
|
+
return bySurface
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* True when the inclusive line span [lineStart, lineEnd] contains at least
|
|
120
|
+
* one changed line.
|
|
121
|
+
*/
|
|
122
|
+
function spanIntersects (lineStart, lineEnd, lineSet) {
|
|
123
|
+
if (lineStart == null || lineEnd == null || !lineSet) return false
|
|
124
|
+
for (const line of lineSet) {
|
|
125
|
+
if (line >= lineStart && line <= lineEnd) return true
|
|
126
|
+
}
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
SURFACE_ROUTES,
|
|
132
|
+
routeFile,
|
|
133
|
+
getChangedLines,
|
|
134
|
+
parseUnifiedDiff,
|
|
135
|
+
classifyDiff,
|
|
136
|
+
spanIntersects
|
|
137
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rule runner for lint-strings.
|
|
5
|
+
*
|
|
6
|
+
* Adopts the VALIDATION_RULES shape from tools/rpk-docs/validate-output.js:
|
|
7
|
+
* each rule is { name, description, severity, check() -> issues[] }, with
|
|
8
|
+
* skipRules/onlyRules filtering and a byRule summary. The difference is the
|
|
9
|
+
* unit of work: rules here check one extracted *declaration* (a property,
|
|
10
|
+
* metric, rpk command, ...) rather than a generated .adoc file.
|
|
11
|
+
*
|
|
12
|
+
* Declaration shape (produced by the surface modules in ./surfaces):
|
|
13
|
+
* {
|
|
14
|
+
* surface: 'properties', // surface id
|
|
15
|
+
* name: 'cluster_id', // declared name (null if unresolvable)
|
|
16
|
+
* file: 'src/v/config/...', // path relative to --repo
|
|
17
|
+
* line_start: 1852, // 1-indexed, inclusive
|
|
18
|
+
* line_end: 1857, // spans the FULL declaration
|
|
19
|
+
* string: 'The doc string', // extracted doc string (null if none)
|
|
20
|
+
* declaration_text: '...', // exact source lines of the span
|
|
21
|
+
* convention: { ... }, // surface conventions (echoed in findings)
|
|
22
|
+
* meta: { ... } // surface-specific extras for rules
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Severity levels: error | warning | info.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const SEVERITIES = ['error', 'warning', 'info']
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run rules over a list of declarations.
|
|
32
|
+
*
|
|
33
|
+
* @param {Array} declarations - Declarations from a surface module
|
|
34
|
+
* @param {Array} rules - Rules ({ name, severity, check(decl) -> issues[] }).
|
|
35
|
+
* Optional rule fields:
|
|
36
|
+
* - runOnUnverifiable: also run when decl.meta.unverifiable is set
|
|
37
|
+
* (all other rules are skipped for unverifiable declarations, so a
|
|
38
|
+
* non-literal source string can never produce an error)
|
|
39
|
+
* @param {Object} options - { skipRules: [names], onlyRules: [names] }
|
|
40
|
+
* @returns {Object} { findings, summary: { byRule, bySurface } }
|
|
41
|
+
*/
|
|
42
|
+
function runRules (declarations, rules, options = {}) {
|
|
43
|
+
const { skipRules = [], onlyRules = null } = options
|
|
44
|
+
const findings = []
|
|
45
|
+
const summary = emptySummary()
|
|
46
|
+
|
|
47
|
+
for (const decl of declarations) {
|
|
48
|
+
summary.totalDeclarations++
|
|
49
|
+
ensureSurface(summary, decl.surface).declarations++
|
|
50
|
+
const issues = []
|
|
51
|
+
|
|
52
|
+
for (const rule of rules) {
|
|
53
|
+
if (skipRules.includes(rule.name)) continue
|
|
54
|
+
if (onlyRules && !onlyRules.includes(rule.name)) continue
|
|
55
|
+
// Unverifiable declarations (for example, a metric description built
|
|
56
|
+
// with fmt::format) only run rules that opt in; they must never error.
|
|
57
|
+
if (decl.meta && decl.meta.unverifiable && !rule.runOnUnverifiable) continue
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
for (const issue of rule.check(decl) || []) {
|
|
61
|
+
const severity = issue.severity || rule.severity
|
|
62
|
+
issues.push({
|
|
63
|
+
id: rule.name,
|
|
64
|
+
severity: SEVERITIES.includes(severity) ? severity : 'warning',
|
|
65
|
+
message: issue.message
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
issues.push({
|
|
70
|
+
id: rule.name,
|
|
71
|
+
severity: 'error',
|
|
72
|
+
message: `Rule execution failed: ${err.message}`
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (issues.length === 0) continue
|
|
78
|
+
|
|
79
|
+
findings.push({
|
|
80
|
+
surface: decl.surface,
|
|
81
|
+
name: decl.name,
|
|
82
|
+
file: decl.file,
|
|
83
|
+
line_start: decl.line_start,
|
|
84
|
+
line_end: decl.line_end,
|
|
85
|
+
string: decl.string,
|
|
86
|
+
declaration_text: decl.declaration_text,
|
|
87
|
+
rules: issues,
|
|
88
|
+
convention: decl.convention || {},
|
|
89
|
+
in_pr_diff: Boolean(decl.in_pr_diff)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
tally(summary, decl.surface, issues)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { findings, summary }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function emptySummary () {
|
|
99
|
+
return {
|
|
100
|
+
totalDeclarations: 0,
|
|
101
|
+
flaggedDeclarations: 0,
|
|
102
|
+
errors: 0,
|
|
103
|
+
warnings: 0,
|
|
104
|
+
info: 0,
|
|
105
|
+
byRule: {},
|
|
106
|
+
bySurface: {}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureSurface (summary, surface) {
|
|
111
|
+
if (!summary.bySurface[surface]) {
|
|
112
|
+
summary.bySurface[surface] = { declarations: 0, flagged: 0, errors: 0, warnings: 0, info: 0 }
|
|
113
|
+
}
|
|
114
|
+
return summary.bySurface[surface]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function tally (summary, surface, issues) {
|
|
118
|
+
summary.flaggedDeclarations++
|
|
119
|
+
ensureSurface(summary, surface).flagged++
|
|
120
|
+
|
|
121
|
+
for (const issue of issues) {
|
|
122
|
+
if (!summary.byRule[issue.id]) {
|
|
123
|
+
summary.byRule[issue.id] = { errors: 0, warnings: 0, info: 0 }
|
|
124
|
+
}
|
|
125
|
+
const bucket = issue.severity === 'error' ? 'errors' : issue.severity === 'warning' ? 'warnings' : 'info'
|
|
126
|
+
summary.byRule[issue.id][bucket]++
|
|
127
|
+
summary.bySurface[surface][bucket]++
|
|
128
|
+
summary[bucket]++
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Merge per-surface run results into one combined result.
|
|
134
|
+
*/
|
|
135
|
+
function mergeResults (results) {
|
|
136
|
+
const merged = { findings: [], summary: emptySummary() }
|
|
137
|
+
for (const result of results) {
|
|
138
|
+
merged.findings.push(...result.findings)
|
|
139
|
+
const s = result.summary
|
|
140
|
+
merged.summary.totalDeclarations += s.totalDeclarations
|
|
141
|
+
merged.summary.flaggedDeclarations += s.flaggedDeclarations
|
|
142
|
+
merged.summary.errors += s.errors
|
|
143
|
+
merged.summary.warnings += s.warnings
|
|
144
|
+
merged.summary.info += s.info
|
|
145
|
+
for (const [rule, counts] of Object.entries(s.byRule)) {
|
|
146
|
+
if (!merged.summary.byRule[rule]) merged.summary.byRule[rule] = { errors: 0, warnings: 0, info: 0 }
|
|
147
|
+
merged.summary.byRule[rule].errors += counts.errors
|
|
148
|
+
merged.summary.byRule[rule].warnings += counts.warnings
|
|
149
|
+
merged.summary.byRule[rule].info += counts.info
|
|
150
|
+
}
|
|
151
|
+
for (const [surface, counts] of Object.entries(s.bySurface)) {
|
|
152
|
+
if (!merged.summary.bySurface[surface]) {
|
|
153
|
+
merged.summary.bySurface[surface] = { declarations: 0, flagged: 0, errors: 0, warnings: 0, info: 0 }
|
|
154
|
+
}
|
|
155
|
+
for (const key of Object.keys(counts)) {
|
|
156
|
+
merged.summary.bySurface[surface][key] += counts[key]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return merged
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { runRules, mergeResults, SEVERITIES }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Package lintfixture is a lint-strings test fixture mirroring connect
|
|
2
|
+
// ConfigSpec shapes. It only needs to parse as Go text for the scanner.
|
|
3
|
+
package lintfixture
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"github.com/redpanda-data/benthos/v4/public/service"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
const (
|
|
10
|
+
// Field name constants resolve like the kfc* pattern in franz_client.go.
|
|
11
|
+
fixFieldSeedBrokers = "seed_brokers"
|
|
12
|
+
fixFieldClientID = "client_id"
|
|
13
|
+
|
|
14
|
+
fixFieldSeedBrokersDescription = "A list of broker addresses to connect to in order to establish the connection."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func fixtureInputConfig() *service.ConfigSpec {
|
|
18
|
+
return service.NewConfigSpec().
|
|
19
|
+
Categories("Services").
|
|
20
|
+
Summary(`A fixture input using the https://example.com[example client library^].`).
|
|
21
|
+
Description(`
|
|
22
|
+
When a consumer group is specified this input consumes one or more topics.
|
|
23
|
+
|
|
24
|
+
This fixture exercises the ` + "`raw` + interpreted + `raw`" + ` concatenation idiom.
|
|
25
|
+
|
|
26
|
+
== Metadata
|
|
27
|
+
|
|
28
|
+
This heading and the table below are legitimate AsciiDoc:
|
|
29
|
+
|
|
30
|
+
|===
|
|
31
|
+
| kafka_key | The record key
|
|
32
|
+
|===
|
|
33
|
+
`).
|
|
34
|
+
Fields(fixtureConfigFields()...)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func fixtureConfigFields() []*service.ConfigField {
|
|
38
|
+
return []*service.ConfigField{
|
|
39
|
+
service.NewStringListField(fixFieldSeedBrokers).
|
|
40
|
+
Description(fixFieldSeedBrokersDescription + " When omitted the global block is referenced.").
|
|
41
|
+
Example([]string{"localhost:9092"}),
|
|
42
|
+
service.NewStringField(fixFieldClientID).
|
|
43
|
+
Description("An identifier for the client connection.").
|
|
44
|
+
Default("fixture").
|
|
45
|
+
Advanced(),
|
|
46
|
+
// Composite helper with built-in docs: never flagged.
|
|
47
|
+
service.NewTLSToggledField("tls"),
|
|
48
|
+
// Known-bad: a bare constructor with no Description ships blank.
|
|
49
|
+
service.NewStringField("naked_field").
|
|
50
|
+
Default(""),
|
|
51
|
+
// Known-bad: description merely restates the field name.
|
|
52
|
+
service.NewDurationField("poll_interval").
|
|
53
|
+
Description("Poll interval."),
|
|
54
|
+
// Deprecated fields are exempt from the missing-description rule.
|
|
55
|
+
service.NewStringField("old_token").Deprecated().Default(""),
|
|
56
|
+
// Dynamic description: skipped silently, never guessed at.
|
|
57
|
+
service.NewStringField("dynamic_field").
|
|
58
|
+
Description(dynamicDescription()),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func init() {
|
|
63
|
+
service.MustRegisterBatchInput("fixture_input", fixtureInputConfig(),
|
|
64
|
+
func(conf *service.ParsedConfig, mgr *service.Resources) (service.BatchInput, error) {
|
|
65
|
+
return nil, nil
|
|
66
|
+
})
|
|
67
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# lint-strings test fixture mirroring operator/crd-ref-docs-config.yaml.
|
|
2
|
+
processor:
|
|
3
|
+
ignoreTypes:
|
|
4
|
+
- 'List$'
|
|
5
|
+
- 'Deprecated.*$'
|
|
6
|
+
ignoreFields:
|
|
7
|
+
- 'migration$'
|
|
8
|
+
customMarkers:
|
|
9
|
+
- name: "hidefromdoc"
|
|
10
|
+
target: field
|
|
11
|
+
- name: "hidefromdoc"
|
|
12
|
+
target: type
|
|
13
|
+
|
|
14
|
+
render:
|
|
15
|
+
kubernetesVersion: 1.28
|