@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.56.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 +4 -5
- package/bin/doc-tools.js +354 -9
- package/bin/mcp-tools/generated-docs-review.js +26 -9
- package/bin/mcp-tools/overrides-audit.js +1 -1
- package/bin/mcp-tools/rpcn-docs.js +4 -14
- package/cli-utils/docs-links-to-xrefs.js +174 -0
- package/cli-utils/format-helm-spec.js +4 -3
- package/cli-utils/generated-marker.js +43 -0
- package/cli-utils/helm-commented-values.js +125 -21
- package/cli-utils/self-managed-docs-branch.js +36 -30
- package/extension-utils/html-utils.js +27 -0
- package/extension-utils/llms-utils.js +6 -2
- package/extension-utils/metadata-candidates.js +304 -0
- package/extension-utils/metadata-check.js +174 -0
- package/extension-utils/metadata-suggest.js +573 -0
- package/extension-utils/taxonomy-categories.js +106 -0
- package/extension-utils/taxonomy.js +566 -0
- package/extensions/REFERENCE.adoc +110 -8
- package/extensions/add-git-dates.js +13 -1
- package/extensions/aggregate-terms.js +34 -20
- package/extensions/algolia-indexer/generate-index.js +13 -1
- package/extensions/algolia-indexer/index.js +4 -2
- package/extensions/convert-sitemap-to-markdown.js +47 -12
- package/extensions/convert-to-markdown.js +176 -0
- package/extensions/generate-fields-only-pages.js +14 -0
- package/extensions/modify-connect-tag-playbook.js +181 -3
- package/extensions/resolve-xrefs-in-attachments.js +1 -12
- package/extensions/url-to-xref.js +92 -22
- package/extensions/util/connect-docs-asset.js +6 -5
- package/extensions/validate-attributes.js +10 -1
- package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
- package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
- package/extensions/version-fetcher/list-connect-releases.js +3 -3
- package/extensions/version-fetcher/release-lines.js +64 -0
- package/extensions/version-fetcher/set-latest-version.js +73 -3
- package/macros/README.adoc +1 -0
- package/macros/REFERENCE.adoc +99 -0
- package/macros/badge.js +5 -8
- package/macros/enterprise.js +2 -5
- package/macros/glossary.js +4 -14
- package/macros/iceberg-explorer.js +242 -0
- package/macros/prop.js +3 -13
- package/mcp/USER_GUIDE.adoc +25 -135
- package/package.json +5 -2
- package/tools/check-metadata/index.js +523 -0
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
- package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
- package/tools/cloud-regions/render-cloud-regions.js +3 -0
- package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
- package/tools/dispatch-graph/classify-runs.js +128 -0
- package/tools/dispatch-graph/parse-workflows.js +499 -0
- package/tools/doc-impact-recording/index.js +190 -0
- package/tools/enterprise-features/detect-gating-changes.js +393 -0
- package/tools/enterprise-features/verify.js +2 -1
- package/tools/gen-rpk-ascii.py +7 -1
- package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
- package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
- package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
- package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
- package/tools/lint-strings/index.js +133 -3
- package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
- package/tools/lint-strings/surfaces/connect.js +72 -12
- package/tools/lint-strings/surfaces/crd.js +10 -6
- package/tools/metrics/metrics.py +11 -0
- package/tools/overrides-audit/README.adoc +1 -2
- package/tools/overrides-audit/adapters/properties.js +1 -1
- package/tools/overrides-audit/index.js +3 -4
- package/tools/property-extractor/generate-handlebars-docs.js +9 -1
- package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
- package/tools/redpanda-connect/AUTOMATION.md +3 -36
- package/tools/redpanda-connect/README.adoc +5 -33
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +32 -11
- package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
- package/tools/redpanda-connect/report-delta.js +6 -3
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +42 -25
- package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
- package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
- package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
- package/tools/rpk-docs/generate-rpk-docs.js +2 -0
- package/tools/rpk-docs/generate-x-env-partial.js +7 -4
- package/tools/rpk-docs/helpers/index.js +4 -1
- package/tools/rpk-docs/templates/command.hbs +2 -2
- package/tools/overrides-audit/adapters/connect.js +0 -144
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Package lintfixture is a lint-strings test fixture: the help text of
|
|
2
|
+
// rpk topic delete before and after a change whose new wording came from a
|
|
3
|
+
// docs reviewer's comments. It only needs to parse as Go text for the
|
|
4
|
+
// scanner; it is never compiled.
|
|
5
|
+
package lintfixture
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func newDeleteCommand() *cobra.Command {
|
|
12
|
+
var re bool
|
|
13
|
+
cmd := &cobra.Command{
|
|
14
|
+
Use: "delete [TOPICS...]",
|
|
15
|
+
Short: "Delete topics",
|
|
16
|
+
Long: `Delete topics.
|
|
17
|
+
|
|
18
|
+
This command deletes all requested topics, printing the success or fail status
|
|
19
|
+
per topic.
|
|
20
|
+
|
|
21
|
+
The --regex flag (-r) opts into parsing the input topics as regular expressions
|
|
22
|
+
and deleting any non-internal topic that matches any of expressions. The input
|
|
23
|
+
expressions are wrapped with ^ and $ so that the expression must match the
|
|
24
|
+
whole topic name (which also prevents accidental delete-everything mistakes).
|
|
25
|
+
|
|
26
|
+
The topic list command accepts the same input regex format as this delete
|
|
27
|
+
command. If you want to check what your regular expressions will delete before
|
|
28
|
+
actually deleting them, you can check the output of 'rpk topic list -r'.
|
|
29
|
+
|
|
30
|
+
For example,
|
|
31
|
+
|
|
32
|
+
delete foo bar # deletes topics foo and bar
|
|
33
|
+
delete -r '^f.*' '.*r$' # deletes any topic starting with f and any topics ending in r
|
|
34
|
+
delete -r '.*' # deletes all topics
|
|
35
|
+
delete -r . # deletes any one-character topics
|
|
36
|
+
|
|
37
|
+
`,
|
|
38
|
+
Run: func(cmd *cobra.Command, topics []string) {},
|
|
39
|
+
}
|
|
40
|
+
cmd.Flags().BoolVarP(&re, "regex", "r", false, "Parse topics as regex; delete any topic that matches any input topic expression")
|
|
41
|
+
return cmd
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Package lintfixture is a lint-strings test fixture: the help text of
|
|
2
|
+
// rpk topic delete before and after a change whose new wording came from a
|
|
3
|
+
// docs reviewer's comments. It only needs to parse as Go text for the
|
|
4
|
+
// scanner; it is never compiled.
|
|
5
|
+
package lintfixture
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func newDeleteCommand() *cobra.Command {
|
|
12
|
+
var re bool
|
|
13
|
+
cmd := &cobra.Command{
|
|
14
|
+
Use: "delete [TOPICS...]",
|
|
15
|
+
Short: "Delete topics",
|
|
16
|
+
Long: `Delete topics.
|
|
17
|
+
|
|
18
|
+
This command deletes all requested topics, printing the success or fail status
|
|
19
|
+
per topic.
|
|
20
|
+
|
|
21
|
+
The --regex flag (-r) treats each argument as a regular expression and deletes
|
|
22
|
+
every topic whose full name matches any of them. rpk wraps each expression in
|
|
23
|
+
^ and $, so it must match the whole name.
|
|
24
|
+
|
|
25
|
+
Regex mode never deletes internal topics: the topics Redpanda reports as internal,
|
|
26
|
+
which rpk topic list shows only with -i. To delete an internal topic, name it
|
|
27
|
+
exactly, without -r.
|
|
28
|
+
|
|
29
|
+
The topic list command accepts the same input regex format as this delete
|
|
30
|
+
command. If you want to check what your regular expressions will delete before
|
|
31
|
+
actually deleting them, you can check the output of 'rpk topic list -r'.
|
|
32
|
+
|
|
33
|
+
For example,
|
|
34
|
+
|
|
35
|
+
delete foo bar # deletes topics foo and bar
|
|
36
|
+
delete -r '^f.*' '.*r$' # deletes any topic starting with f and any topics ending in r
|
|
37
|
+
delete -r '.*' # deletes all non-internal topics
|
|
38
|
+
delete -r . # deletes any one-character topics
|
|
39
|
+
|
|
40
|
+
`,
|
|
41
|
+
Run: func(cmd *cobra.Command, topics []string) {},
|
|
42
|
+
}
|
|
43
|
+
cmd.Flags().BoolVarP(&re, "regex", "r", false, "Treat the topic arguments as regular expressions and delete every matching topic except internal topics")
|
|
44
|
+
return cmd
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
["```suggestion\n\tcmd.Flags().BoolVarP(\u0026re, \"regex\", \"r\", false, \"Treat the topic arguments as regular expressions and delete every matching topic except internal topics\")\n```","Suggest replacing the `--regex` paragraph with:\n\nThe --regex flag (-r) treats each argument as a regular expression and deletes every topic whose full name matches any of them. rpk wraps each expression in ^ and $, so it must match the whole name.\n\nRegex mode never deletes internal topics: the topics Redpanda reports as internal, which 'rpk topic list' shows only with -i. To delete an internal topic, name it exactly, without -r."]
|
|
@@ -60,6 +60,9 @@ function rulesFor (surface) {
|
|
|
60
60
|
* @param {Set<string>} [options.reviewedFingerprints] - Diff mode: skip
|
|
61
61
|
* declarations (and removals) whose fingerprint is in this set, so a PR
|
|
62
62
|
* review sees each string once across pushes
|
|
63
|
+
* @param {string[]} [options.reviewerPassages] - Diff mode: passages from
|
|
64
|
+
* human review comments. A changed line found in one counts as written by
|
|
65
|
+
* a reviewer, and a declaration whose changed lines all are is skipped
|
|
63
66
|
* @param {Function} [options.log] - Progress logger (stderr by default)
|
|
64
67
|
* @returns {Object} { findings, summary, unsupported_surfaces }
|
|
65
68
|
*/
|
|
@@ -71,6 +74,7 @@ function lintStrings (options) {
|
|
|
71
74
|
skipRules = [],
|
|
72
75
|
onlyRules = null,
|
|
73
76
|
reviewedFingerprints = null,
|
|
77
|
+
reviewerPassages = null,
|
|
74
78
|
log = (msg) => process.stderr.write(`${msg}\n`)
|
|
75
79
|
} = options
|
|
76
80
|
|
|
@@ -93,6 +97,8 @@ function lintStrings (options) {
|
|
|
93
97
|
const reviewed = reviewedFingerprints || new Set()
|
|
94
98
|
const pending = []
|
|
95
99
|
let alreadyReviewed = 0
|
|
100
|
+
const reviewerWritten = []
|
|
101
|
+
const headLines = new Map()
|
|
96
102
|
let removal = { rawFiles: [], declarations: [] }
|
|
97
103
|
const skipped = []
|
|
98
104
|
let unverifiable = 0
|
|
@@ -138,6 +144,17 @@ function lintStrings (options) {
|
|
|
138
144
|
alreadyReviewed++
|
|
139
145
|
continue
|
|
140
146
|
}
|
|
147
|
+
if (reviewerPassages && reviewerPassages.length) {
|
|
148
|
+
const changed = changedLinesOf(repoPath, decl, files.get(decl.file), headLines)
|
|
149
|
+
const own = passagesInDeclaration(decl, reviewerPassages)
|
|
150
|
+
if (changed.length > 0 && changed.every((l) => writtenByReviewer(l.text, own))) {
|
|
151
|
+
// No wording review, but the published-content check still
|
|
152
|
+
// sees it: a reviewer's wording can describe a behavior change
|
|
153
|
+
// that makes another page wrong.
|
|
154
|
+
reviewerWritten.push(pendingEntry(decl))
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
}
|
|
141
158
|
declarations.push(decl)
|
|
142
159
|
if (decl.meta && decl.meta.unverifiable) unverifiable++
|
|
143
160
|
const detail = surface.reviewContext ? surface.reviewContext(decl) : null
|
|
@@ -181,6 +198,8 @@ function lintStrings (options) {
|
|
|
181
198
|
if (diffBase) {
|
|
182
199
|
merged.declarations = pending
|
|
183
200
|
merged.summary.alreadyReviewed = alreadyReviewed
|
|
201
|
+
merged.summary.reviewerWritten = reviewerWritten.length
|
|
202
|
+
merged.summary.reviewerWrittenDeclarations = reviewerWritten
|
|
184
203
|
}
|
|
185
204
|
merged.summary.removedDeclarations = removal.declarations.map(pendingEntry)
|
|
186
205
|
// Declarations whose text has gaps the extractor could not evaluate
|
|
@@ -468,6 +487,9 @@ function formatHuman (result) {
|
|
|
468
487
|
if (summary.alreadyReviewed) {
|
|
469
488
|
lines.push(`Already reviewed on an earlier push (skipped): ${summary.alreadyReviewed}`)
|
|
470
489
|
}
|
|
490
|
+
if (summary.reviewerWritten) {
|
|
491
|
+
lines.push(`Text a human reviewer suggested on the PR (skipped): ${summary.reviewerWritten}`)
|
|
492
|
+
}
|
|
471
493
|
for (const decl of summary.removedDeclarations || []) {
|
|
472
494
|
lines.push(`Removed or renamed: ${decl.name} (${decl.surface}, ${decl.file})`)
|
|
473
495
|
}
|
|
@@ -520,6 +542,102 @@ function readFingerprints (file) {
|
|
|
520
542
|
return new Set(text.split(/[\s,]+/).filter((token) => /^[0-9a-f]{16}$/.test(token)))
|
|
521
543
|
}
|
|
522
544
|
|
|
545
|
+
// Shorter passages are labels, lead-ins ("Suggest replacing this with:") or
|
|
546
|
+
// fragments that occur in many strings, so matching on them would skip
|
|
547
|
+
// declarations nobody reviewed.
|
|
548
|
+
const MIN_REVIEWER_PASSAGE = 40
|
|
549
|
+
|
|
550
|
+
function normalizeText (text) {
|
|
551
|
+
return String(text == null ? '' : text).replace(/\s+/g, ' ').trim()
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Passages from human review comments that a declaration can be matched
|
|
556
|
+
* against: the body of every ```suggestion block, and every prose paragraph
|
|
557
|
+
* outside a code block. Whitespace is collapsed so a paragraph the author
|
|
558
|
+
* re-wrapped to the file's line length still matches.
|
|
559
|
+
*/
|
|
560
|
+
function reviewerPassagesFrom (bodies) {
|
|
561
|
+
const passages = new Set()
|
|
562
|
+
for (const body of Array.isArray(bodies) ? bodies : []) {
|
|
563
|
+
if (typeof body !== 'string') continue
|
|
564
|
+
const chunks = [...body.matchAll(/```suggestion[^\n]*\n([\s\S]*?)```/g)].map((m) => m[1])
|
|
565
|
+
chunks.push(...body.replace(/```[\s\S]*?(```|$)/g, '\n\n').split(/\n\s*\n/))
|
|
566
|
+
for (const chunk of chunks) {
|
|
567
|
+
const text = normalizeText(chunk)
|
|
568
|
+
if (text.length >= MIN_REVIEWER_PASSAGE) passages.add(text)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return [...passages]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// A changed line shorter than this ("per topic.", "For example,") occurs in
|
|
575
|
+
// too many paragraphs to say who wrote it, so it never counts as a
|
|
576
|
+
// reviewer's. That errs toward reviewing.
|
|
577
|
+
const MIN_REVIEWER_LINE = 12
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The lines this PR changed inside a declaration's span, as HEAD has them.
|
|
581
|
+
* Lines with no letters or digits (a closing backtick, a blank line) say
|
|
582
|
+
* nothing about who wrote the string and are left out.
|
|
583
|
+
*/
|
|
584
|
+
function changedLinesOf (repoPath, decl, lineSet, cache) {
|
|
585
|
+
if (!lineSet) return []
|
|
586
|
+
if (!cache.has(decl.file)) {
|
|
587
|
+
let lines = null
|
|
588
|
+
try {
|
|
589
|
+
lines = fs.readFileSync(path.join(repoPath, decl.file), 'utf8').split('\n')
|
|
590
|
+
} catch {}
|
|
591
|
+
cache.set(decl.file, lines)
|
|
592
|
+
}
|
|
593
|
+
const lines = cache.get(decl.file)
|
|
594
|
+
if (!lines) return []
|
|
595
|
+
const out = []
|
|
596
|
+
for (const [start, end] of spansOf(decl)) {
|
|
597
|
+
if (start == null || end == null) continue
|
|
598
|
+
for (let n = start; n <= end; n++) {
|
|
599
|
+
if (!lineSet.has(n) || n > lines.length) continue
|
|
600
|
+
const text = normalizeText(lines[n - 1])
|
|
601
|
+
if (/[A-Za-z0-9]/.test(text)) out.push({ line: n, text })
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return out
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* The passages a declaration contains whole. A reviewer's proposed wording
|
|
609
|
+
* lands in the string as written, but a comment that quotes the string to
|
|
610
|
+
* criticize it ("Please change this: ...") carries a lead-in the string
|
|
611
|
+
* does not have, so it never counts as the reviewer writing the string.
|
|
612
|
+
*/
|
|
613
|
+
function passagesInDeclaration (decl, passages) {
|
|
614
|
+
const forms = [normalizeText(decl.string), normalizeText(decl.declaration_text)].filter(Boolean)
|
|
615
|
+
return passages.filter((passage) => forms.some((form) => form.includes(passage)))
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* True when a human reviewer wrote this line on the PR: it appears whole in
|
|
620
|
+
* one of their passages. Matching per line, not per string, means an author
|
|
621
|
+
* who keeps a reviewer's paragraph and rewrites the next one still gets the
|
|
622
|
+
* new paragraph reviewed (streaming-enterprise PR 461 is the case this
|
|
623
|
+
* exists for: the review argued with help text applied from a reviewer).
|
|
624
|
+
*/
|
|
625
|
+
function writtenByReviewer (line, passages) {
|
|
626
|
+
return line.length >= MIN_REVIEWER_LINE && passages.some((passage) => passage.includes(line))
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Read a --reviewer-comments file: a JSON array of comment bodies. A missing
|
|
631
|
+
* or unparseable file yields no passages, so it means "review everything".
|
|
632
|
+
*/
|
|
633
|
+
function readReviewerComments (file) {
|
|
634
|
+
try {
|
|
635
|
+
return reviewerPassagesFrom(JSON.parse(fs.readFileSync(file, 'utf8')))
|
|
636
|
+
} catch {
|
|
637
|
+
return []
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
523
641
|
/**
|
|
524
642
|
* CLI entry point shared by bin/doc-tools.js and direct invocation
|
|
525
643
|
* (node tools/lint-strings --repo <path> ...).
|
|
@@ -536,7 +654,8 @@ function runCli (options) {
|
|
|
536
654
|
diffBase: options.diff || null,
|
|
537
655
|
skipRules: options.skipRules ? String(options.skipRules).split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
538
656
|
onlyRules: options.onlyRules ? String(options.onlyRules).split(',').map((s) => s.trim()).filter(Boolean) : null,
|
|
539
|
-
reviewedFingerprints: options.reviewed ? readFingerprints(options.reviewed) : null
|
|
657
|
+
reviewedFingerprints: options.reviewed ? readFingerprints(options.reviewed) : null,
|
|
658
|
+
reviewerPassages: options.reviewerComments ? readReviewerComments(options.reviewerComments) : null
|
|
540
659
|
})
|
|
541
660
|
} catch (err) {
|
|
542
661
|
console.error(`Error: ${err.message}`)
|
|
@@ -555,7 +674,7 @@ function runCli (options) {
|
|
|
555
674
|
process.exitCode = options.strict && result.summary.errors > 0 ? 1 : 0
|
|
556
675
|
}
|
|
557
676
|
|
|
558
|
-
module.exports = { lintStrings, formatHuman, runCli, SURFACES, rulesFor, fingerprint, readFingerprints, pageContext }
|
|
677
|
+
module.exports = { lintStrings, formatHuman, runCli, SURFACES, rulesFor, fingerprint, readFingerprints, reviewerPassagesFrom, readReviewerComments, pageContext }
|
|
559
678
|
|
|
560
679
|
// Direct usage: node tools/lint-strings --repo <path> [--surface a,b]
|
|
561
680
|
// [--diff <base>] [--format json|human] [--strict]
|
|
@@ -571,10 +690,21 @@ if (require.main === module) {
|
|
|
571
690
|
else if (arg === '--skip-rules') options.skipRules = args[++i]
|
|
572
691
|
else if (arg === '--only-rules') options.onlyRules = args[++i]
|
|
573
692
|
else if (arg === '--reviewed') options.reviewed = args[++i]
|
|
693
|
+
else if (arg === '--reviewer-comments') {
|
|
694
|
+
// A present flag with no operand must not act like an absent one, or
|
|
695
|
+
// `--reviewer-comments --strict` would read --strict as the file and
|
|
696
|
+
// quietly turn strict mode off.
|
|
697
|
+
const operand = args[i + 1]
|
|
698
|
+
if (!operand || operand.startsWith('-')) {
|
|
699
|
+
console.error('--reviewer-comments requires a file path')
|
|
700
|
+
process.exit(2)
|
|
701
|
+
}
|
|
702
|
+
options.reviewerComments = args[++i]
|
|
703
|
+
}
|
|
574
704
|
else if (arg === '--strict') options.strict = true
|
|
575
705
|
else {
|
|
576
706
|
console.error(`Unknown argument: ${arg}`)
|
|
577
|
-
console.error('Usage: node tools/lint-strings --repo <path> [--surface a,b] [--diff <base>] [--format json|human] [--skip-rules a,b] [--only-rules a,b] [--reviewed <file>] [--strict]')
|
|
707
|
+
console.error('Usage: node tools/lint-strings --repo <path> [--surface a,b] [--diff <base>] [--format json|human] [--skip-rules a,b] [--only-rules a,b] [--reviewed <file>] [--reviewer-comments <file>] [--strict]')
|
|
578
708
|
process.exit(2)
|
|
579
709
|
}
|
|
580
710
|
}
|
|
@@ -105,4 +105,169 @@ const VERBATIM_ASCIIDOC_RULES = [
|
|
|
105
105
|
}
|
|
106
106
|
]
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* AsciiDoc preprocessor conditionals in doc strings that publish to two
|
|
110
|
+
* audiences. Connect spec strings are published on the self-managed Connect
|
|
111
|
+
* docs and on the Redpanda Cloud docs, which set the `env-cloud` attribute,
|
|
112
|
+
* and each Connect binary renders the same strings as Markdown with the
|
|
113
|
+
* attribute set only in the Cloud binaries. An author who wraps text whose
|
|
114
|
+
* behavior differs in Cloud has one convention:
|
|
115
|
+
*
|
|
116
|
+
* - `env-cloud` is the only attribute;
|
|
117
|
+
* - block form only: `ifdef::env-cloud[]` or `ifndef::env-cloud[]` alone
|
|
118
|
+
* on a line at column 0, the whole paragraph inside, `endif::[]` alone on
|
|
119
|
+
* a line, a blank line before the opening directive and after the closing
|
|
120
|
+
* one (unless the string starts or ends there);
|
|
121
|
+
* - nothing after the closing bracket, not even whitespace: the Markdown
|
|
122
|
+
* converter in the Connect binaries only treats a line ending in `]` as
|
|
123
|
+
* a directive;
|
|
124
|
+
* - never the single-line form `ifndef::env-cloud[text]`;
|
|
125
|
+
* - never in a Summary, which renders as one line, or in an example
|
|
126
|
+
* summary;
|
|
127
|
+
* - never inside a code fence;
|
|
128
|
+
* - never `ifndef::env-cloud[]` inside `ifdef::env-cloud[]` or the
|
|
129
|
+
* reverse, whose inner text renders for neither audience.
|
|
130
|
+
*
|
|
131
|
+
* Links to pages the other site lacks are handled by the renderers, so a
|
|
132
|
+
* conditional is only for behavior that differs. Not part of
|
|
133
|
+
* VERBATIM_ASCIIDOC_RULES: a surface that publishes to both audiences opts
|
|
134
|
+
* in (connect does); the other verbatim surfaces carry audience conditionals
|
|
135
|
+
* as data, not in the string.
|
|
136
|
+
*/
|
|
137
|
+
const CONDITIONAL_ATTRIBUTE = 'env-cloud'
|
|
138
|
+
const CONDITIONAL_WHY = 'The Cloud docs and the Cloud binaries set env-cloud, and the text inside must read as a whole paragraph in both outputs. See docs/README.md in the connect repo.'
|
|
139
|
+
const CONDITIONAL_TOKEN = /\b(?:ifdef|ifndef|ifeval|endif)::/
|
|
140
|
+
// A directive on its own line: optional indent (reported), kind, attribute
|
|
141
|
+
// list, bracketed text, then optional trailing whitespace (reported).
|
|
142
|
+
const CONDITIONAL_LINE = /^(\s*)(ifdef|ifndef|ifeval|endif)::([^\s[\]]*)\[(.*)\](\s*)$/
|
|
143
|
+
// Listing and literal delimiters, and Markdown fences with an optional
|
|
144
|
+
// language tag (```yaml). Tracked so a directive inside one is reported; the
|
|
145
|
+
// preprocessor still evaluates it there.
|
|
146
|
+
const CODE_FENCE = /^(?:(-{4,}|\.{4,})|(`{3,})([\w.+#-]*))[ \t]*$/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Which summary a declaration is, or null. Summaries render as one line, and
|
|
150
|
+
* connect's docs test rejects a conditional in any JSON `summary` key, which
|
|
151
|
+
* includes example summaries.
|
|
152
|
+
*/
|
|
153
|
+
function summaryKind (decl) {
|
|
154
|
+
const meta = decl.meta || {}
|
|
155
|
+
if (meta.kind === 'summary' || (meta.kind === 'bloblang' && meta.part === 'summary')) return 'a Summary'
|
|
156
|
+
if (meta.kind === 'example-summary' || meta.kind === 'bloblang-example') return 'an example summary'
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Advance the code-fence state over one fence line. An AsciiDoc block closes
|
|
162
|
+
* only on a delimiter of the same character and length; a Markdown fence
|
|
163
|
+
* closes on a bare fence at least as long as the opener.
|
|
164
|
+
*/
|
|
165
|
+
function stepFence (fence, match) {
|
|
166
|
+
const [, adoc, ticks, lang] = match
|
|
167
|
+
if (fence === null) return adoc ? { char: adoc[0], length: adoc.length } : { char: '`', length: ticks.length }
|
|
168
|
+
if (adoc) return fence.char === adoc[0] && fence.length === adoc.length ? null : fence
|
|
169
|
+
return fence.char === '`' && !lang && ticks.length >= fence.length ? null : fence
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const CONDITIONAL_RULE = {
|
|
173
|
+
name: 'conditional',
|
|
174
|
+
description: 'AsciiDoc conditional that is not a well-formed env-cloud block',
|
|
175
|
+
severity: 'warning',
|
|
176
|
+
check: (decl) => {
|
|
177
|
+
const text = decl.string || ''
|
|
178
|
+
if (!text || !CONDITIONAL_TOKEN.test(text)) return []
|
|
179
|
+
// CLI help is not AsciiDoc; the surface marks those declarations.
|
|
180
|
+
if (decl.convention && decl.convention.verbatim_asciidoc === false) return []
|
|
181
|
+
const issues = []
|
|
182
|
+
const say = (what, severity) => issues.push({ message: `${what} ${CONDITIONAL_WHY}`, ...(severity ? { severity } : {}) })
|
|
183
|
+
|
|
184
|
+
const summary = summaryKind(decl)
|
|
185
|
+
if (summary) {
|
|
186
|
+
say(`Contains an AsciiDoc conditional in ${summary}. A summary renders as one line, so it cannot hold a conditional block; keep the summary the same for both audiences and put the differing paragraph in the Description.`)
|
|
187
|
+
return issues
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lines = text.split('\n')
|
|
191
|
+
const open = []
|
|
192
|
+
let fence = null
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
const line = lines[i]
|
|
195
|
+
const fenceMatch = line.match(CODE_FENCE)
|
|
196
|
+
if (fenceMatch) {
|
|
197
|
+
fence = stepFence(fence, fenceMatch)
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
const m = line.match(CONDITIONAL_LINE)
|
|
201
|
+
if (!m) {
|
|
202
|
+
if (CONDITIONAL_TOKEN.test(line)) {
|
|
203
|
+
say(`"${line.trim().slice(0, 60)}" is not a directive on a line of its own at column 0. Put ifdef::env-cloud[], ifndef::env-cloud[] and endif::[] each on their own line with nothing before them.`)
|
|
204
|
+
}
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
const [, indent, kind, attribute, body, trailing] = m
|
|
208
|
+
const label = `${kind}::${attribute}[${body}]`
|
|
209
|
+
// What is between the brackets decides how Asciidoctor reads the line.
|
|
210
|
+
const bracket = body === '' ? 'empty' : body.trim() === '' ? 'whitespace' : 'text'
|
|
211
|
+
if (fence !== null) {
|
|
212
|
+
// Reported on its own: the form checks below assume prose around
|
|
213
|
+
// the block, and a fence line is not prose.
|
|
214
|
+
say(`${label} is inside a code fence. Keep conditionals out of code fences; wrap the whole paragraph or block instead.`)
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (indent) {
|
|
218
|
+
// Asciidoctor and the Connect Markdown converter both read an
|
|
219
|
+
// indented directive as text, so it neither opens nor closes a block
|
|
220
|
+
// here either: the never-closed and no-matching-opener errors below
|
|
221
|
+
// then report the block it leaves unbalanced.
|
|
222
|
+
say(`${label} is indented. A directive starts at column 0, or AsciiDoc and the Connect binaries treat it as text and it does not open or close a block.`)
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (trailing) {
|
|
226
|
+
say(`${label} has trailing whitespace after the closing bracket. The Connect binaries only treat a line ending in ] as a directive, so CLI readers would see the directive and the text for the other audience. Remove everything after the ].`, 'error')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (kind === 'ifeval') {
|
|
230
|
+
say(`${label} uses ifeval. env-cloud is the only attribute a doc string may test, with ifdef::env-cloud[] or ifndef::env-cloud[].`)
|
|
231
|
+
open.push({ label, line: i })
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (kind === 'endif') {
|
|
235
|
+
if (attribute && attribute !== CONDITIONAL_ATTRIBUTE) say(`${label} names "${attribute}". env-cloud is the only attribute; close the block with endif::[].`)
|
|
236
|
+
if (bracket !== 'empty') {
|
|
237
|
+
// Asciidoctor rejects an endif with anything in its brackets and
|
|
238
|
+
// leaves the block open, so it does not close one here either: the
|
|
239
|
+
// never-closed error below then reports the block it leaves open.
|
|
240
|
+
say(`${label} has ${bracket === 'text' ? 'text' : 'whitespace'} inside its brackets. Asciidoctor rejects it and leaves the block open, so the text after it is hidden from one audience. Write endif::[] with nothing between the brackets.`, 'error')
|
|
241
|
+
} else if (open.length === 0) say(`${label} has no matching ifdef::env-cloud[] or ifndef::env-cloud[] before it.`, 'error')
|
|
242
|
+
else open.pop()
|
|
243
|
+
if (i < lines.length - 1 && lines[i + 1].trim() !== '') say(`${label} needs a blank line after it, so the paragraph inside ends where the block ends.`)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
// ifdef / ifndef
|
|
247
|
+
if (attribute !== CONDITIONAL_ATTRIBUTE) say(`${label} tests "${attribute}". env-cloud is the only attribute a doc string may test.`)
|
|
248
|
+
if (bracket === 'whitespace') {
|
|
249
|
+
// Asciidoctor reads any bracket content, whitespace included, as the
|
|
250
|
+
// single-line form, which opens no block: the text that follows shows
|
|
251
|
+
// for both audiences. So it is not pushed as an opener, and the endif
|
|
252
|
+
// that follows reports no matching opener.
|
|
253
|
+
say(`${label} has whitespace inside its brackets. Asciidoctor then reads it as the single-line form, which opens no block, so the text after it shows for both audiences. Write ${kind}::${attribute}[] with nothing between the brackets.`, 'error')
|
|
254
|
+
} else if (bracket === 'text') {
|
|
255
|
+
say(`${label} is the single-line form. Use the block form: the directive alone on its line, the whole paragraph inside, then endif::[] on its own line.`)
|
|
256
|
+
} else {
|
|
257
|
+
if (attribute === CONDITIONAL_ATTRIBUTE) {
|
|
258
|
+
const opposite = kind === 'ifdef' ? 'ifndef' : 'ifdef'
|
|
259
|
+
const outer = open.find((o) => o.kind === opposite && o.attribute === CONDITIONAL_ATTRIBUTE)
|
|
260
|
+
if (outer) say(`${label} is inside ${outer.label}, so the text between them renders for neither audience. Close the outer block with endif::[] before opening the other one.`)
|
|
261
|
+
}
|
|
262
|
+
open.push({ label, kind, attribute, line: i })
|
|
263
|
+
}
|
|
264
|
+
if (i > 0 && lines[i - 1].trim() !== '') say(`${label} needs a blank line before it, so the paragraph inside starts where the block starts.`)
|
|
265
|
+
}
|
|
266
|
+
for (const { label } of open) {
|
|
267
|
+
say(`${label} is never closed with endif::[]. Everything after it is hidden from one audience, or the rest of the page is.`, 'error')
|
|
268
|
+
}
|
|
269
|
+
return issues
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { VERBATIM_ASCIIDOC_RULES, CONDITIONAL_RULE, KNOWN_ATTRIBUTES, MACRO_NAMES, countRawPipes }
|
|
@@ -8,6 +8,7 @@ const { SourceCache } = require('../source-text')
|
|
|
8
8
|
const { splitTopLevelArgs, findBalancedClose, collectGoFiles } = require('../go-source')
|
|
9
9
|
const { GoIndex, statementEnd, straightLineReturn, stripStrings, renderResult, isResolved, unresolvedParts } = require('../go-index')
|
|
10
10
|
const { parseLiteralFields } = require('./rpk')
|
|
11
|
+
const { CONDITIONAL_RULE } = require('../rules/verbatim-asciidoc')
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Connect surface: every published string a Redpanda Connect build defines.
|
|
@@ -18,6 +19,11 @@ const { parseLiteralFields } = require('./rpk')
|
|
|
18
19
|
* the option descriptions of annotated enum fields.
|
|
19
20
|
* - Bloblang plugins (bloblang.PluginSpec): Description, Example summaries,
|
|
20
21
|
* and parameter descriptions.
|
|
22
|
+
* - benthos's internal field API (internal/docs): the description argument
|
|
23
|
+
* of docs.FieldString, docs.FieldObject, docs.FieldBool and the other
|
|
24
|
+
* docs.Field constructors, which document the shared config blocks
|
|
25
|
+
* (http_server, tls, logger, batching, ...) that benthos builds outside
|
|
26
|
+
* the public service API.
|
|
21
27
|
* - `rpk connect` help (urfave/cli): command Usage, UsageText, ArgsUsage and
|
|
22
28
|
* Description, and flag Usage.
|
|
23
29
|
*
|
|
@@ -87,6 +93,17 @@ const BARE_FIELD_CTORS = new Set([
|
|
|
87
93
|
|
|
88
94
|
const SERVICE_PKG = 'github.com/redpanda-data/benthos/v4/public/service'
|
|
89
95
|
const BLOBLANG_PKG = 'github.com/redpanda-data/benthos/v4/public/bloblang'
|
|
96
|
+
const DOCS_PKG = 'github.com/redpanda-data/benthos/v4/internal/docs'
|
|
97
|
+
|
|
98
|
+
// internal/docs constructors with the signature
|
|
99
|
+
// (name, description string, examples ...any). docs.FieldComponent takes
|
|
100
|
+
// no description and is left out.
|
|
101
|
+
const DOCS_FIELD_CTORS = [
|
|
102
|
+
'FieldAnything', 'FieldObject', 'FieldString', 'FieldInterpolatedString', 'FieldBloblang',
|
|
103
|
+
'FieldInt', 'FieldFloat', 'FieldBool', 'FieldURL',
|
|
104
|
+
'FieldInput', 'FieldProcessor', 'FieldOutput', 'FieldBuffer', 'FieldCache',
|
|
105
|
+
'FieldRateLimit', 'FieldMetrics', 'FieldTracer', 'FieldScanner'
|
|
106
|
+
]
|
|
90
107
|
const CLI_PKG = /^github\.com\/urfave\/cli(?:\/v\d+)?$/
|
|
91
108
|
|
|
92
109
|
// Repos whose urfave/cli commands are `rpk connect` help.
|
|
@@ -331,6 +348,7 @@ class ConnectScanner {
|
|
|
331
348
|
file._aliases = {
|
|
332
349
|
service: aliasesFor(file, SERVICE_PKG),
|
|
333
350
|
bloblang: aliasesFor(file, BLOBLANG_PKG),
|
|
351
|
+
docs: aliasesFor(file, DOCS_PKG),
|
|
334
352
|
cli: aliasesFor(file, CLI_PKG)
|
|
335
353
|
}
|
|
336
354
|
}
|
|
@@ -804,6 +822,7 @@ class ConnectScanner {
|
|
|
804
822
|
const skipped = []
|
|
805
823
|
const { service, bloblang, cli } = this.aliases(file)
|
|
806
824
|
if (service.size > 0 || bloblang.size > 0) this.scanSpecs(file, rel, declarations, skipped)
|
|
825
|
+
this.scanDocsFields(file, rel, declarations)
|
|
807
826
|
if (cli.size > 0 && this.cli) this.scanCli(file, rel, declarations)
|
|
808
827
|
return { declarations, skipped }
|
|
809
828
|
}
|
|
@@ -1055,6 +1074,53 @@ class ConnectScanner {
|
|
|
1055
1074
|
this.scanJsFunctions(file, rel, declarations)
|
|
1056
1075
|
}
|
|
1057
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* benthos's internal field API: `docs.FieldString(name, description)`
|
|
1079
|
+
* and the other internal/docs constructors. The description is the second
|
|
1080
|
+
* argument, not a `.Description()` call, so scanSpecs never sees it.
|
|
1081
|
+
* Inside internal/docs itself the constructors are unqualified. An empty
|
|
1082
|
+
* description is skipped: internal/docs uses `FieldString("type", "")`
|
|
1083
|
+
* for structural fields that no page documents.
|
|
1084
|
+
*/
|
|
1085
|
+
scanDocsFields (file, rel, declarations) {
|
|
1086
|
+
const { docs } = this.aliases(file)
|
|
1087
|
+
const mod = this.index.gomod.module
|
|
1088
|
+
const inPackage = Boolean(mod) && `${mod}/${this.index.rel(file.dir).split(path.sep).join('/')}` === DOCS_PKG
|
|
1089
|
+
if (docs.size === 0 && !inPackage) return
|
|
1090
|
+
const qual = docs.size > 0 ? `(?:${[...docs].map(escapeRe).join('|')})\\s*\\.\\s*` : ''
|
|
1091
|
+
const prefix = inPackage ? (qual ? `(?:${qual})?` : '') : qual
|
|
1092
|
+
const pattern = new RegExp(`(^|[^\\w.])${prefix}(${DOCS_FIELD_CTORS.join('|')})\\s*\\(`, 'g')
|
|
1093
|
+
const masked = file.masked
|
|
1094
|
+
// String contents blanked, so example YAML or prose that mentions a
|
|
1095
|
+
// constructor never reads as a call.
|
|
1096
|
+
const text = stripStrings(masked)
|
|
1097
|
+
let m
|
|
1098
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
1099
|
+
const start = m.index + m[1].length
|
|
1100
|
+
if (/\bfunc\s*$/.test(text.slice(Math.max(0, start - 10), start))) continue
|
|
1101
|
+
const open = m.index + m[0].length - 1
|
|
1102
|
+
const close = file.pairs.get(open)
|
|
1103
|
+
if (close === undefined) continue
|
|
1104
|
+
const args = splitTopLevelArgs(masked.slice(open + 1, close))
|
|
1105
|
+
if (args.length < 2) continue
|
|
1106
|
+
const argStart = (i) => open + 1 + args.slice(0, i).reduce((n, a) => n + a.length + 1, 0)
|
|
1107
|
+
const result = this.evalAt(file, args[1], argStart(1))
|
|
1108
|
+
if (isResolved(result) && renderResult(result).trim() === '') continue
|
|
1109
|
+
const nameResult = this.evalAt(file, args[0], argStart(0))
|
|
1110
|
+
this.pushDecl(declarations, file, rel, {
|
|
1111
|
+
kind: 'field',
|
|
1112
|
+
name: isResolved(nameResult) ? renderResult(nameResult) : null,
|
|
1113
|
+
nameResult,
|
|
1114
|
+
nameExpr: { text: args[0], offset: argStart(0) },
|
|
1115
|
+
result,
|
|
1116
|
+
valueExpr: { text: args[1], offset: argStart(1) },
|
|
1117
|
+
start,
|
|
1118
|
+
end: close,
|
|
1119
|
+
extra: { ctor: `docs.${m[2]}`, ...this.componentMeta(file, start) }
|
|
1120
|
+
})
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1058
1124
|
/**
|
|
1059
1125
|
* javascript processor functions: `registerVMRunnerFunction(name,
|
|
1060
1126
|
* description).Param(name, type, what)`. The processor renders each one
|
|
@@ -1272,7 +1338,7 @@ class ConnectScanner {
|
|
|
1272
1338
|
extra: {
|
|
1273
1339
|
literal: literal === 'Command' ? 'command' : literal,
|
|
1274
1340
|
// rpk-docs formats CLI help like cobra help; it is not AsciiDoc.
|
|
1275
|
-
skip_rules: ['raw-pipe', 'unknown-attribute', 'broken-macro', 'too-short']
|
|
1341
|
+
skip_rules: ['raw-pipe', 'unknown-attribute', 'broken-macro', 'conditional', 'too-short']
|
|
1276
1342
|
}
|
|
1277
1343
|
}))
|
|
1278
1344
|
}
|
|
@@ -1460,8 +1526,6 @@ const MARKUP_PATTERNS = [
|
|
|
1460
1526
|
[/\n/, 'a line break']
|
|
1461
1527
|
]
|
|
1462
1528
|
|
|
1463
|
-
const ENV_CONDITIONAL = /\b(?:ifdef|ifndef|ifeval|endif)::/
|
|
1464
|
-
|
|
1465
1529
|
// Modules of the Redpanda Connect docs component. A generated partial is
|
|
1466
1530
|
// rendered on the self-managed page and included into Cloud pages, so a
|
|
1467
1531
|
// link needs an explicit module to resolve in both places.
|
|
@@ -1491,15 +1555,11 @@ const RULES = [
|
|
|
1491
1555
|
return [{ message: `ShortDescription contains ${found.join(', ')}. Form-based config editors, such as the one in Redpanda Cloud, show it as plain text, so markup renders literally. Write one plain sentence and keep markup in Description().` }]
|
|
1492
1556
|
}
|
|
1493
1557
|
},
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
if (!decl.string || !ENV_CONDITIONAL.test(decl.string)) return []
|
|
1500
|
-
return [{ message: 'Contains an AsciiDoc conditional (ifdef::, ifndef:: or endif::). The docs generator owns every Cloud conditional in the partials, and a hand-written one nests inside them, so the Cloud copy shows or hides the wrong text. Fix the availability data in internal/plugins/info.csv instead, or ask the docs team to carry the sentence on the page.' }]
|
|
1501
|
-
}
|
|
1502
|
-
},
|
|
1558
|
+
// Spec strings publish to the self-managed and the Cloud docs, and the
|
|
1559
|
+
// Cloud binaries render them with env-cloud set, so an env-cloud block
|
|
1560
|
+
// conditional is the supported way to vary a paragraph between the two.
|
|
1561
|
+
// The rule checks the form; see rules/verbatim-asciidoc.js.
|
|
1562
|
+
CONDITIONAL_RULE,
|
|
1503
1563
|
{
|
|
1504
1564
|
name: 'connect-xref-module',
|
|
1505
1565
|
description: 'xref without a module, or to a module the Connect docs do not have',
|
|
@@ -405,18 +405,22 @@ const RULES = [
|
|
|
405
405
|
},
|
|
406
406
|
{
|
|
407
407
|
name: 'go-field-name-first',
|
|
408
|
-
description: 'Description
|
|
408
|
+
description: 'Description opens with the Go field name or the YAML key instead of what the field does',
|
|
409
409
|
severity: 'warning',
|
|
410
410
|
check: (decl) => {
|
|
411
411
|
const text = (decl.string || '').trim()
|
|
412
412
|
if (!text) return []
|
|
413
413
|
const firstWord = (text.match(/^[A-Za-z0-9_]+/) || [null])[0]
|
|
414
414
|
if (!firstWord) return []
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
415
|
+
// Case-sensitive on purpose: `RetentionTime is...` on
|
|
416
|
+
// json:"retentionTime" opens with the Go name, and `retentionTime
|
|
417
|
+
// is...` opens with the key. Neither says what the field does.
|
|
418
|
+
const { go_name: goName, json_name: jsonName } = decl.meta
|
|
419
|
+
let opener = null
|
|
420
|
+
if (firstWord === goName) opener = `the Go field name "${goName}"`
|
|
421
|
+
else if (firstWord === jsonName) opener = `the YAML key "${jsonName}"`
|
|
422
|
+
if (!opener) return []
|
|
423
|
+
return [{ message: `Description opens with ${opener}. Start with what the field does, for example "Specifies ...". Don't open with the Go field name "${goName}" or the YAML key "${jsonName}".` }]
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
426
|
]
|