@descent-vtt/spec-guard 0.2.0 → 0.3.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/CHANGELOG.md +90 -0
- package/README.md +173 -12
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -3
- package/dist/cli.js.map +1 -1
- package/dist/comments.d.ts +78 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +241 -0
- package/dist/comments.js.map +1 -0
- package/dist/engine.d.ts +29 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +155 -13
- package/dist/engine.js.map +1 -1
- package/dist/glob.d.ts +15 -0
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +36 -0
- package/dist/glob.js.map +1 -1
- package/dist/imports.d.ts +94 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +492 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +24 -2
- package/dist/parser.js.map +1 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +37 -2
- package/dist/reporter.js.map +1 -1
- package/dist/runner.d.ts +12 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +168 -12
- package/dist/runner.js.map +1 -1
- package/dist/types.d.ts +34 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/comments.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment classification.
|
|
3
|
+
*
|
|
4
|
+
* A documented codebase defeats a plain text search in the most ironic way
|
|
5
|
+
* available: the comment recording that a symbol was deleted is itself a match
|
|
6
|
+
* for that symbol. `@assert-absence symbol="LegacyThing"` then fails on
|
|
7
|
+
*
|
|
8
|
+
* // LegacyThing was removed in ADR-398
|
|
9
|
+
*
|
|
10
|
+
* which is the sentence proving the assertion true. So matches are classified
|
|
11
|
+
* and, by default, those inside comments do not count.
|
|
12
|
+
*
|
|
13
|
+
* The scanner tracks strings as well as comments, and it has to: a `//` inside
|
|
14
|
+
* a string literal is not a comment, and misreading one is the dangerous
|
|
15
|
+
* direction. Consider
|
|
16
|
+
*
|
|
17
|
+
* const url = "http://example.com"; const x = LegacyThing;
|
|
18
|
+
*
|
|
19
|
+
* Treating `//` in that URL as a comment start would hide a real use of
|
|
20
|
+
* LegacyThing and turn a violation into a silent pass. Every uncertainty here
|
|
21
|
+
* therefore resolves the same way: **when in doubt, it is code**. A match
|
|
22
|
+
* wrongly kept is a visible failure someone can argue with; a match wrongly
|
|
23
|
+
* dropped is a lie.
|
|
24
|
+
*
|
|
25
|
+
* Languages it does not recognise get no comment ranges at all, so their
|
|
26
|
+
* matches all count - and the result says so rather than implying the file was
|
|
27
|
+
* understood.
|
|
28
|
+
*/
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
const QUOTES = [
|
|
31
|
+
{ open: '"', close: '"', escape: true },
|
|
32
|
+
{ open: "'", close: "'", escape: true },
|
|
33
|
+
];
|
|
34
|
+
const C_LIKE = {
|
|
35
|
+
name: 'c-like',
|
|
36
|
+
line: ['//'],
|
|
37
|
+
block: [['/*', '*/']],
|
|
38
|
+
nested: false,
|
|
39
|
+
strings: QUOTES,
|
|
40
|
+
};
|
|
41
|
+
const JS_LIKE = {
|
|
42
|
+
...C_LIKE,
|
|
43
|
+
name: 'javascript',
|
|
44
|
+
strings: [...QUOTES, { open: '`', close: '`', escape: true }],
|
|
45
|
+
};
|
|
46
|
+
const C_SHARP = {
|
|
47
|
+
...C_LIKE,
|
|
48
|
+
name: 'c#',
|
|
49
|
+
// Verbatim strings come first so @" wins over ".
|
|
50
|
+
strings: [{ open: '@"', close: '"', escape: false }, ...QUOTES],
|
|
51
|
+
};
|
|
52
|
+
const RUST = {
|
|
53
|
+
name: 'rust',
|
|
54
|
+
line: ['//'],
|
|
55
|
+
block: [['/*', '*/']],
|
|
56
|
+
// Rust block comments nest, so /* /* */ */ is one comment, not one and a half.
|
|
57
|
+
nested: true,
|
|
58
|
+
strings: [
|
|
59
|
+
{ open: 'r#"', close: '"#', escape: false },
|
|
60
|
+
{ open: 'r"', close: '"', escape: false },
|
|
61
|
+
...QUOTES,
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
const GO = {
|
|
65
|
+
...C_LIKE,
|
|
66
|
+
name: 'go',
|
|
67
|
+
strings: [...QUOTES, { open: '`', close: '`', escape: false }],
|
|
68
|
+
};
|
|
69
|
+
const HASH = {
|
|
70
|
+
name: 'hash',
|
|
71
|
+
line: ['#'],
|
|
72
|
+
block: [],
|
|
73
|
+
nested: false,
|
|
74
|
+
// Triple quotes first: a Python docstring is a string, not a comment, and
|
|
75
|
+
// treating it as code is the conservative reading.
|
|
76
|
+
strings: [
|
|
77
|
+
{ open: '"""', close: '"""', escape: true },
|
|
78
|
+
{ open: "'''", close: "'''", escape: true },
|
|
79
|
+
...QUOTES,
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
const SQL_LIKE = {
|
|
83
|
+
name: 'sql-like',
|
|
84
|
+
line: ['--'],
|
|
85
|
+
block: [['/*', '*/']],
|
|
86
|
+
nested: false,
|
|
87
|
+
strings: QUOTES,
|
|
88
|
+
};
|
|
89
|
+
const MARKUP = {
|
|
90
|
+
name: 'markup',
|
|
91
|
+
line: [],
|
|
92
|
+
block: [['<!--', '-->']],
|
|
93
|
+
nested: false,
|
|
94
|
+
strings: [],
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Formats with no comments at all.
|
|
98
|
+
*
|
|
99
|
+
* Worth naming rather than leaving unknown: "this file has no comments" is
|
|
100
|
+
* something we know about JSON, and saying "I could not read it" instead would
|
|
101
|
+
* attach a caveat to every match in a package.json. Dialects that do allow
|
|
102
|
+
* comments in practice - tsconfig.json and its `//` lines - simply have them
|
|
103
|
+
* counted as code, which is the direction that fails loudly.
|
|
104
|
+
*/
|
|
105
|
+
const NO_COMMENTS = {
|
|
106
|
+
name: 'none',
|
|
107
|
+
line: [],
|
|
108
|
+
block: [],
|
|
109
|
+
nested: false,
|
|
110
|
+
strings: [],
|
|
111
|
+
};
|
|
112
|
+
/** Extension to comment syntax. Anything absent is left unclassified. */
|
|
113
|
+
const BY_EXTENSION = new Map([
|
|
114
|
+
...['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx'].map((extension) => [extension, JS_LIKE]),
|
|
115
|
+
...['.c', '.h', '.cc', '.cpp', '.cxx', '.hpp', '.java', '.kt', '.kts', '.scala', '.swift', '.dart', '.php', '.m', '.mm', '.zig'].map((extension) => [extension, C_LIKE]),
|
|
116
|
+
...['.cs', '.csx'].map((extension) => [extension, C_SHARP]),
|
|
117
|
+
...['.rs'].map((extension) => [extension, RUST]),
|
|
118
|
+
...['.go'].map((extension) => [extension, GO]),
|
|
119
|
+
...['.py', '.pyi', '.rb', '.sh', '.bash', '.zsh', '.yaml', '.yml', '.toml', '.tf', '.pl', '.r'].map((extension) => [extension, HASH]),
|
|
120
|
+
...['.sql', '.lua', '.hs', '.elm'].map((extension) => [extension, SQL_LIKE]),
|
|
121
|
+
...['.html', '.htm', '.xml', '.svg', '.vue', '.svelte', '.md', '.markdown'].map((extension) => [extension, MARKUP]),
|
|
122
|
+
// .jsonc is named for the comments it allows, so it gets the C-style reader.
|
|
123
|
+
...['.jsonc'].map((extension) => [extension, C_LIKE]),
|
|
124
|
+
...['.json', '.txt', '.csv', '.tsv', '.lock', '.log'].map((extension) => [extension, NO_COMMENTS]),
|
|
125
|
+
]);
|
|
126
|
+
/** The comment syntax for a path, or null when the language is unknown. */
|
|
127
|
+
export function syntaxFor(filePath) {
|
|
128
|
+
return BY_EXTENSION.get(path.extname(filePath).toLowerCase()) ?? null;
|
|
129
|
+
}
|
|
130
|
+
/** Where the literal opened at `at` ends, or the end of the file. */
|
|
131
|
+
function endOfString(source, at, rule) {
|
|
132
|
+
let index = at + rule.open.length;
|
|
133
|
+
while (index < source.length) {
|
|
134
|
+
if (rule.escape && source[index] === '\\') {
|
|
135
|
+
index += 2;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (source.startsWith(rule.close, index))
|
|
139
|
+
return index + rule.close.length;
|
|
140
|
+
index += 1;
|
|
141
|
+
}
|
|
142
|
+
return source.length;
|
|
143
|
+
}
|
|
144
|
+
/** Where the block comment opened at `at` ends, or the end of the file. */
|
|
145
|
+
function endOfBlock(source, at, pair, nested) {
|
|
146
|
+
const [open, close] = pair;
|
|
147
|
+
let index = at + open.length;
|
|
148
|
+
let depth = 1;
|
|
149
|
+
while (index < source.length && depth > 0) {
|
|
150
|
+
if (nested && source.startsWith(open, index)) {
|
|
151
|
+
depth += 1;
|
|
152
|
+
index += open.length;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (source.startsWith(close, index)) {
|
|
156
|
+
depth -= 1;
|
|
157
|
+
index += close.length;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
index += 1;
|
|
161
|
+
}
|
|
162
|
+
return index;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Finds every comment in the source.
|
|
166
|
+
*
|
|
167
|
+
* One pass, tracking strings so that comment markers inside them are ignored.
|
|
168
|
+
* An unterminated comment or string runs to the end of the file, which matches
|
|
169
|
+
* how a compiler would read it.
|
|
170
|
+
*
|
|
171
|
+
* The scan is written so that it cannot stand still: whatever the branches
|
|
172
|
+
* below decide, the loop advances by at least one character. That guarantee is
|
|
173
|
+
* the difference between a wrong answer and a hang, and this loop appends as it
|
|
174
|
+
* goes - standing still here would not spin, it would eat memory until the
|
|
175
|
+
* process died, on somebody's file, in somebody's CI.
|
|
176
|
+
*/
|
|
177
|
+
export function commentRanges(source, syntax) {
|
|
178
|
+
const ranges = [];
|
|
179
|
+
let index = 0;
|
|
180
|
+
while (index < source.length) {
|
|
181
|
+
const start = index;
|
|
182
|
+
const startsWith = (token) => source.startsWith(token, start);
|
|
183
|
+
// First match wins, in this order: a literal hides comment markers inside
|
|
184
|
+
// it, and a line comment hides a block opener on the same line. Each lookup
|
|
185
|
+
// sits in the branch that needs it, so none of them runs speculatively.
|
|
186
|
+
const stringRule = syntax.strings.find((rule) => startsWith(rule.open));
|
|
187
|
+
if (stringRule) {
|
|
188
|
+
index = endOfString(source, start, stringRule);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const lineToken = syntax.line.find((token) => startsWith(token));
|
|
192
|
+
if (lineToken !== undefined) {
|
|
193
|
+
const newline = source.indexOf('\n', start);
|
|
194
|
+
index = newline === -1 ? source.length : newline;
|
|
195
|
+
ranges.push([start, index]);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const blockPair = syntax.block.find(([open]) => startsWith(open));
|
|
199
|
+
if (blockPair) {
|
|
200
|
+
index = endOfBlock(source, start, blockPair, syntax.nested);
|
|
201
|
+
ranges.push([start, index]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// The single advance point, and the only place termination depends on.
|
|
206
|
+
if (index <= start)
|
|
207
|
+
index = start + 1;
|
|
208
|
+
}
|
|
209
|
+
return ranges;
|
|
210
|
+
}
|
|
211
|
+
const UNCLASSIFIED = {
|
|
212
|
+
classified: false,
|
|
213
|
+
syntax: null,
|
|
214
|
+
isComment: () => false,
|
|
215
|
+
};
|
|
216
|
+
export function createCommentMask(source, filePath) {
|
|
217
|
+
const syntax = syntaxFor(filePath);
|
|
218
|
+
if (!syntax)
|
|
219
|
+
return UNCLASSIFIED;
|
|
220
|
+
const ranges = commentRanges(source, syntax);
|
|
221
|
+
return {
|
|
222
|
+
classified: true,
|
|
223
|
+
syntax: syntax.name,
|
|
224
|
+
isComment(offset) {
|
|
225
|
+
let low = 0;
|
|
226
|
+
let high = ranges.length - 1;
|
|
227
|
+
while (low <= high) {
|
|
228
|
+
const mid = (low + high) >> 1;
|
|
229
|
+
const [start, end] = ranges[mid];
|
|
230
|
+
if (offset < start)
|
|
231
|
+
high = mid - 1;
|
|
232
|
+
else if (offset >= end)
|
|
233
|
+
low = mid + 1;
|
|
234
|
+
else
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.js","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* flags are answered by a single pass. Measured on a 2,000 file / 5 MB tree,
|
|
15
15
|
* eight assertions cost ~280ms as eight ripgrep passes and ~90ms as one.
|
|
16
16
|
*/
|
|
17
|
+
import { type CommentMask } from './comments.js';
|
|
17
18
|
import type { EngineName, MatchLocation, SearchOptions, SearchResult } from './types.js';
|
|
18
19
|
/** Files larger than this are skipped by both engines, keeping them in sync. */
|
|
19
20
|
export declare const MAX_FILE_SIZE: number;
|
|
@@ -37,10 +38,27 @@ export interface Engine {
|
|
|
37
38
|
}
|
|
38
39
|
export type EnginePreference = 'auto' | 'ripgrep' | 'javascript';
|
|
39
40
|
export declare function escapeRegExp(value: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Converts ripgrep's byte column into a character column.
|
|
43
|
+
*
|
|
44
|
+
* ripgrep counts bytes; the scanner counts characters, and so does every editor
|
|
45
|
+
* that a reader will paste the location into. On an ASCII line the two agree,
|
|
46
|
+
* which is why the difference went unnoticed - it only appears once a line
|
|
47
|
+
* holds a non-ASCII character before the match, and then the two engines report
|
|
48
|
+
* different columns for the same match.
|
|
49
|
+
*/
|
|
50
|
+
export declare function byteColumnToCharacter(line: string, byteOffset: number): number;
|
|
51
|
+
/**
|
|
52
|
+
* Orders matches by path, then by line. ripgrep searches in parallel and emits
|
|
53
|
+
* files in no fixed order, so this is what makes snippet output stable.
|
|
54
|
+
*/
|
|
55
|
+
export declare function sortLocations(locations: MatchLocation[]): MatchLocation[];
|
|
40
56
|
/** Per-pattern accumulator used by both engines. */
|
|
41
57
|
interface Tally {
|
|
42
58
|
count: number;
|
|
43
59
|
locations: MatchLocation[];
|
|
60
|
+
/** Matches skipped because they were inside a comment. */
|
|
61
|
+
commentCount: number;
|
|
44
62
|
}
|
|
45
63
|
/**
|
|
46
64
|
* True when no two of these literals can ever match overlapping text, which is
|
|
@@ -109,7 +127,14 @@ export interface EnumerationBudget {
|
|
|
109
127
|
*/
|
|
110
128
|
export declare function enumerateCandidates(request: SearchRequest, budget?: EnumerationBudget): Promise<Enumeration>;
|
|
111
129
|
/** Counts matches in one file and records per-line snippets. */
|
|
112
|
-
export declare function scanContent(content: string, relativePath: string, regexp: RegExp
|
|
130
|
+
export declare function scanContent(content: string, relativePath: string, regexp: RegExp,
|
|
131
|
+
/**
|
|
132
|
+
* Resolved lazily, and only once the file has produced a match. Classifying
|
|
133
|
+
* every file in scope would mean a comment scan of the whole tree; this way
|
|
134
|
+
* the cost is proportional to matches, which for an architecture assertion is
|
|
135
|
+
* usually zero.
|
|
136
|
+
*/
|
|
137
|
+
getMask?: () => CommentMask): Tally;
|
|
113
138
|
/** An engine that is guaranteed to implement the batch API. */
|
|
114
139
|
export type BatchEngine = Engine & Required<Pick<Engine, 'searchBatch'>> & {
|
|
115
140
|
/** Scans a file list that the caller already enumerated. */
|
|
@@ -133,6 +158,9 @@ export declare const javascriptEngine: BatchEngine;
|
|
|
133
158
|
* Windows avoids a spawn that would cost ten times the whole search.
|
|
134
159
|
*/
|
|
135
160
|
export declare const SMALL_TREE_BUDGET: EnumerationBudget;
|
|
161
|
+
/** Exported so the code list can be asserted; EACCES and friends are not
|
|
162
|
+
* reproducible on demand from a real spawn. */
|
|
163
|
+
export declare function isMissingBinary(error: unknown): boolean;
|
|
136
164
|
/** Resolves the engine to use, honouring an explicit preference. */
|
|
137
165
|
export declare function resolveEngine(preference?: EnginePreference): Promise<Engine>;
|
|
138
166
|
export type CachedEngine = Engine & {
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
import { spawn } from 'node:child_process';
|
|
18
18
|
import { promises as fs } from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
-
import {
|
|
20
|
+
import { createCommentMask } from './comments.js';
|
|
21
|
+
import { createExcludeMatcher, createGlobMatcher, toPosix, walkFiles } from './glob.js';
|
|
21
22
|
/** Files larger than this are skipped by both engines, keeping them in sync. */
|
|
22
23
|
export const MAX_FILE_SIZE = 20 * 1024 * 1024;
|
|
23
24
|
/** Hard cap on collected locations; the reporter only ever shows a handful. */
|
|
@@ -31,11 +32,31 @@ function truncate(value) {
|
|
|
31
32
|
const trimmed = value.replace(/\r?\n$/, '');
|
|
32
33
|
return trimmed.length > MAX_SNIPPET_LENGTH ? `${trimmed.slice(0, MAX_SNIPPET_LENGTH)}…` : trimmed;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Converts ripgrep's byte column into a character column.
|
|
37
|
+
*
|
|
38
|
+
* ripgrep counts bytes; the scanner counts characters, and so does every editor
|
|
39
|
+
* that a reader will paste the location into. On an ASCII line the two agree,
|
|
40
|
+
* which is why the difference went unnoticed - it only appears once a line
|
|
41
|
+
* holds a non-ASCII character before the match, and then the two engines report
|
|
42
|
+
* different columns for the same match.
|
|
43
|
+
*/
|
|
44
|
+
export function byteColumnToCharacter(line, byteOffset) {
|
|
45
|
+
// One byte per character, so the offset is already a character count. This
|
|
46
|
+
// also covers the case where ripgrep gave us no line text to measure.
|
|
47
|
+
if (line.length === Buffer.byteLength(line))
|
|
48
|
+
return byteOffset + 1;
|
|
49
|
+
return Buffer.from(line, 'utf8').subarray(0, byteOffset).toString('utf8').length + 1;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Orders matches by path, then by line. ripgrep searches in parallel and emits
|
|
53
|
+
* files in no fixed order, so this is what makes snippet output stable.
|
|
54
|
+
*/
|
|
55
|
+
export function sortLocations(locations) {
|
|
35
56
|
return locations.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1));
|
|
36
57
|
}
|
|
37
58
|
function emptyTally() {
|
|
38
|
-
return { count: 0, locations: [] };
|
|
59
|
+
return { count: 0, locations: [], commentCount: 0 };
|
|
39
60
|
}
|
|
40
61
|
/**
|
|
41
62
|
* True when no two of these literals can ever match overlapping text, which is
|
|
@@ -75,6 +96,8 @@ function sharesOnePass(requests) {
|
|
|
75
96
|
request.options.ignoreCase === first.options.ignoreCase &&
|
|
76
97
|
request.options.globs.length === first.options.globs.length &&
|
|
77
98
|
request.options.globs.every((glob, index) => glob === first.options.globs[index]) &&
|
|
99
|
+
request.options.excludeGlobs.length === first.options.excludeGlobs.length &&
|
|
100
|
+
request.options.excludeGlobs.every((glob, index) => glob === first.options.excludeGlobs[index]) &&
|
|
78
101
|
request.options.excludeFiles === first.options.excludeFiles);
|
|
79
102
|
}
|
|
80
103
|
/** Runs requests through the batch API when the engine has one. */
|
|
@@ -131,6 +154,10 @@ export function buildRipgrepArgs(request, patterns = [request.symbol]) {
|
|
|
131
154
|
args.push('--ignore-case');
|
|
132
155
|
for (const glob of options.globs)
|
|
133
156
|
args.push('--glob', glob);
|
|
157
|
+
// ripgrep reads a leading "!" as an exclusion, with gitignore semantics that
|
|
158
|
+
// createExcludeMatcher mirrors for the JavaScript engine.
|
|
159
|
+
for (const glob of options.excludeGlobs)
|
|
160
|
+
args.push('--glob', `!${glob}`);
|
|
134
161
|
for (const pattern of patterns)
|
|
135
162
|
args.push('--regexp', pattern);
|
|
136
163
|
args.push('--');
|
|
@@ -147,6 +174,10 @@ class RipgrepEngine {
|
|
|
147
174
|
this.binary = binary;
|
|
148
175
|
}
|
|
149
176
|
async search(request) {
|
|
177
|
+
if (request.options.ignoreComments) {
|
|
178
|
+
const [result] = await this.commentAware(request, [request.symbol], [request]);
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
150
181
|
const tallies = await this.run(request, [request.symbol]);
|
|
151
182
|
return this.toResult(tallies.get(request.symbol));
|
|
152
183
|
}
|
|
@@ -155,6 +186,15 @@ class RipgrepEngine {
|
|
|
155
186
|
if (!first)
|
|
156
187
|
return [];
|
|
157
188
|
const patterns = [...new Set(requests.map((request) => request.symbol))];
|
|
189
|
+
// Comment-aware counting needs the file's text, which ripgrep's match
|
|
190
|
+
// stream does not carry. Rather than re-implement classification twice,
|
|
191
|
+
// ripgrep is used for what it is unmatched at - telling us which handful of
|
|
192
|
+
// files out of thousands contain the symbol at all - and those files are
|
|
193
|
+
// then counted by the scanner, which already knows about comments. Both
|
|
194
|
+
// engines therefore produce comment-aware counts through one code path.
|
|
195
|
+
if (first.options.ignoreComments) {
|
|
196
|
+
return this.commentAware(first, patterns, requests);
|
|
197
|
+
}
|
|
158
198
|
if (!shouldBatchPatterns(patterns, first.options)) {
|
|
159
199
|
return Promise.all(requests.map((request) => this.search(request)));
|
|
160
200
|
}
|
|
@@ -168,9 +208,63 @@ class RipgrepEngine {
|
|
|
168
208
|
return Promise.all(requests.map((request) => this.search(request)));
|
|
169
209
|
}
|
|
170
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Two-phase search, used whenever comments must be classified.
|
|
213
|
+
*
|
|
214
|
+
* The result still names ripgrep: it did the searching, and classification is
|
|
215
|
+
* a post-step both engines share rather than a different engine.
|
|
216
|
+
*/
|
|
217
|
+
async commentAware(request, patterns, requests) {
|
|
218
|
+
const files = await this.filesWithMatches(request, patterns);
|
|
219
|
+
const results = await javascriptEngine.searchFiles(files, requests);
|
|
220
|
+
return results.map((result) => ({ ...result, engine: this.name }));
|
|
221
|
+
}
|
|
222
|
+
/** Phase one: which files contain any of these patterns at all. */
|
|
223
|
+
filesWithMatches(request, patterns) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const args = buildRipgrepArgs(request, patterns).map((argument) => argument === '--json' ? '--files-with-matches' : argument);
|
|
226
|
+
const child = spawn(this.binary, args, {
|
|
227
|
+
cwd: request.root,
|
|
228
|
+
windowsHide: true,
|
|
229
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
230
|
+
});
|
|
231
|
+
let stdout = '';
|
|
232
|
+
let stderr = '';
|
|
233
|
+
child.stdout.setEncoding('utf8');
|
|
234
|
+
child.stdout.on('data', (chunk) => (stdout += chunk));
|
|
235
|
+
child.stderr.setEncoding('utf8');
|
|
236
|
+
child.stderr.on('data', (chunk) => (stderr += chunk));
|
|
237
|
+
child.once('error', reject);
|
|
238
|
+
child.once('close', (code) => {
|
|
239
|
+
// 0 = matches, 1 = none, anything else is a real failure.
|
|
240
|
+
if (code !== 0 && code !== 1) {
|
|
241
|
+
reject(new Error(`ripgrep exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const files = [];
|
|
245
|
+
for (const line of stdout.split('\n')) {
|
|
246
|
+
const trimmed = line.trim();
|
|
247
|
+
if (trimmed.length === 0)
|
|
248
|
+
continue;
|
|
249
|
+
const absolutePath = path.resolve(request.root, trimmed);
|
|
250
|
+
if (request.options.excludeFiles.has(absolutePath))
|
|
251
|
+
continue;
|
|
252
|
+
files.push({ absolutePath, relativePath: toPosix(path.relative(request.root, absolutePath)) });
|
|
253
|
+
}
|
|
254
|
+
files.sort((a, b) => (a.relativePath < b.relativePath ? -1 : 1));
|
|
255
|
+
resolve(files);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
171
259
|
toResult(tally) {
|
|
172
260
|
const resolved = tally ?? emptyTally();
|
|
173
|
-
return {
|
|
261
|
+
return {
|
|
262
|
+
count: resolved.count,
|
|
263
|
+
commentMatches: resolved.commentCount,
|
|
264
|
+
unclassifiedFiles: 0,
|
|
265
|
+
matches: sortLocations(resolved.locations),
|
|
266
|
+
engine: this.name,
|
|
267
|
+
};
|
|
174
268
|
}
|
|
175
269
|
/** One ripgrep pass; returns a tally per pattern. */
|
|
176
270
|
run(request, patterns) {
|
|
@@ -266,7 +360,8 @@ export function createRipgrepSink(request, patterns) {
|
|
|
266
360
|
return;
|
|
267
361
|
const file = toPosix(path.relative(request.root, absolute));
|
|
268
362
|
const lineNumber = data.line_number ?? 0;
|
|
269
|
-
const
|
|
363
|
+
const lineText = decodeText(data.lines);
|
|
364
|
+
const text_ = truncate(lineText);
|
|
270
365
|
for (const submatch of submatches) {
|
|
271
366
|
// With --fixed-strings and no --ignore-case the matched text is the
|
|
272
367
|
// pattern verbatim, which is what makes attribution exact.
|
|
@@ -282,7 +377,13 @@ export function createRipgrepSink(request, patterns) {
|
|
|
282
377
|
last.count += 1;
|
|
283
378
|
}
|
|
284
379
|
else if (tally.locations.length < MAX_COLLECTED_MATCHES) {
|
|
285
|
-
tally.locations.push({
|
|
380
|
+
tally.locations.push({
|
|
381
|
+
file,
|
|
382
|
+
line: lineNumber,
|
|
383
|
+
column: byteColumnToCharacter(lineText, submatch.start ?? 0),
|
|
384
|
+
text: text_,
|
|
385
|
+
count: 1,
|
|
386
|
+
});
|
|
286
387
|
}
|
|
287
388
|
}
|
|
288
389
|
},
|
|
@@ -338,6 +439,7 @@ class JavaScriptEngine {
|
|
|
338
439
|
const concurrency = Math.min(16, Math.max(1, files.length));
|
|
339
440
|
let cursor = 0;
|
|
340
441
|
const perFile = new Map();
|
|
442
|
+
let unclassifiedFiles = 0;
|
|
341
443
|
const worker = async () => {
|
|
342
444
|
while (cursor < files.length) {
|
|
343
445
|
const file = files[cursor++];
|
|
@@ -348,10 +450,19 @@ class JavaScriptEngine {
|
|
|
348
450
|
if (!buffer || isProbablyBinary(buffer))
|
|
349
451
|
continue;
|
|
350
452
|
const content = buffer.toString('utf8');
|
|
453
|
+
let mask;
|
|
454
|
+
const getMask = first.options.ignoreComments
|
|
455
|
+
? () => (mask ??= createCommentMask(content, file.relativePath))
|
|
456
|
+
: undefined;
|
|
351
457
|
const scanned = new Map();
|
|
352
458
|
for (const [pattern, regexp] of regexps) {
|
|
353
|
-
scanned.set(pattern, scanContent(content, file.relativePath, regexp));
|
|
459
|
+
scanned.set(pattern, scanContent(content, file.relativePath, regexp, getMask));
|
|
354
460
|
}
|
|
461
|
+
// A mask exists only if some pattern matched, since that is the only
|
|
462
|
+
// thing that calls getMask - so reaching here already means this file
|
|
463
|
+
// matched and its language was not understood.
|
|
464
|
+
if (mask && !mask.classified)
|
|
465
|
+
unclassifiedFiles += 1;
|
|
355
466
|
perFile.set(file.relativePath, scanned);
|
|
356
467
|
}
|
|
357
468
|
};
|
|
@@ -364,6 +475,7 @@ class JavaScriptEngine {
|
|
|
364
475
|
for (const [pattern, tally] of scanned) {
|
|
365
476
|
const total = tallies.get(pattern);
|
|
366
477
|
total.count += tally.count;
|
|
478
|
+
total.commentCount += tally.commentCount;
|
|
367
479
|
for (const location of tally.locations) {
|
|
368
480
|
if (total.locations.length < MAX_COLLECTED_MATCHES)
|
|
369
481
|
total.locations.push(location);
|
|
@@ -372,7 +484,13 @@ class JavaScriptEngine {
|
|
|
372
484
|
}
|
|
373
485
|
return requests.map((request) => {
|
|
374
486
|
const tally = tallies.get(request.symbol) ?? emptyTally();
|
|
375
|
-
return {
|
|
487
|
+
return {
|
|
488
|
+
count: tally.count,
|
|
489
|
+
commentMatches: tally.commentCount,
|
|
490
|
+
unclassifiedFiles,
|
|
491
|
+
matches: tally.locations,
|
|
492
|
+
engine: this.name,
|
|
493
|
+
};
|
|
376
494
|
});
|
|
377
495
|
}
|
|
378
496
|
/** Every file a request should look at, sorted by relative path. */
|
|
@@ -390,6 +508,8 @@ class JavaScriptEngine {
|
|
|
390
508
|
*/
|
|
391
509
|
export async function enumerateCandidates(request, budget) {
|
|
392
510
|
const matcher = createGlobMatcher(request.options.globs);
|
|
511
|
+
const excluded = createExcludeMatcher(request.options.excludeGlobs);
|
|
512
|
+
const admits = (relativePath) => matcher(relativePath) && !excluded(relativePath);
|
|
393
513
|
const found = new Map();
|
|
394
514
|
let bytes = 0;
|
|
395
515
|
let exceeded = false;
|
|
@@ -409,7 +529,7 @@ export async function enumerateCandidates(request, budget) {
|
|
|
409
529
|
continue;
|
|
410
530
|
if (stats.isFile()) {
|
|
411
531
|
const relativePath = toPosix(path.relative(request.root, absoluteTarget));
|
|
412
|
-
if (stats.size <= MAX_FILE_SIZE &&
|
|
532
|
+
if (stats.size <= MAX_FILE_SIZE && admits(relativePath)) {
|
|
413
533
|
if (!admit(absoluteTarget, relativePath, stats.size))
|
|
414
534
|
break outer;
|
|
415
535
|
}
|
|
@@ -419,7 +539,7 @@ export async function enumerateCandidates(request, budget) {
|
|
|
419
539
|
if (file.size > MAX_FILE_SIZE)
|
|
420
540
|
continue;
|
|
421
541
|
const relativePath = toPosix(path.relative(request.root, file.absolutePath));
|
|
422
|
-
if (!
|
|
542
|
+
if (!admits(relativePath))
|
|
423
543
|
continue;
|
|
424
544
|
if (!admit(file.absolutePath, relativePath, file.size))
|
|
425
545
|
break outer;
|
|
@@ -431,9 +551,17 @@ export async function enumerateCandidates(request, budget) {
|
|
|
431
551
|
return { files, exceeded, bytes };
|
|
432
552
|
}
|
|
433
553
|
/** Counts matches in one file and records per-line snippets. */
|
|
434
|
-
export function scanContent(content, relativePath, regexp
|
|
554
|
+
export function scanContent(content, relativePath, regexp,
|
|
555
|
+
/**
|
|
556
|
+
* Resolved lazily, and only once the file has produced a match. Classifying
|
|
557
|
+
* every file in scope would mean a comment scan of the whole tree; this way
|
|
558
|
+
* the cost is proportional to matches, which for an architecture assertion is
|
|
559
|
+
* usually zero.
|
|
560
|
+
*/
|
|
561
|
+
getMask) {
|
|
435
562
|
regexp.lastIndex = 0;
|
|
436
563
|
let count = 0;
|
|
564
|
+
let commentCount = 0;
|
|
437
565
|
const byLine = new Map();
|
|
438
566
|
// Lazily built line index: only paid for when the file actually matches.
|
|
439
567
|
let lineStarts = null;
|
|
@@ -458,6 +586,12 @@ export function scanContent(content, relativePath, regexp) {
|
|
|
458
586
|
};
|
|
459
587
|
let match;
|
|
460
588
|
while ((match = regexp.exec(content)) !== null) {
|
|
589
|
+
if (getMask?.().isComment(match.index)) {
|
|
590
|
+
commentCount += 1;
|
|
591
|
+
if (match[0].length === 0)
|
|
592
|
+
regexp.lastIndex += 1;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
461
595
|
count += 1;
|
|
462
596
|
const { line, column } = lineOf(match.index);
|
|
463
597
|
const existing = byLine.get(line);
|
|
@@ -478,7 +612,11 @@ export function scanContent(content, relativePath, regexp) {
|
|
|
478
612
|
if (match[0].length === 0)
|
|
479
613
|
regexp.lastIndex += 1;
|
|
480
614
|
}
|
|
481
|
-
|
|
615
|
+
// No sort: exec scans forward, so lines are discovered in increasing order and
|
|
616
|
+
// a Map preserves insertion order. Sorting here was dead code - mutation
|
|
617
|
+
// testing found it by reporting that neither reversing nor removing the
|
|
618
|
+
// comparator changed any result.
|
|
619
|
+
return { count, locations: [...byLine.values()], commentCount };
|
|
482
620
|
}
|
|
483
621
|
export const javascriptEngine = new JavaScriptEngine();
|
|
484
622
|
/**
|
|
@@ -500,7 +638,9 @@ export const javascriptEngine = new JavaScriptEngine();
|
|
|
500
638
|
export const SMALL_TREE_BUDGET = process.platform === 'win32' ? { maxFiles: 512, maxBytes: 1024 * 1024 } : { maxFiles: 32, maxBytes: 64 * 1024 };
|
|
501
639
|
/** Spawn failures that mean "this binary is not installed", not "search failed". */
|
|
502
640
|
const MISSING_BINARY_CODES = new Set(['ENOENT', 'EACCES', 'EPERM', 'EINVAL', 'UNKNOWN']);
|
|
503
|
-
|
|
641
|
+
/** Exported so the code list can be asserted; EACCES and friends are not
|
|
642
|
+
* reproducible on demand from a real spawn. */
|
|
643
|
+
export function isMissingBinary(error) {
|
|
504
644
|
const code = error?.code;
|
|
505
645
|
return typeof code === 'string' && MISSING_BINARY_CODES.has(code);
|
|
506
646
|
}
|
|
@@ -528,6 +668,7 @@ function enumerationKey(request) {
|
|
|
528
668
|
request.root,
|
|
529
669
|
request.targets,
|
|
530
670
|
request.options.globs,
|
|
671
|
+
request.options.excludeGlobs,
|
|
531
672
|
excludeSetId(request.options.excludeFiles),
|
|
532
673
|
]);
|
|
533
674
|
}
|
|
@@ -614,6 +755,7 @@ export function createCachedEngine(engine) {
|
|
|
614
755
|
request.options.word,
|
|
615
756
|
request.options.ignoreCase,
|
|
616
757
|
request.options.globs,
|
|
758
|
+
request.options.excludeGlobs,
|
|
617
759
|
[...request.options.excludeFiles].sort(),
|
|
618
760
|
]);
|
|
619
761
|
const recordFallback = (error) => {
|