@veda-hq/pariksha 0.5.13 → 0.5.14
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/README.md +133 -2
- package/dist/cli.js +55 -0
- package/dist/doc-command-rules.js +179 -0
- package/dist/doc-command-targets.js +122 -0
- package/dist/doc-commands.js +187 -0
- package/dist/doc-flag-spec.js +87 -0
- package/dist/doc-identifiers.js +348 -0
- package/dist/doc-links.js +183 -0
- package/dist/doc-paths.js +267 -0
- package/dist/doc-references-config.js +172 -0
- package/dist/doc-references-gate.js +338 -0
- package/dist/doc-references.js +535 -0
- package/dist/doc-scripts.js +224 -0
- package/dist/shell-text.js +193 -0
- package/package.json +3 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// DOC-DRIFT-02, documented scripts: a `pnpm <script>` a doc shows exists, in
|
|
2
|
+
// the scope the doc runs it from, and a verification step does not run a
|
|
3
|
+
// script that changes the tree it is meant to check (`lint` auto-fixes:
|
|
4
|
+
// `lint:ci` is the check). A step that names a missing script looks like a
|
|
5
|
+
// check and runs nothing.
|
|
6
|
+
import { readdirSync } from 'node:fs';
|
|
7
|
+
import { join, posix } from 'node:path';
|
|
8
|
+
import { codeSpans as markdownCodeSpans } from "./doc-references.js";
|
|
9
|
+
import { isRecord } from "./registry-ratchet.js";
|
|
10
|
+
import { maskQuoted, stripComment } from "./shell-text.js";
|
|
11
|
+
/**
|
|
12
|
+
* Executables in `node_modules/.bin`. THROWS rather than returning a
|
|
13
|
+
* wildcard: a missing install that let every name pass would print a pass
|
|
14
|
+
* while scanning nothing.
|
|
15
|
+
*/
|
|
16
|
+
export const binNames = (root) => {
|
|
17
|
+
const dir = join(root, 'node_modules', '.bin');
|
|
18
|
+
try {
|
|
19
|
+
return readdirSync(dir);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`cannot read ${dir}; install the dependencies before this check`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Every manifest the config lists that exists. One that does not parse would
|
|
27
|
+
* silently shrink the known-script set, so it fails loud instead.
|
|
28
|
+
*/
|
|
29
|
+
const readManifests = (manifests, read) => manifests.flatMap((path) => {
|
|
30
|
+
const text = read(path);
|
|
31
|
+
if (text === undefined)
|
|
32
|
+
return [];
|
|
33
|
+
let pkg;
|
|
34
|
+
try {
|
|
35
|
+
pkg = JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`${path} is not valid JSON — cannot determine which scripts exist`);
|
|
39
|
+
}
|
|
40
|
+
const name = isRecord(pkg) && typeof pkg['name'] === 'string' ? pkg['name'] : undefined;
|
|
41
|
+
const raw = isRecord(pkg) ? pkg['scripts'] : undefined;
|
|
42
|
+
const scripts = {};
|
|
43
|
+
if (isRecord(raw))
|
|
44
|
+
for (const [key, body] of Object.entries(raw))
|
|
45
|
+
scripts[key] = String(body);
|
|
46
|
+
return [{ scope: posix.dirname(path), name, scripts }];
|
|
47
|
+
});
|
|
48
|
+
/** A manifest's scope keys: its directory, and its package name if any. */
|
|
49
|
+
const scopeKeys = (manifest) => (manifest.name === undefined ? [manifest.scope] : [manifest.scope, manifest.name]);
|
|
50
|
+
/** Every name `pnpm <name>` can resolve to: a script in any listed manifest, or an executable in `node_modules/.bin`. */
|
|
51
|
+
export const definedScripts = (root, manifests, read) => new Set([...binNames(root), ...readManifests(manifests, read).flatMap((manifest) => Object.keys(manifest.scripts))]);
|
|
52
|
+
/**
|
|
53
|
+
* Scripts per scope, because `pnpm foo` does not resolve `foo` from a
|
|
54
|
+
* workspace. Keyed by both the workspace directory and the package name, as
|
|
55
|
+
* `--filter` takes either. Bins resolve in every scope: pnpm puts the same
|
|
56
|
+
* hoisted `.bin` on PATH for a filtered command.
|
|
57
|
+
*/
|
|
58
|
+
export const scriptScopes = (root, manifests, read) => {
|
|
59
|
+
const scopes = new Map();
|
|
60
|
+
const bins = binNames(root);
|
|
61
|
+
for (const manifest of readManifests(manifests, read)) {
|
|
62
|
+
const names = new Set([...Object.keys(manifest.scripts), ...bins]);
|
|
63
|
+
for (const key of scopeKeys(manifest))
|
|
64
|
+
scopes.set(key, names);
|
|
65
|
+
}
|
|
66
|
+
return scopes;
|
|
67
|
+
};
|
|
68
|
+
export const scriptBodies = (manifests, read) => {
|
|
69
|
+
const scripts = new Map();
|
|
70
|
+
const scopeDirs = new Map();
|
|
71
|
+
for (const manifest of readManifests(manifests, read)) {
|
|
72
|
+
const bodies = new Map(Object.entries(manifest.scripts));
|
|
73
|
+
for (const key of scopeKeys(manifest)) {
|
|
74
|
+
scripts.set(key, bodies);
|
|
75
|
+
scopeDirs.set(key, manifest.scope);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { scripts, scopeDirs };
|
|
79
|
+
};
|
|
80
|
+
/** Half-open [start, end) ranges of every inline code span on a line, between its delimiters. */
|
|
81
|
+
export const spanRanges = (line) => markdownCodeSpans(line).map((span) => [span.start, span.end]);
|
|
82
|
+
const FENCE_OPEN = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
83
|
+
/**
|
|
84
|
+
* Each line's place relative to fenced code blocks, by CommonMark's rule: an
|
|
85
|
+
* opening fence is three or more backticks or tildes indented at most three
|
|
86
|
+
* spaces (a backtick fence's info string has no backtick); it closes at a
|
|
87
|
+
* fence of the same character, at least as long, with nothing after it but
|
|
88
|
+
* spaces; an unclosed fence runs to the end.
|
|
89
|
+
*/
|
|
90
|
+
export const fenceRoles = (lines) => {
|
|
91
|
+
const roles = [];
|
|
92
|
+
let fence = null;
|
|
93
|
+
for (const raw of lines) {
|
|
94
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
|
|
95
|
+
if (fence === null) {
|
|
96
|
+
const open = FENCE_OPEN.exec(line);
|
|
97
|
+
const marker = open?.[2] ?? '';
|
|
98
|
+
if (open !== null && !(marker.startsWith('`') && String(open[3]).includes('`'))) {
|
|
99
|
+
fence = { char: marker.charAt(0), length: marker.length };
|
|
100
|
+
roles.push('fence');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
roles.push('prose');
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const close = /^ {0,3}(`+|~+)[ \t]*$/.exec(line)?.[1];
|
|
108
|
+
if (close !== undefined && close.charAt(0) === fence.char && close.length >= fence.length) {
|
|
109
|
+
fence = null;
|
|
110
|
+
roles.push('fence');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
roles.push('code');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return roles;
|
|
117
|
+
};
|
|
118
|
+
/** pnpm's own subcommands, which are not package scripts. */
|
|
119
|
+
const BUILTIN = new Set([
|
|
120
|
+
'install',
|
|
121
|
+
'add',
|
|
122
|
+
'remove',
|
|
123
|
+
'exec',
|
|
124
|
+
'dlx',
|
|
125
|
+
'why',
|
|
126
|
+
'audit',
|
|
127
|
+
'list',
|
|
128
|
+
'outdated',
|
|
129
|
+
'update',
|
|
130
|
+
'publish',
|
|
131
|
+
'pack',
|
|
132
|
+
'link',
|
|
133
|
+
'patch',
|
|
134
|
+
'store',
|
|
135
|
+
'config',
|
|
136
|
+
'approve-builds',
|
|
137
|
+
'rebuild',
|
|
138
|
+
'prune',
|
|
139
|
+
'setup',
|
|
140
|
+
'licenses',
|
|
141
|
+
'deploy',
|
|
142
|
+
'env',
|
|
143
|
+
'init',
|
|
144
|
+
'create',
|
|
145
|
+
'run',
|
|
146
|
+
'help',
|
|
147
|
+
'self-update',
|
|
148
|
+
'doctor',
|
|
149
|
+
'root',
|
|
150
|
+
'bin',
|
|
151
|
+
'fetch',
|
|
152
|
+
'import',
|
|
153
|
+
'unlink',
|
|
154
|
+
'dedupe',
|
|
155
|
+
'cache',
|
|
156
|
+
]);
|
|
157
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
158
|
+
/** What may precede `pnpm` for it to start a command: nothing, a separator, a prompt, env assignments, or a wrapper. */
|
|
159
|
+
export const commandStart = (wrappers) => new RegExp(`(?:^|&&|\\|\\||;|\\||\\$|\\(|\\b(?:${['exec', ...wrappers.map(escapeRegExp), 'time', 'env', 'then', 'do'].join('|')})|\\s--)\\s*(?:[A-Z_][A-Z0-9_]*=\\S*\\s+)*$`);
|
|
160
|
+
/** The documented-script rules over `files`; `scopes` judges where each resolves. */
|
|
161
|
+
export const findScriptProblems = (config, files, defined, scopes, read) => {
|
|
162
|
+
const problems = [];
|
|
163
|
+
const start = commandStart(config.scripts.wrappers);
|
|
164
|
+
// `pnpm run foo`, `pnpm foo`, `pnpm --filter x foo`; not `npx`, which
|
|
165
|
+
// resolves from the registry. The `--filter` target is captured: a filtered
|
|
166
|
+
// command resolves in another scope.
|
|
167
|
+
const invocation = /\bpnpm\s+(?:run\s+)?(?:--filter[= ](\S+)\s+)?([a-z][a-z0-9:-]*)/g;
|
|
168
|
+
for (const relative of files) {
|
|
169
|
+
const content = read(relative);
|
|
170
|
+
if (content === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
const lines = content.split('\n');
|
|
173
|
+
const roles = fenceRoles(lines);
|
|
174
|
+
lines.forEach((line, index) => {
|
|
175
|
+
const role = roles[index];
|
|
176
|
+
// A fence line itself carries no command; its info string is a label.
|
|
177
|
+
if (role === 'fence' || config.exemptMarker.test(line))
|
|
178
|
+
return;
|
|
179
|
+
const inFence = role === 'code';
|
|
180
|
+
const spans = inFence ? null : spanRanges(line);
|
|
181
|
+
// A shell comment in a fence is prose; a `#` inside quotes or escaped starts none.
|
|
182
|
+
const text = inFence ? stripComment(line) : line;
|
|
183
|
+
// A fence line is shell: a quoted separator starts no command, so the
|
|
184
|
+
// start is judged in the quote-masked line. Prose is not shell.
|
|
185
|
+
const code = inFence ? maskQuoted(text) : text;
|
|
186
|
+
for (const match of text.matchAll(invocation)) {
|
|
187
|
+
const filter = match[1];
|
|
188
|
+
const script = String(match[2]);
|
|
189
|
+
if (BUILTIN.has(script))
|
|
190
|
+
continue;
|
|
191
|
+
// Only text the author marked as code counts, and all of the invocation must be in it.
|
|
192
|
+
const span = spans === null ? [0, text.length] : spans.find(([a, b]) => match.index >= a && match.index + match[0].length <= b);
|
|
193
|
+
if (span === undefined)
|
|
194
|
+
continue;
|
|
195
|
+
// `pnpm dev:*` and `pnpm <script>` name a family, not a script.
|
|
196
|
+
if (/^[*<]/.test(text.slice(match.index + match[0].length)))
|
|
197
|
+
continue;
|
|
198
|
+
// Only a command's start runs anything: `using pnpm v11` in a quoted log line is output.
|
|
199
|
+
if (!start.test(code.slice(span[0], match.index)))
|
|
200
|
+
continue;
|
|
201
|
+
const report = (reason) => {
|
|
202
|
+
problems.push({ file: relative, line: index + 1, script, reason });
|
|
203
|
+
};
|
|
204
|
+
if (!defined.has(script)) {
|
|
205
|
+
report(`no workspace defines a \`${script}\` script — this step runs nothing`);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Existing somewhere is not resolving where the doc runs it: an unknown
|
|
209
|
+
// --filter target is not this check's to adjudicate.
|
|
210
|
+
const scope = scopes.get(filter ?? '.');
|
|
211
|
+
if (scope !== undefined && !scope.has(script)) {
|
|
212
|
+
report(filter === undefined
|
|
213
|
+
? `\`${script}\` is not a root script — \`pnpm ${script}\` resolves the root manifest, so this needs \`--filter\``
|
|
214
|
+
: `\`${script}\` is not a script of \`${filter}\` — the filter names a workspace that does not define it`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const safer = config.scripts.mutating.get(script);
|
|
218
|
+
if (safer !== undefined && config.scripts.verification.test(line))
|
|
219
|
+
report(`\`${script}\` MUTATES the tree (it auto-fixes) — a verification step must use \`${safer}\``);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return problems;
|
|
224
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Just enough shell lexing for the commands a doc shows: a `#` comment cut
|
|
2
|
+
// off, and quoted text masked so an operator inside a string splits nothing.
|
|
3
|
+
// Not a parser; each caller says what it reads.
|
|
4
|
+
/**
|
|
5
|
+
* The characters of `code` from `start`, each with its index. Every scanner
|
|
6
|
+
* walks this bounded sequence, skipping indexes below the point a token it
|
|
7
|
+
* consumed ends at, so no scan runs past the end or loops forever. Lazy: a
|
|
8
|
+
* scan that stops early never builds the rest.
|
|
9
|
+
*/
|
|
10
|
+
function* charsFrom(code, start) {
|
|
11
|
+
for (let index = start; index < code.length; index += 1)
|
|
12
|
+
yield [index, code.charAt(index)];
|
|
13
|
+
}
|
|
14
|
+
/** A line continued by backslash-newline: an odd run of trailing backslashes. */
|
|
15
|
+
const CONTINUATION = /(?:^|[^\\])(?:\\\\)*\\$/;
|
|
16
|
+
/**
|
|
17
|
+
* Whether `#` at `at` starts a word (a comment), not `$#`, `a#b`, `${#x}` or
|
|
18
|
+
* `{#a,b}`: the line starts there, or a blank, `;` or `(` precedes it.
|
|
19
|
+
*/
|
|
20
|
+
const startsWord = (line, at) => /(?:^|[\s;(])$/.test(line.slice(0, at));
|
|
21
|
+
/**
|
|
22
|
+
* Where an unquoted `#` comment starts in `line` (-1 if none). A `$(…)`
|
|
23
|
+
* inside double quotes has its own quotes, so `"$(printf "%s" " #")"` holds
|
|
24
|
+
* no comment.
|
|
25
|
+
*/
|
|
26
|
+
const commentAt = (line) => {
|
|
27
|
+
const stack = [];
|
|
28
|
+
let resume = 0;
|
|
29
|
+
for (const [at, char] of charsFrom(line, 0)) {
|
|
30
|
+
if (at < resume)
|
|
31
|
+
continue;
|
|
32
|
+
const top = stack.at(-1);
|
|
33
|
+
if (top === "'") {
|
|
34
|
+
if (char === "'")
|
|
35
|
+
stack.pop();
|
|
36
|
+
}
|
|
37
|
+
else if (char === '\\') {
|
|
38
|
+
resume = at + 2;
|
|
39
|
+
}
|
|
40
|
+
else if (top === '"') {
|
|
41
|
+
if (char === '"')
|
|
42
|
+
stack.pop();
|
|
43
|
+
if (line.startsWith('$(', at)) {
|
|
44
|
+
stack.push(')');
|
|
45
|
+
resume = at + 2;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (char === '#' && startsWord(line, at)) {
|
|
49
|
+
// At top level or inside a quoted `$(…)` body, the rest is a comment.
|
|
50
|
+
return at;
|
|
51
|
+
}
|
|
52
|
+
else if (char === '"' || char === "'") {
|
|
53
|
+
stack.push(char);
|
|
54
|
+
}
|
|
55
|
+
else if (top === ')' && char === '(') {
|
|
56
|
+
stack.push(')');
|
|
57
|
+
}
|
|
58
|
+
else if (top === ')' && char === ')') {
|
|
59
|
+
stack.pop();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* `line` without a `#` comment that starts a word outside quotes, trailing
|
|
66
|
+
* blanks trimmed, except an escaped one: `echo ok \ # note` keeps `\ `,
|
|
67
|
+
* which is no continuation.
|
|
68
|
+
*/
|
|
69
|
+
export const stripComment = (line) => {
|
|
70
|
+
const at = commentAt(line);
|
|
71
|
+
if (at === -1)
|
|
72
|
+
return line;
|
|
73
|
+
const trimmed = line.slice(0, at).trimEnd();
|
|
74
|
+
return CONTINUATION.test(trimmed) ? `${trimmed} ` : trimmed;
|
|
75
|
+
};
|
|
76
|
+
/** Whether `$(` at `at` opens a command substitution (`$((` is arithmetic). */
|
|
77
|
+
const opensSubstitution = (code, at) => code.startsWith('$(', at) && !code.startsWith('$((', at);
|
|
78
|
+
/**
|
|
79
|
+
* Outside quotes, `\` makes the next character literal. An escaped operator,
|
|
80
|
+
* quote, `$` or backtick is blanked with its backslash; any other stays as
|
|
81
|
+
* written. Returns the index past the escape.
|
|
82
|
+
*/
|
|
83
|
+
const maskEscape = (code, at, out) => {
|
|
84
|
+
if (/^\\[;&|()<>`$"']/.test(code.slice(at)))
|
|
85
|
+
out.fill(' ', at, at + 2);
|
|
86
|
+
return at + 2;
|
|
87
|
+
};
|
|
88
|
+
/** Blanks a single-quoted body from `start`; returns the index past its close. */
|
|
89
|
+
const maskSingle = (code, start, out) => {
|
|
90
|
+
const close = code.indexOf("'", start);
|
|
91
|
+
const end = close === -1 ? code.length : close;
|
|
92
|
+
out.fill(' ', start, end);
|
|
93
|
+
return end + 1;
|
|
94
|
+
};
|
|
95
|
+
/** What the substitution skippers return when the body never closes. */
|
|
96
|
+
const NEVER_CLOSES = Number.POSITIVE_INFINITY;
|
|
97
|
+
/** A case pattern just before its `)`: after `in`, `;;` or a line start. */
|
|
98
|
+
const CASE_PATTERN_BEFORE = /(?:^|\bin|;;|\n)\s*[^\s;&|()]+(?:\s*\|\s*[^\s;&|()]+)*\s*$/;
|
|
99
|
+
/** Whether a `)` after `before` ends a pattern of a `case` open in `before`. */
|
|
100
|
+
const closesCasePattern = (before) => {
|
|
101
|
+
const cases = before.match(/\bcase\b/g)?.length ?? 0;
|
|
102
|
+
const ends = before.match(/\besac\b/g)?.length ?? 0;
|
|
103
|
+
return cases > ends && CASE_PATTERN_BEFORE.test(before);
|
|
104
|
+
};
|
|
105
|
+
/** Leaves a backtick substitution's body as code, masking the strings inside it; `open` is the opening backtick. */
|
|
106
|
+
const skipBacktick = (code, open, out) => {
|
|
107
|
+
let resume = open;
|
|
108
|
+
for (const [at, char] of charsFrom(code, open + 1)) {
|
|
109
|
+
if (at < resume)
|
|
110
|
+
continue;
|
|
111
|
+
if (char === '`')
|
|
112
|
+
return at + 1;
|
|
113
|
+
if (char === '\\')
|
|
114
|
+
resume = maskEscape(code, at, out);
|
|
115
|
+
else if (char === "'")
|
|
116
|
+
resume = maskSingle(code, at + 1, out);
|
|
117
|
+
else if (char === '"')
|
|
118
|
+
resume = maskDouble(code, at + 1, out);
|
|
119
|
+
}
|
|
120
|
+
return NEVER_CLOSES;
|
|
121
|
+
};
|
|
122
|
+
/** Leaves a `$( … )` body as code, masking strings inside it; returns the index past `)`. */
|
|
123
|
+
const skipSubstitution = (code, start, out) => {
|
|
124
|
+
let depth = 1;
|
|
125
|
+
let resume = start;
|
|
126
|
+
for (const [at, char] of charsFrom(code, start)) {
|
|
127
|
+
if (at < resume)
|
|
128
|
+
continue;
|
|
129
|
+
if (char === '\\')
|
|
130
|
+
resume = maskEscape(code, at, out);
|
|
131
|
+
else if (char === "'")
|
|
132
|
+
resume = maskSingle(code, at + 1, out);
|
|
133
|
+
else if (char === '"')
|
|
134
|
+
resume = maskDouble(code, at + 1, out);
|
|
135
|
+
else if (char === '(')
|
|
136
|
+
depth += 1;
|
|
137
|
+
else if (char === ')' && !closesCasePattern(code.slice(start, at))) {
|
|
138
|
+
// A case arm's `)` (`$(case $x in a) …; esac)`) closes nothing.
|
|
139
|
+
depth -= 1;
|
|
140
|
+
if (depth === 0)
|
|
141
|
+
return at + 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return NEVER_CLOSES;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Blanks a double-quoted body from `start`, leaving `$(…)` and backtick
|
|
148
|
+
* substitutions as code; returns the index past the closing quote.
|
|
149
|
+
*/
|
|
150
|
+
function maskDouble(code, start, out) {
|
|
151
|
+
let resume = start;
|
|
152
|
+
for (const [at, char] of charsFrom(code, start)) {
|
|
153
|
+
if (at < resume)
|
|
154
|
+
continue;
|
|
155
|
+
if (char === '"')
|
|
156
|
+
return at + 1;
|
|
157
|
+
if (char === '\\') {
|
|
158
|
+
out.fill(' ', at, at + 2);
|
|
159
|
+
resume = at + 2;
|
|
160
|
+
}
|
|
161
|
+
else if (opensSubstitution(code, at)) {
|
|
162
|
+
resume = skipSubstitution(code, at + 2, out);
|
|
163
|
+
}
|
|
164
|
+
else if (char === '`') {
|
|
165
|
+
resume = skipBacktick(code, at, out);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
out[at] = ' ';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return code.length;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Blanks the contents of quoted strings, keeping every index in place so a
|
|
175
|
+
* position found in the masked text is the same position in `code`:
|
|
176
|
+
* `"a b"` → `" "`. A command substitution inside double quotes runs, so
|
|
177
|
+
* it stays visible; an escaped operator or quote is blanked too.
|
|
178
|
+
*/
|
|
179
|
+
export const maskQuoted = (code) => {
|
|
180
|
+
const out = code.split('');
|
|
181
|
+
let resume = 0;
|
|
182
|
+
for (const [index, char] of charsFrom(code, 0)) {
|
|
183
|
+
if (index < resume)
|
|
184
|
+
continue;
|
|
185
|
+
if (char === '\\')
|
|
186
|
+
resume = maskEscape(code, index, out);
|
|
187
|
+
else if (char === "'")
|
|
188
|
+
resume = maskSingle(code, index + 1, out);
|
|
189
|
+
else if (char === '"')
|
|
190
|
+
resume = maskDouble(code, index + 1, out);
|
|
191
|
+
}
|
|
192
|
+
return out.join('');
|
|
193
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veda-hq/pariksha",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: authority-citations, boundary, codec-registry, coverage-by-type, coverage-exemptions, doc-budgets, enforcement-claims, env-gate-switches, export-deletions, fault-injection-registry, required-settings. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
|
|
3
|
+
"version": "0.5.14",
|
|
4
|
+
"description": "Repository checks: doc drift, rule enforcement, test-quality ratchets. Checks: authority-citations, boundary, codec-registry, coverage-by-type, coverage-exemptions, doc-budgets, doc-references, enforcement-claims, env-gate-switches, export-deletions, fault-injection-registry, required-settings. Also markdownlint rules for Markdown that renders mangled: @veda-hq/pariksha/markdownlint.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"repository checks",
|
|
7
7
|
"lint",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "24.19.2",
|
|
58
58
|
"eslint": "9.39.1",
|
|
59
|
+
"mdast-util-from-markdown": "2.0.3",
|
|
59
60
|
"typescript": "5.9.3"
|
|
60
61
|
},
|
|
61
62
|
"scripts": {
|