@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.
Files changed (46) hide show
  1. package/bin/doc-tools-mcp.js +88 -0
  2. package/bin/doc-tools.js +349 -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/sync-schemas.js +144 -0
  10. package/docs-data/rpk-overrides.schema.json +4 -0
  11. package/package.json +2 -1
  12. package/tools/generate-cli-docs.js +28 -67
  13. package/tools/lint-strings/diff.js +137 -0
  14. package/tools/lint-strings/engine.js +163 -0
  15. package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
  16. package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
  17. package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
  18. package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
  19. package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
  20. package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
  21. package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
  22. package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
  23. package/tools/lint-strings/go-source.js +327 -0
  24. package/tools/lint-strings/index.js +229 -0
  25. package/tools/lint-strings/rules/common.js +148 -0
  26. package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
  27. package/tools/lint-strings/source-text.js +41 -0
  28. package/tools/lint-strings/surfaces/connect.js +233 -0
  29. package/tools/lint-strings/surfaces/crd.js +239 -0
  30. package/tools/lint-strings/surfaces/helm.js +159 -0
  31. package/tools/lint-strings/surfaces/metrics.js +368 -0
  32. package/tools/lint-strings/surfaces/properties.js +216 -0
  33. package/tools/lint-strings/surfaces/rpk.js +377 -0
  34. package/tools/overrides-audit/README.adoc +152 -0
  35. package/tools/overrides-audit/adapters/connect.js +144 -0
  36. package/tools/overrides-audit/adapters/properties.js +117 -0
  37. package/tools/overrides-audit/adapters/rpk.js +192 -0
  38. package/tools/overrides-audit/classify.js +546 -0
  39. package/tools/overrides-audit/index.js +185 -0
  40. package/tools/preview-string/index.js +245 -0
  41. package/tools/property-extractor/Makefile +9 -4
  42. package/tools/property-extractor/parser.py +26 -0
  43. package/tools/property-extractor/property_extractor.py +41 -0
  44. package/tools/property-extractor/transformers.py +15 -2
  45. package/tools/rpk-docs/detect-platform-commands.js +8 -1
  46. package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ # lint-strings test fixture: helm-docs conventions with known-bads and
2
+ # conforming counterparts.
3
+
4
+ # -- Enable or disable the thing entirely. This description conforms.
5
+ enabled: true
6
+
7
+ # -- Image settings for the fixture chart.
8
+ image:
9
+ # -- The container image repository to pull from.
10
+ repository: docker.redpanda.com/redpandadata/redpanda
11
+ tag: latest
12
+
13
+ # -- Service settings that were commented out wholesale. The nested markers
14
+ # below are dead: no pipeline can attach them.
15
+ # service:
16
+ # -- set service.name to override the default service name
17
+ # name: redpanda
18
+ # -- internal Service settings
19
+ # internal:
20
+ # annotations: {}
21
+
22
+
23
+ # A marker separated from any key by a blank line is dead too:
24
+ # -- This orphaned description attaches to nothing.
25
+
26
+ # @doc external.domain -- Optional domain advertised to external clients.
27
+ # @default -- `nil`
28
+
29
+ resources:
30
+ # -- Requested CPU cores. Commented-out key documented in place:
31
+ # cpu: 1
32
+
33
+ # @ignored
34
+ internalHiddenKey: {}
35
+
36
+ undocumentedTopLevel:
37
+ child: value
38
+
39
+ config: |
40
+ # -- inside a block scalar this marker is content, not documentation
41
+ some: content
42
+
43
+ # -- External access settings.
44
+ external:
45
+ # -- Optional domain advertised to external clients. Hyphenated key names
46
+ # must survive both the generator and the linter.
47
+ # my-domain: local
@@ -0,0 +1,41 @@
1
+ // Fixture source for lint-strings metrics-scanner tests. Mirrors the shapes
2
+ // in redpanda's src/v/cluster/partition_probe.cc: single-literal and
3
+ // clang-format-wrapped adjacent-literal sm::description() arguments, plus a
4
+ // non-literal argument that must stay unverifiable (info-level, never error).
5
+ // Tests assert both rule ids and line spans, located by marker strings.
6
+ #include "cluster/lint_probe.h"
7
+
8
+ namespace cluster {
9
+
10
+ void lint_probe::setup_metrics() {
11
+ namespace sm = ss::metrics;
12
+
13
+ _metrics.add_group(
14
+ prometheus_sanitize::metrics_name("cluster:partition"),
15
+ {
16
+ sm::make_gauge(
17
+ "start_offset",
18
+ [this] { return _start_offset; },
19
+ sm::description("start offset"),
20
+ labels),
21
+ sm::make_gauge(
22
+ "committed_offset",
23
+ [this] { return _committed_offset; },
24
+ sm::description(
25
+ "Partition commited offset. i.e. safely persisted on "
26
+ "majority of replicas."),
27
+ labels),
28
+ sm::make_counter(
29
+ "records_produced",
30
+ [this] { return _records_produced; },
31
+ sm::description("Total number of records produced"),
32
+ labels),
33
+ sm::make_gauge(
34
+ "buffer_size",
35
+ [this] { return _buffer_size; },
36
+ sm::description(fmt::format("Size of {} buffer", _name)),
37
+ labels),
38
+ });
39
+ }
40
+
41
+ } // namespace cluster
@@ -0,0 +1,51 @@
1
+ // Fixture source for lint-strings tests. Member-initializer entries mirror
2
+ // redpanda's src/v/config/configuration.cc, including clang-format-wrapped
3
+ // adjacent string literals. Tests assert both rule ids and line spans, so
4
+ // edit with care: spans are located by searching for the marker strings.
5
+ #include "lint_fixture.h"
6
+
7
+ namespace config {
8
+
9
+ lint_fixture_config::lint_fixture_config()
10
+ : compaction_ctrl_update_interval_ms(
11
+ *this,
12
+ "compaction_ctrl_update_interval_ms",
13
+ "",
14
+ {.needs_restart = needs_restart::no, .visibility = visibility::tunable},
15
+ 30s)
16
+ , cloud_storage_credentials_source(
17
+ *this,
18
+ "cloud_storage_credentials_source",
19
+ "The source of credentials used to connect to cloud services. "
20
+ "Accepted values: `config_file`, `aws_instance_metadata`, `sts, "
21
+ "`gcp_instance_metadata`.",
22
+ {.needs_restart = needs_restart::yes, .visibility = visibility::user},
23
+ std::nullopt)
24
+ , cloud_storage_upload_ctrl_p_coeff(
25
+ *this,
26
+ "cloud_storage_upload_ctrl_p_coeff",
27
+ "proportional coefficient for upload PID controller",
28
+ {.needs_restart = needs_restart::yes, .visibility = visibility::tunable},
29
+ -2.0)
30
+ , cluster_id(
31
+ *this,
32
+ "cluster_id",
33
+ "Cluster identifier.",
34
+ {.needs_restart = needs_restart::no, .visibility = visibility::user},
35
+ std::nullopt)
36
+ , write_caching_default_bytes(
37
+ *this,
38
+ "write_caching_default_bytes",
39
+ "recomended default",
40
+ {.needs_restart = needs_restart::no, .visibility = visibility::tunable},
41
+ 1024)
42
+ , kafka_batch_max_bytes(
43
+ *this,
44
+ "kafka_batch_max_bytes",
45
+ "Maximum size of a batch processed by the server. If the batch is "
46
+ "compressed, the limit applies to the compressed batch size. Default "
47
+ "is 1048576 bytes.",
48
+ {.needs_restart = needs_restart::no, .visibility = visibility::user},
49
+ 1048576) {}
50
+
51
+ } // namespace config
@@ -0,0 +1,33 @@
1
+ // Fixture header for lint-strings tests. The declarations mirror the shapes
2
+ // in redpanda's src/v/config/configuration.h so the real property extractor
3
+ // can parse the matching .cc file.
4
+ #pragma once
5
+
6
+ #include "config/property.h"
7
+
8
+ namespace config {
9
+
10
+ struct lint_fixture_config {
11
+ // Known-bad: empty description (mirrors configuration.cc
12
+ // compaction_ctrl_update_interval_ms)
13
+ property<std::chrono::milliseconds> compaction_ctrl_update_interval_ms;
14
+
15
+ // Known-bad: unbalanced backticks (mirrors configuration.cc
16
+ // cloud_storage_credentials_source)
17
+ enum_property<ss::sstring> cloud_storage_credentials_source;
18
+
19
+ // Known-bad: lowercase-start fragment, no terminal period
20
+ property<double> cloud_storage_upload_ctrl_p_coeff;
21
+
22
+ // Known-bad: tautology (description just restates the name)
23
+ property<std::optional<ss::sstring>> cluster_id;
24
+
25
+ // Known-bad: "recomended" typo string. Typo detection is NOT a lint rule;
26
+ // this doubles as the lowercase-start + under-20-chars example.
27
+ property<int32_t> write_caching_default_bytes;
28
+
29
+ // Conforming counterpart: must produce zero findings.
30
+ property<int32_t> kafka_batch_max_bytes;
31
+ };
32
+
33
+ } // namespace config
@@ -0,0 +1,69 @@
1
+ // Package lintfixture is a lint-strings test fixture: known-bad rpk
2
+ // declarations with conforming counterparts. It only needs to parse as Go
3
+ // text for the scanner; it is never compiled.
4
+ package lintfixture
5
+
6
+ import (
7
+ "github.com/spf13/afero"
8
+ "github.com/spf13/cobra"
9
+ )
10
+
11
+ // cobra.Command mentioned in a comment must never match: cobra.Command{Use: "fake"}.
12
+
13
+ func newLowercaseShortCommand(fs afero.Fs) *cobra.Command {
14
+ cmd := &cobra.Command{
15
+ Use: "badshort",
16
+ Short: "queries cluster for health overview.",
17
+ Long: `Query the cluster.
18
+
19
+ USAGE DETAILS
20
+
21
+ This section heading above is intended.
22
+
23
+ Some detail text here that is not a heading.
24
+ ====
25
+
26
+ The ==== line above breaks the generated page.
27
+ `,
28
+ Run: func(cmd *cobra.Command, _ []string) {
29
+ // A paren ) and brace } in a string: "don't break (the) scanner".
30
+ },
31
+ }
32
+ cmd.Flags().BoolVarP(&watch, "watch", "w", false, "Blocks and writes out all changes")
33
+ cmd.Flags().StringVar(&dest, "trailing-period", "", "Writes the output to the given path.")
34
+ cmd.Flags().String("verbose-output", "", "verbose output")
35
+ return cmd
36
+ }
37
+
38
+ const conformingLong = `Manage widgets.
39
+
40
+ This is a conforming Long description with no heading-shaped lines,
41
+ resolved through a package-level constant.
42
+ `
43
+
44
+ func newConformingCommand() *cobra.Command {
45
+ cmd := &cobra.Command{
46
+ Use: "widget [NAME]",
47
+ Aliases: []string{"widgets"},
48
+ Short: "Manage widgets in the cluster",
49
+ Long: conformingLong,
50
+ }
51
+ f := cmd.Flags()
52
+ f.IntVar(&count, "retries", 3, "Number of retries before giving up")
53
+ f.DurationVar(&timeout, "timeout", 0, "How long to wait for the cluster to respond, for example 30s")
54
+ cmd.Flags().StringP("format", "f", "text", "Output format, one of text or json")
55
+ // Dynamic usage strings stay unverifiable, never errors:
56
+ cmd.Flags().BoolP("help", "h", false, "Help for "+cmd.Name())
57
+ return cmd
58
+ }
59
+
60
+ // A multiline Short: the rpk convention is a one-line Short, so this is the
61
+ // known-bad counterpart for the rpk-short-multiline rule. Everything else
62
+ // about it conforms, so it isolates that one rule.
63
+ func newMultilineShortCommand() *cobra.Command {
64
+ return &cobra.Command{
65
+ Use: "multiline",
66
+ Short: `Manage the widget cache
67
+ and everything attached to it`,
68
+ }
69
+ }
@@ -0,0 +1,327 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Shared Go source scanning helpers for the rpk, crd, and connect surfaces.
5
+ *
6
+ * Same philosophy as the metrics scanner (surfaces/metrics.js): a
7
+ * comment-masked regex/paren scanner over real source text, never a compiler.
8
+ * Go adds two string shapes the C++ scanner does not have: raw strings
9
+ * (`...` backticks, no escapes, literal newlines) and rune literals ('x').
10
+ * All helpers preserve offsets and newlines so line numbers stay exact.
11
+ */
12
+
13
+ /**
14
+ * Replace // and /* *\/ comments with spaces, preserving every offset and
15
+ * newline. String and rune literal contents are left untouched, so a "//"
16
+ * inside a string never masks anything.
17
+ */
18
+ function maskComments (content) {
19
+ const out = content.split('')
20
+ let i = 0
21
+ while (i < content.length) {
22
+ const ch = content[i]
23
+ if (ch === '"' || ch === '`' || ch === "'") {
24
+ i = skipString(content, i)
25
+ continue
26
+ }
27
+ if (ch === '/' && content[i + 1] === '/') {
28
+ while (i < content.length && content[i] !== '\n') out[i++] = ' '
29
+ continue
30
+ }
31
+ if (ch === '/' && content[i + 1] === '*') {
32
+ const end = content.indexOf('*/', i + 2)
33
+ const stop = end === -1 ? content.length : end + 2
34
+ while (i < stop) {
35
+ if (content[i] !== '\n') out[i] = ' '
36
+ i++
37
+ }
38
+ continue
39
+ }
40
+ i++
41
+ }
42
+ return out.join('')
43
+ }
44
+
45
+ /**
46
+ * Skip a Go string/rune literal starting at index; returns the index after
47
+ * it. Interpreted strings ("...") and runes ('x') honor backslash escapes;
48
+ * raw strings (`...`) have no escapes and may span lines.
49
+ */
50
+ function skipString (content, start) {
51
+ const quote = content[start]
52
+ let i = start + 1
53
+ if (quote === '`') {
54
+ while (i < content.length && content[i] !== '`') i++
55
+ return Math.min(i + 1, content.length)
56
+ }
57
+ while (i < content.length) {
58
+ if (content[i] === '\\') {
59
+ i += 2
60
+ continue
61
+ }
62
+ if (content[i] === quote) return i + 1
63
+ i++
64
+ }
65
+ return content.length
66
+ }
67
+
68
+ /**
69
+ * Walk forward from an opening delimiter and return the index of its
70
+ * balanced closing delimiter, skipping string/rune literals. Assumes
71
+ * comment-masked content. Returns -1 when unbalanced.
72
+ */
73
+ function findBalancedClose (content, openIndex, open = '(', close = ')') {
74
+ let depth = 0
75
+ let i = openIndex
76
+ while (i < content.length) {
77
+ const ch = content[i]
78
+ if (ch === '"' || ch === '`' || ch === "'") {
79
+ i = skipString(content, i)
80
+ continue
81
+ }
82
+ if (ch === open) depth++
83
+ if (ch === close) {
84
+ depth--
85
+ if (depth === 0) return i
86
+ }
87
+ i++
88
+ }
89
+ return -1
90
+ }
91
+
92
+ /** Decode a Go interpreted-string body (the text between the quotes). */
93
+ function unescapeGo (text) {
94
+ return text.replace(/\\(.)/g, (whole, ch) => {
95
+ if (ch === 'n') return '\n'
96
+ if (ch === 't') return '\t'
97
+ if (ch === 'r') return '\r'
98
+ return ch
99
+ })
100
+ }
101
+
102
+ /**
103
+ * Split argument text on top-level commas (string, paren, brace, and
104
+ * bracket aware). Assumes comment-masked content.
105
+ */
106
+ function splitTopLevelArgs (argText) {
107
+ const args = []
108
+ let start = 0
109
+ let depth = 0
110
+ let i = 0
111
+ while (i < argText.length) {
112
+ const ch = argText[i]
113
+ if (ch === '"' || ch === '`' || ch === "'") {
114
+ i = skipString(argText, i)
115
+ continue
116
+ }
117
+ if (ch === '(' || ch === '{' || ch === '[') depth++
118
+ if (ch === ')' || ch === '}' || ch === ']') depth--
119
+ if (ch === ',' && depth === 0) {
120
+ args.push(argText.slice(start, i))
121
+ start = i + 1
122
+ }
123
+ i++
124
+ }
125
+ const last = argText.slice(start)
126
+ if (last.trim() !== '' || args.length > 0) args.push(last)
127
+ return args
128
+ }
129
+
130
+ /**
131
+ * Collect same-file string constants: `const x = "..."` and const blocks.
132
+ * Only constants whose value is purely (concatenated) string literals are
133
+ * recorded - anything else stays unresolvable so it can never fake a
134
+ * verifiable description.
135
+ *
136
+ * @param {string} masked - Comment-masked file content
137
+ * @returns {Map<string, string>} identifier -> string value
138
+ */
139
+ function collectStringConsts (masked) {
140
+ const consts = new Map()
141
+ const pattern = /\bconst\b/g
142
+ let match
143
+ while ((match = pattern.exec(masked)) !== null) {
144
+ let i = match.index + match[0].length
145
+ while (i < masked.length && /[ \t]/.test(masked[i])) i++
146
+ if (masked[i] === '(') {
147
+ const close = findBalancedClose(masked, i)
148
+ if (close === -1) continue
149
+ collectConstEntries(masked.slice(i + 1, close), consts)
150
+ pattern.lastIndex = close + 1
151
+ } else {
152
+ // Single-const form: the statement ends at the first newline OUTSIDE
153
+ // a string (a raw-string value may span lines), unless that newline
154
+ // follows a `+` continuation - Go allows a string concatenation to
155
+ // wrap across lines with no surrounding parens.
156
+ let j = i
157
+ while (j < masked.length) {
158
+ if (masked[j] === '"' || masked[j] === '`' || masked[j] === "'") {
159
+ j = skipString(masked, j)
160
+ continue
161
+ }
162
+ if (masked[j] === '\n') {
163
+ let k = j - 1
164
+ while (k >= i && /[ \t\r]/.test(masked[k])) k--
165
+ if (k >= i && masked[k] === '+') {
166
+ j++
167
+ continue
168
+ }
169
+ break
170
+ }
171
+ j++
172
+ }
173
+ collectConstEntries(masked.slice(i, j), consts)
174
+ pattern.lastIndex = j
175
+ }
176
+ }
177
+ return consts
178
+ }
179
+
180
+ /** Parse `name [type] = <string expr>` lines inside a const region. */
181
+ function collectConstEntries (regionText, consts) {
182
+ // Split on newlines OUTSIDE strings (raw strings may span lines), unless
183
+ // the newline follows a `+` continuation - a string concatenation may
184
+ // wrap across lines with no surrounding parens, and must stay one entry.
185
+ const lines = []
186
+ let start = 0
187
+ let i = 0
188
+ while (i < regionText.length) {
189
+ const ch = regionText[i]
190
+ if (ch === '"' || ch === '`' || ch === "'") {
191
+ i = skipString(regionText, i)
192
+ continue
193
+ }
194
+ if (ch === '\n') {
195
+ let k = i - 1
196
+ while (k >= start && /[ \t\r]/.test(regionText[k])) k--
197
+ if (k >= start && regionText[k] === '+') {
198
+ i++
199
+ continue
200
+ }
201
+ lines.push(regionText.slice(start, i))
202
+ start = i + 1
203
+ }
204
+ i++
205
+ }
206
+ lines.push(regionText.slice(start))
207
+
208
+ for (const line of lines) {
209
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:[A-Za-z_][A-Za-z0-9_.]*\s*)?=\s*(.+)$/s)
210
+ if (!m) continue
211
+ const { verifiable, value } = evalStringExpr(m[2], consts)
212
+ if (verifiable) consts.set(m[1], value)
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Evaluate a Go expression expected to produce a string: a sequence of
218
+ * string literals and known identifiers joined by `+`. Anything else
219
+ * (function calls, fmt.Sprintf, selectors, unknown identifiers) makes the
220
+ * result unverifiable - the caller reports it, never guesses.
221
+ *
222
+ * @param {string} exprText - Comment-masked expression text
223
+ * @param {Map<string, string>} [consts] - Same-file string constants
224
+ * @returns {{ verifiable: boolean, value: string|null, sawString: boolean }}
225
+ */
226
+ function evalStringExpr (exprText, consts = new Map()) {
227
+ const parts = []
228
+ let verifiable = true
229
+ let sawString = false
230
+ let i = 0
231
+ while (i < exprText.length) {
232
+ const ch = exprText[i]
233
+ if (/\s/.test(ch) || ch === '+') {
234
+ i++
235
+ continue
236
+ }
237
+ if (ch === '"') {
238
+ const end = skipString(exprText, i)
239
+ parts.push(unescapeGo(exprText.slice(i + 1, end - 1)))
240
+ sawString = true
241
+ i = end
242
+ continue
243
+ }
244
+ if (ch === '`') {
245
+ const end = skipString(exprText, i)
246
+ parts.push(exprText.slice(i + 1, end - 1))
247
+ sawString = true
248
+ i = end
249
+ continue
250
+ }
251
+ if (/[A-Za-z_]/.test(ch)) {
252
+ let j = i
253
+ while (j < exprText.length && /[A-Za-z0-9_.]/.test(exprText[j])) j++
254
+ const ident = exprText.slice(i, j)
255
+ // A following "(" means a call - unverifiable.
256
+ let k = j
257
+ while (k < exprText.length && /\s/.test(exprText[k])) k++
258
+ if (exprText[k] === '(') {
259
+ verifiable = false
260
+ const close = findBalancedClose(exprText, k)
261
+ i = close === -1 ? exprText.length : close + 1
262
+ continue
263
+ }
264
+ if (consts.has(ident)) {
265
+ parts.push(consts.get(ident))
266
+ sawString = true
267
+ } else {
268
+ verifiable = false
269
+ }
270
+ i = j
271
+ continue
272
+ }
273
+ // Any other token (&, numbers, composite literals, ...) is not a string.
274
+ verifiable = false
275
+ if (ch === '(' || ch === '{' || ch === '[') {
276
+ const close = findBalancedClose(exprText, i, ch, ch === '(' ? ')' : ch === '{' ? '}' : ']')
277
+ i = close === -1 ? exprText.length : close + 1
278
+ continue
279
+ }
280
+ i++
281
+ }
282
+ if (!sawString) return { verifiable: false, value: null, sawString }
283
+ return { verifiable, value: verifiable ? parts.join('') : null, sawString }
284
+ }
285
+
286
+ /** 1-indexed line number of a character offset. */
287
+ function lineOf (content, index) {
288
+ let line = 1
289
+ for (let i = 0; i < index; i++) {
290
+ if (content[i] === '\n') line++
291
+ }
292
+ return line
293
+ }
294
+
295
+ const fs = require('fs')
296
+ const path = require('path')
297
+
298
+ /**
299
+ * Recursively collect .go files under root (repo-relative paths), skipping
300
+ * _test.go files, testdata/fuzzing dirs, and hidden dirs.
301
+ */
302
+ function collectGoFiles (root, base = root, out = []) {
303
+ if (!fs.existsSync(root)) return out
304
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
305
+ if (entry.name.startsWith('.')) continue
306
+ const fullPath = path.join(root, entry.name)
307
+ if (entry.isDirectory()) {
308
+ if (entry.name === 'testdata' || entry.name === 'fuzzing' || entry.name === 'node_modules') continue
309
+ collectGoFiles(fullPath, base, out)
310
+ } else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
311
+ out.push(path.relative(base, fullPath))
312
+ }
313
+ }
314
+ return out
315
+ }
316
+
317
+ module.exports = {
318
+ maskComments,
319
+ skipString,
320
+ findBalancedClose,
321
+ unescapeGo,
322
+ splitTopLevelArgs,
323
+ collectStringConsts,
324
+ evalStringExpr,
325
+ lineOf,
326
+ collectGoFiles
327
+ }