@redpanda-data/docs-extensions-and-macros 5.14.0 → 5.22.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.
Files changed (56) hide show
  1. package/bin/doc-tools-mcp.js +88 -0
  2. package/bin/doc-tools.js +334 -102
  3. package/bin/mcp-tools/index.js +20 -0
  4. package/bin/mcp-tools/lint-doc-strings.js +75 -0
  5. package/bin/mcp-tools/overrides-audit.js +100 -0
  6. package/bin/mcp-tools/preview-doc-string.js +71 -0
  7. package/cli-utils/diff-utils.js +77 -0
  8. package/cli-utils/helm-commented-values.js +172 -23
  9. package/cli-utils/validation-result.js +53 -0
  10. package/macros/REFERENCE.adoc +3 -2
  11. package/macros/prop.js +34 -6
  12. package/package.json +2 -1
  13. package/tools/generate-cli-docs.js +28 -67
  14. package/tools/lint-strings/diff.js +137 -0
  15. package/tools/lint-strings/engine.js +163 -0
  16. package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
  17. package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
  18. package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
  19. package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
  20. package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
  21. package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
  22. package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
  23. package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
  24. package/tools/lint-strings/go-source.js +327 -0
  25. package/tools/lint-strings/index.js +229 -0
  26. package/tools/lint-strings/rules/common.js +148 -0
  27. package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
  28. package/tools/lint-strings/source-text.js +41 -0
  29. package/tools/lint-strings/surfaces/connect.js +233 -0
  30. package/tools/lint-strings/surfaces/crd.js +239 -0
  31. package/tools/lint-strings/surfaces/helm.js +159 -0
  32. package/tools/lint-strings/surfaces/metrics.js +368 -0
  33. package/tools/lint-strings/surfaces/properties.js +216 -0
  34. package/tools/lint-strings/surfaces/rpk.js +377 -0
  35. package/tools/overrides-audit/README.adoc +152 -0
  36. package/tools/overrides-audit/adapters/connect.js +144 -0
  37. package/tools/overrides-audit/adapters/properties.js +117 -0
  38. package/tools/overrides-audit/adapters/rpk.js +192 -0
  39. package/tools/overrides-audit/classify.js +546 -0
  40. package/tools/overrides-audit/index.js +185 -0
  41. package/tools/preview-string/index.js +245 -0
  42. package/tools/property-extractor/Makefile +9 -4
  43. package/tools/property-extractor/README.adoc +17 -3
  44. package/tools/property-extractor/helpers/index.js +1 -2
  45. package/tools/property-extractor/helpers/seeAlsoView.js +76 -0
  46. package/tools/property-extractor/parser.py +26 -0
  47. package/tools/property-extractor/property_extractor.py +65 -6
  48. package/tools/property-extractor/templates/property.hbs +25 -37
  49. package/tools/property-extractor/templates/topic-property.hbs +25 -37
  50. package/tools/property-extractor/transformers.py +15 -2
  51. package/tools/property-extractor/validate-overrides.js +120 -0
  52. package/tools/rpk-docs/detect-platform-commands.js +8 -1
  53. package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
  54. package/tools/rpk-docs/validate-overrides.js +1 -45
  55. package/tools/property-extractor/helpers/allTopicsConditional.js +0 -19
  56. package/tools/property-extractor/helpers/parseRelatedTopic.js +0 -13
package/macros/prop.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * prop:cloud_storage_enabled[]
8
8
  * prop:iceberg_enabled[link=true]
9
9
  * prop:retention.ms[link=true,page=properties/topic-properties]
10
+ * prop:cloud_storage_enabled[link=true,page=cloud:reference:properties/cluster-properties]
10
11
  * prop:write_caching_default[text=write caching]
11
12
  *
12
13
  * The property renders as a marked <code> element that the docs UI decorates
@@ -54,11 +55,17 @@
54
55
  * a page in this component actually documents it. The page is discovered by
55
56
  * indexing which reference-module page documents each property, scanning
56
57
  * property headings in the partials each page includes and respecting include
57
- * tag filters such as tags=redpanda-cloud. Links are therefore
58
+ * tag filters such as tags=redpanda-cloud. Discovered links are therefore
58
59
  * component-relative and stay correct if properties are split across
59
- * different pages later. Nothing is ever linked across components, and no
60
- * page target is guessed from a property's scope: a link the build cannot
61
- * verify would be a broken link. Use page= to set the target explicitly.
60
+ * different pages later: nothing is ever discovered across components, and no
61
+ * page target is guessed from a property's scope, because a link the build
62
+ * cannot verify would be a broken link.
63
+ *
64
+ * Use page= to set the target explicitly instead of discovering it -- an
65
+ * override the build does not verify either way, so it may name any page that
66
+ * documents the property, in this component or another: a bare name
67
+ * (properties/topic-properties) stays module-relative within reference here,
68
+ * while module:page or component:module:page points anywhere else.
62
69
  *
63
70
  * With helm-path=auto, pages rendered with the env-kubernetes attribute
64
71
  * display the property as its Helm values path (storage.tiered.config.*
@@ -814,6 +821,23 @@ function reportUnpublishedProperty ({ name, registry, mode, filePath }) {
814
821
  ))
815
822
  }
816
823
 
824
+ /**
825
+ * Resolve a page= value into the resource ID prefix an xref needs, before the
826
+ * trailing .adoc. A bare name (no colon) is module-relative within reference
827
+ * in the page's own component, matching every page= value published today.
828
+ * Qualifying it with a module (module:page) or a component and module
829
+ * (component:module:page) points the link anywhere else the property is
830
+ * documented: page= is never build-verified (buildPageIndex only runs when
831
+ * page= is absent), so restricting its shape bought no safety, only
832
+ * inflexibility.
833
+ *
834
+ * @param {string} page
835
+ * @returns {string}
836
+ */
837
+ function resolvePageTarget (page) {
838
+ return page.includes(':') ? page : `reference:${page}`
839
+ }
840
+
817
841
  /**
818
842
  * Build the AsciiDoc content emitted for one macro instance. Exported for
819
843
  * unit testing.
@@ -823,7 +847,10 @@ function reportUnpublishedProperty ({ name, registry, mode, filePath }) {
823
847
  * @param {string} [opts.text] - Display text override.
824
848
  * @param {boolean} [opts.link] - Whether to link to the reference page. Ignored
825
849
  * without a page: the macro never guesses a target.
826
- * @param {string} [opts.page] - Reference page (module-relative, no .adoc).
850
+ * @param {string} [opts.page] - Reference page. A bare name (no colon) is
851
+ * module-relative within reference in the current component; qualify it
852
+ * with a module (module:page) or a component and module
853
+ * (component:module:page) to point elsewhere. No .adoc.
827
854
  * @param {string} [opts.scope] - config_scope, used only for the Helm path.
828
855
  * @param {string} opts.role - CSS class for the code element.
829
856
  * @param {boolean} [opts.plain] - Render unmarked code (no tooltip, no link)
@@ -834,7 +861,7 @@ function buildPropContent ({ name, text, link, page, scope, role, helmPath = fal
834
861
  const display = text || (helmPath && helmValuesPath(name, scope)) || name
835
862
  if (plain) return `<code>${display}</code>`
836
863
  const inner = link && page
837
- ? `xref:reference:${page}.adoc#${propertyAnchor(name)}[${display}]`
864
+ ? `xref:${resolvePageTarget(page)}.adoc#${propertyAnchor(name)}[${display}]`
838
865
  : display
839
866
  // Escape every interpolated attribute value. role comes from a site
840
867
  // attribute, docUrl from a page's published URL, and name from a dataset
@@ -980,6 +1007,7 @@ function register (registry, config = {}) {
980
1007
 
981
1008
  module.exports.register = register
982
1009
  module.exports.buildPropContent = buildPropContent
1010
+ module.exports.resolvePageTarget = resolvePageTarget
983
1011
  module.exports.propertyAnchor = propertyAnchor
984
1012
  module.exports.helmValuesPath = helmValuesPath
985
1013
  module.exports.loadPropertiesFor = loadPropertiesFor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.14.0",
3
+ "version": "5.22.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
- // Top-level commands (excluding 'generate' which has subcommands)
281
- const topLevelCommands = [
282
- 'install-test-dependencies',
283
- 'get-redpanda-version',
284
- 'get-console-version',
285
- 'link-readme',
286
- 'fetch',
287
- 'setup-mcp',
288
- 'validate-mcp',
289
- 'preview-prompt',
290
- 'mcp-version'
291
- ];
292
-
293
- topLevelCommands.forEach(cmd => {
294
- console.log(` Generating docs for: ${cmd}`);
295
- const help = getHelpOutput(cmd);
296
- const data = parseHelp(help);
297
- const jsdoc = jsdocs[cmd];
298
- doc += generateCommandDoc(cmd, data, jsdoc, 2);
299
- });
300
-
301
- // Generate command and its subcommands
302
- console.log(' Generating docs for: generate');
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
- // Validate command and its subcommands
332
- console.log(' Generating docs for: validate');
333
- const validateHelp = getHelpOutput('validate');
334
- const validateData = parseHelp(validateHelp);
335
- doc += generateCommandDoc('validate', validateData, null, 2);
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
@@ -0,0 +1,57 @@
1
+ // Package v1alpha2 is a lint-strings test fixture mirroring the operator
2
+ // API type shapes. It only needs to parse as Go text for the scanner.
3
+ package v1alpha2
4
+
5
+ import (
6
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7
+ )
8
+
9
+ // WidgetSpec defines the configuration of a fixture widget.
10
+ type WidgetSpec struct {
11
+ // ClusterSource is a reference to the cluster hosting the widget.
12
+ // It leads with the Go field name, which users never type.
13
+ // +required
14
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="ClusterSource is immutable"
15
+ ClusterSource *ClusterSource `json:"cluster"`
16
+ // Text is the actual unescaped text of a widget. The Go name matches
17
+ // the json name (case only), so this conforms.
18
+ // +required
19
+ Text string `json:"text,omitempty"`
20
+ // Specifies how many replicas to run. Conforming: describes behavior,
21
+ // never names the field.
22
+ // +kubebuilder:default=3
23
+ Replicas *int `json:"replicas,omitempty"`
24
+ // +hidefromdoc
25
+ // Internal knob hidden from the docs; never linted.
26
+ HiddenKnob string `json:"hiddenKnob,omitempty"`
27
+ NotSerialized string `json:"-"`
28
+ }
29
+
30
+ // WidgetReference is a way for one widget to reference another. Its fields
31
+ // are undocumented on purpose (the known-bad this fixture exists for).
32
+ type WidgetReference struct {
33
+ Name string `json:"name"`
34
+ Subject string `json:"subject"`
35
+ Version int `json:"version"`
36
+ }
37
+
38
+ // WidgetList is ignored via the config's ignoreTypes ('List$').
39
+ type WidgetList struct {
40
+ metav1.TypeMeta `json:",inline"`
41
+ UndocumentedButIgnored string `json:"items"`
42
+ }
43
+
44
+ // DeprecatedWidget matches ignoreTypes ('Deprecated.*$').
45
+ type DeprecatedWidget struct {
46
+ Old string `json:"old"`
47
+ }
48
+
49
+ // +hidefromdoc
50
+ // HiddenStruct is hidden from the docs wholesale.
51
+ type HiddenStruct struct {
52
+ Anything string `json:"anything"`
53
+ }
54
+
55
+ func (w *WidgetSpec) helper() bool { // functions between types must not confuse the parser
56
+ return w != nil && len(w.Text) > 0
57
+ }