@redpanda-data/docs-extensions-and-macros 5.13.5 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools-mcp.js +88 -0
- package/bin/doc-tools.js +349 -102
- package/bin/mcp-tools/index.js +20 -0
- package/bin/mcp-tools/lint-doc-strings.js +75 -0
- package/bin/mcp-tools/overrides-audit.js +100 -0
- package/bin/mcp-tools/preview-doc-string.js +71 -0
- package/cli-utils/diff-utils.js +77 -0
- package/cli-utils/helm-commented-values.js +172 -23
- package/cli-utils/sync-schemas.js +144 -0
- package/docs-data/rpk-overrides.schema.json +4 -0
- package/package.json +2 -1
- package/tools/generate-cli-docs.js +28 -67
- package/tools/lint-strings/diff.js +137 -0
- package/tools/lint-strings/engine.js +163 -0
- package/tools/lint-strings/fixtures/connect/lint_connect.go +67 -0
- package/tools/lint-strings/fixtures/crd/crd-ref-docs-config.yaml +15 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +57 -0
- package/tools/lint-strings/fixtures/helm/lint_values.yaml +47 -0
- package/tools/lint-strings/fixtures/metrics/lint_probe.cc +41 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.cc +51 -0
- package/tools/lint-strings/fixtures/properties/lint_fixture.h +33 -0
- package/tools/lint-strings/fixtures/rpk/lint_cmd.go +69 -0
- package/tools/lint-strings/go-source.js +327 -0
- package/tools/lint-strings/index.js +229 -0
- package/tools/lint-strings/rules/common.js +148 -0
- package/tools/lint-strings/rules/verbatim-asciidoc.js +108 -0
- package/tools/lint-strings/source-text.js +41 -0
- package/tools/lint-strings/surfaces/connect.js +233 -0
- package/tools/lint-strings/surfaces/crd.js +239 -0
- package/tools/lint-strings/surfaces/helm.js +159 -0
- package/tools/lint-strings/surfaces/metrics.js +368 -0
- package/tools/lint-strings/surfaces/properties.js +216 -0
- package/tools/lint-strings/surfaces/rpk.js +377 -0
- package/tools/overrides-audit/README.adoc +152 -0
- package/tools/overrides-audit/adapters/connect.js +144 -0
- package/tools/overrides-audit/adapters/properties.js +117 -0
- package/tools/overrides-audit/adapters/rpk.js +192 -0
- package/tools/overrides-audit/classify.js +546 -0
- package/tools/overrides-audit/index.js +185 -0
- package/tools/preview-string/index.js +245 -0
- package/tools/property-extractor/Makefile +9 -4
- package/tools/property-extractor/parser.py +26 -0
- package/tools/property-extractor/property_extractor.py +41 -0
- package/tools/property-extractor/transformers.py +15 -2
- package/tools/rpk-docs/detect-platform-commands.js +8 -1
- package/tools/rpk-docs/scan-deprecated-commands.js +15 -18
|
@@ -0,0 +1,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
|
+
}
|