@descent-vtt/spec-guard 0.7.0 → 0.9.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 (76) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +224 -27
  3. package/dist/cli.d.ts +37 -2
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +275 -59
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +66 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +165 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/engine.d.ts +22 -8
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +118 -67
  14. package/dist/engine.js.map +1 -1
  15. package/dist/facts.d.ts +106 -0
  16. package/dist/facts.d.ts.map +1 -0
  17. package/dist/facts.js +188 -0
  18. package/dist/facts.js.map +1 -0
  19. package/dist/glob.d.ts +17 -21
  20. package/dist/glob.d.ts.map +1 -1
  21. package/dist/glob.js +13 -24
  22. package/dist/glob.js.map +1 -1
  23. package/dist/imports.d.ts +6 -1
  24. package/dist/imports.d.ts.map +1 -1
  25. package/dist/imports.js +9 -5
  26. package/dist/imports.js.map +1 -1
  27. package/dist/index.d.ts +20 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +11 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/io.d.ts +51 -0
  32. package/dist/io.d.ts.map +1 -0
  33. package/dist/io.js +44 -0
  34. package/dist/io.js.map +1 -0
  35. package/dist/mcp.d.ts +11 -0
  36. package/dist/mcp.d.ts.map +1 -1
  37. package/dist/mcp.js +23 -18
  38. package/dist/mcp.js.map +1 -1
  39. package/dist/memo.d.ts +53 -0
  40. package/dist/memo.d.ts.map +1 -0
  41. package/dist/memo.js +67 -0
  42. package/dist/memo.js.map +1 -0
  43. package/dist/parser.d.ts.map +1 -1
  44. package/dist/parser.js +17 -0
  45. package/dist/parser.js.map +1 -1
  46. package/dist/query.d.ts +5 -2
  47. package/dist/query.d.ts.map +1 -1
  48. package/dist/query.js +12 -3
  49. package/dist/query.js.map +1 -1
  50. package/dist/reporter.d.ts +10 -0
  51. package/dist/reporter.d.ts.map +1 -1
  52. package/dist/reporter.js +33 -4
  53. package/dist/reporter.js.map +1 -1
  54. package/dist/rules.d.ts +12 -1
  55. package/dist/rules.d.ts.map +1 -1
  56. package/dist/rules.js +45 -1
  57. package/dist/rules.js.map +1 -1
  58. package/dist/runner.d.ts +42 -2
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +261 -76
  61. package/dist/runner.js.map +1 -1
  62. package/dist/specs.d.ts +3 -1
  63. package/dist/specs.d.ts.map +1 -1
  64. package/dist/specs.js +6 -5
  65. package/dist/specs.js.map +1 -1
  66. package/dist/structure.d.ts +104 -0
  67. package/dist/structure.d.ts.map +1 -0
  68. package/dist/structure.js +254 -0
  69. package/dist/structure.js.map +1 -0
  70. package/dist/types.d.ts +50 -5
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/watch.d.ts +125 -0
  73. package/dist/watch.d.ts.map +1 -0
  74. package/dist/watch.js +321 -0
  75. package/dist/watch.js.map +1 -0
  76. package/package.json +8 -2
package/dist/config.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The options a project keeps in its `package.json`, under `"specGuard"`.
3
+ *
4
+ * Validated from a value, never read from a file: the command line reads the
5
+ * file, and every malformed option can then be tested on a plain object. The
6
+ * keys are the options that are a project's policy - which specs, and how
7
+ * strictly to hold them - and nothing that describes one invocation. See
8
+ * ADR-0014.
9
+ */
10
+ /** The name the options sit under in `package.json`. */
11
+ export const CONFIG_KEY = 'specGuard';
12
+ /** Every key, in the order the documentation lists them. */
13
+ export const CONFIG_KEYS = [
14
+ 'specs',
15
+ 'engine',
16
+ 'strict',
17
+ 'allowMissingTargets',
18
+ 'allowEmptyScope',
19
+ 'ignoreStatus',
20
+ 'includeSpecs',
21
+ 'defaultSkips',
22
+ 'maxSnippets',
23
+ 'concurrency',
24
+ ];
25
+ /**
26
+ * Options that exist on the command line and are refused here by name.
27
+ *
28
+ * Each describes one invocation rather than a project. `"format": "sarif"`
29
+ * would make every developer's local run print SARIF, and `root` is where the
30
+ * configuration was found in the first place.
31
+ */
32
+ export const INVOCATION_OPTIONS = new Set([
33
+ 'root',
34
+ 'format',
35
+ 'json',
36
+ 'verbose',
37
+ 'color',
38
+ 'failFast',
39
+ 'printBaseline',
40
+ 'allowEmpty',
41
+ 'watch',
42
+ ]);
43
+ /** A configuration that cannot be used, with the words to say why. */
44
+ export class ConfigError extends Error {
45
+ }
46
+ const ENGINE_ALIASES = {
47
+ auto: 'auto',
48
+ rg: 'ripgrep',
49
+ ripgrep: 'ripgrep',
50
+ js: 'javascript',
51
+ javascript: 'javascript',
52
+ node: 'javascript',
53
+ };
54
+ /**
55
+ * The engine a name asks for, whether the name came from a flag or a file.
56
+ *
57
+ * One function, so `--engine rgg` and `"engine": "rgg"` fail with the same
58
+ * words. Case is ignored, as the flag always has.
59
+ */
60
+ export function engineNamed(name) {
61
+ const engine = ENGINE_ALIASES[name.toLowerCase()];
62
+ if (engine === undefined)
63
+ throw new ConfigError(`Unknown engine "${name}". Expected auto, rg or js.`);
64
+ return engine;
65
+ }
66
+ /** A value as a message names it: `"yes"`, `3`, `null`, an array, an object. */
67
+ function describe(value) {
68
+ if (Array.isArray(value))
69
+ return 'an array';
70
+ if (value !== null && typeof value === 'object')
71
+ return 'an object';
72
+ return JSON.stringify(value);
73
+ }
74
+ function isObject(value) {
75
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
76
+ }
77
+ /**
78
+ * Checks one key's value, or says what is wrong with it.
79
+ *
80
+ * A string is not a boolean and a number in a string is not a number. JSON has
81
+ * both types, and accepting `"true"` would be a second grammar for one file.
82
+ */
83
+ function checkValue(key, value) {
84
+ switch (key) {
85
+ case 'specs':
86
+ return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === 'string' && entry.trim() !== '')
87
+ ? null
88
+ : `must be a non-empty list of spec globs, got ${describe(value)}`;
89
+ case 'engine':
90
+ return typeof value === 'string' ? null : `must be a string, got ${describe(value)}`;
91
+ case 'maxSnippets':
92
+ return Number.isInteger(value) && value >= 0 ? null : `must be an integer, 0 or more, got ${describe(value)}`;
93
+ case 'concurrency':
94
+ return Number.isInteger(value) && value >= 1 ? null : `must be an integer, 1 or more, got ${describe(value)}`;
95
+ default:
96
+ return typeof value === 'boolean' ? null : `must be true or false, got ${describe(value)}`;
97
+ }
98
+ }
99
+ /**
100
+ * The configuration in a root's `package.json`, read by whoever is asking.
101
+ *
102
+ * The reader is a parameter because this module reads nothing itself: the
103
+ * command line passes the filesystem, and a watch session passes the door that
104
+ * records what it read, so an edit to `package.json` reaches the session like
105
+ * an edit to any other file. A file that is not there - or a root that is not a
106
+ * directory - is no configuration. A file that is there and cannot be read is
107
+ * an error, because nobody can tell what it would have said.
108
+ */
109
+ export async function loadConfig(root, read) {
110
+ // Joined by hand rather than with node:path, which would be this module's
111
+ // only import: a root with either separator at its end is still one root.
112
+ const file = `${root.replace(/[\\/]+$/, '')}/package.json`;
113
+ let text;
114
+ try {
115
+ text = await read(file);
116
+ }
117
+ catch (error) {
118
+ const code = error.code;
119
+ if (code === 'ENOENT' || code === 'ENOTDIR')
120
+ return {};
121
+ throw new ConfigError(`package.json could not be read (${error.message}), so its "${CONFIG_KEY}" options cannot be read.`);
122
+ }
123
+ return parseConfig(text);
124
+ }
125
+ /**
126
+ * The configuration in a `package.json`'s text, or none.
127
+ *
128
+ * A `package.json` with no `"specGuard"` is no configuration rather than a
129
+ * mistake. Anything else that cannot be used throws a `ConfigError` naming the
130
+ * file and the key, so a run never starts under options nobody meant.
131
+ */
132
+ export function parseConfig(text, file = 'package.json') {
133
+ let manifest;
134
+ try {
135
+ manifest = JSON.parse(text);
136
+ }
137
+ catch (error) {
138
+ throw new ConfigError(`${file} is not valid JSON (${error.message}), so its "${CONFIG_KEY}" options cannot be read.`);
139
+ }
140
+ const options = isObject(manifest) ? manifest[CONFIG_KEY] : undefined;
141
+ if (options === undefined)
142
+ return {};
143
+ if (!isObject(options))
144
+ throw new ConfigError(`${file}: "${CONFIG_KEY}" must be an object, got ${describe(options)}.`);
145
+ const config = {};
146
+ for (const [key, value] of Object.entries(options)) {
147
+ if (INVOCATION_OPTIONS.has(key)) {
148
+ throw new ConfigError(`${file}: "${CONFIG_KEY}.${key}" is chosen on the command line, not in ${file}.`);
149
+ }
150
+ if (!CONFIG_KEYS.includes(key)) {
151
+ throw new ConfigError(`${file}: unknown option "${key}" in "${CONFIG_KEY}". Options are ${CONFIG_KEYS.join(', ')}.`);
152
+ }
153
+ const issue = checkValue(key, value);
154
+ if (issue !== null)
155
+ throw new ConfigError(`${file}: "${CONFIG_KEY}.${key}" ${issue}.`);
156
+ try {
157
+ config[key] = key === 'engine' ? engineNamed(value) : value;
158
+ }
159
+ catch (error) {
160
+ throw new ConfigError(`${file}: "${CONFIG_KEY}.${key}": ${error.message}`);
161
+ }
162
+ }
163
+ return config;
164
+ }
165
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/engine.d.ts CHANGED
@@ -19,7 +19,8 @@
19
19
  * flags are answered by a single pass.
20
20
  */
21
21
  import { type CommentMask } from './comments.js';
22
- import { type DirectoryReader } from './glob.js';
22
+ import { type Io } from './io.js';
23
+ import { type Memo } from './memo.js';
23
24
  import { type SkippedPath } from './scope.js';
24
25
  import type { EngineName, MatchLocation, SearchOptions, SearchResult } from './types.js';
25
26
  /** Files larger than this are skipped by both engines, keeping them in sync. */
@@ -222,13 +223,13 @@ export declare const ANY_FILE_PROBE: EnumerationBudget;
222
223
  */
223
224
  export declare function enumerateCandidates(request: WalkRequest, budget?: EnumerationBudget,
224
225
  /**
225
- * Directory reader, forwarded to the walk. Exists for the same reason
226
- * `walkFiles` takes one: the interesting behaviour here is what the
227
- * enumeration does with a directory it *cannot* read, and there is no
228
- * portable way to create one - Windows has no chmod, and a permission bit set
229
- * by a test is a permission bit a failed test leaves behind.
226
+ * The door the enumeration reads through, forwarded to the walk. Injectable
227
+ * for the same reason `walkFiles` takes one: the interesting behaviour here
228
+ * is what the enumeration does with a directory it *cannot* read, and there
229
+ * is no portable way to create one - Windows has no chmod, and a permission
230
+ * bit set by a test is a permission bit a failed test leaves behind.
230
231
  */
231
- readDirectory?: DirectoryReader): Promise<Enumeration>;
232
+ io?: Io): Promise<Enumeration>;
232
233
  /** Counts matches in one file and records per-line snippets. */
233
234
  export declare function scanContent(content: string, relativePath: string, regexp: RegExp,
234
235
  /**
@@ -243,6 +244,15 @@ export type BatchEngine = Engine & Required<Pick<Engine, 'searchBatch'>> & {
243
244
  /** Scans a file list that the caller already enumerated. */
244
245
  searchFiles(files: readonly CandidateFile[], requests: SearchRequest[], unreadable?: readonly string[], skipped?: readonly SkippedPath[]): Promise<SearchResult[]>;
245
246
  };
247
+ /**
248
+ * A scanner that reads through the given door, and remembers its scans in the
249
+ * given memo.
250
+ *
251
+ * One per watch session rule, so the session sees what each rule read
252
+ * (ADR-0014). A plain run shares the one below, which reads the filesystem and
253
+ * remembers nothing.
254
+ */
255
+ export declare function createJavaScriptEngine(io?: Io, memo?: Memo): BatchEngine;
246
256
  export declare const javascriptEngine: BatchEngine;
247
257
  /**
248
258
  * How much scanning the JavaScript engine may do before ripgrep is worth a
@@ -283,7 +293,11 @@ export type CachedEngine = Engine & Required<Pick<Engine, 'searchBatch'>> & {
283
293
  /**
284
294
  * Wraps an engine with a de-duplicating cache plus an automatic fallback to the
285
295
  * JS engine, so a ripgrep hiccup degrades to "slower" instead of "broken".
296
+ *
297
+ * The fallback is a parameter because a watch session's scanner reads through a
298
+ * door of its own, and falling back to the shared one would read past it.
299
+ * Nothing falls back from the fallback: its failure is the run's failure.
286
300
  */
287
- export declare function createCachedEngine(engine: Engine): CachedEngine;
301
+ export declare function createCachedEngine(engine: Engine, fallback?: BatchEngine): CachedEngine;
288
302
  export {};
289
303
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAEpE,OAAO,EAAU,KAAK,EAAE,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAW,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAA8C,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE1F,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEzF,gFAAgF;AAChF,eAAO,MAAM,aAAa,QAAmB,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAKzC,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAU,CAAC;AAOrD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACtD;;;OAGG;IACH,WAAW,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAClE;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AAEjE,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG9C;AAGD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAEzE;AAED,oDAAoD;AACpD,UAAU,KAAK;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,0DAA0D;IAC1D,YAAY,EAAE,MAAM,CAAC;CACtB;AAgDD;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAGtD;AAmBD,mEAAmE;AACnE,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAUpG;AAOD,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAcpD;AAGD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,QAAQ,GAAE,SAAS,MAAM,EAAqB,GAAG,MAAM,EAAE,CA6BjH;AAuFD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,aAAa,EAAE,CAYjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAW3D;AAID,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAS5E;AAGD,UAAU,aAAa;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AA0LD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,uEAAuE;IACvE,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,EAAE,iBAAgD,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,WAAW,EACpB,MAAM,CAAC,EAAE,iBAAiB;AAC1B;;;;;;GAMG;AACH,EAAE,GAAE,EAAW,GACd,OAAO,CAAC,WAAW,CAAC,CAoEtB;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM;AACd;;;;;GAKG;AACH,OAAO,CAAC,EAAE,MAAM,WAAW,GAC1B,KAAK,CA8CP;AAID,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,MAAM,GAC9B,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,GAAG;IACtC,4DAA4D;IAC5D,WAAW,CACT,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,QAAQ,EAAE,aAAa,EAAE,EACzB,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,EAC9B,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,GAC/B,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAC5B,CAAC;AAEJ;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,GAAE,EAAW,EAAE,IAAI,GAAE,IAAc,GAAG,WAAW,CAEzF;AAED,eAAO,MAAM,gBAAgB,EAAE,WAAsC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,iBAAiB,CAE5E;AAED,eAAO,MAAM,iBAAiB,EAAE,iBAAqD,CAAC;AAkBtF;+CAC+C;AAC/C,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEvD;AAoED,oEAAoE;AACpE,wBAAsB,aAAa,CAAC,UAAU,GAAE,gBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,CAW1F;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEpG;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,WAA8B,GAAG,YAAY,CAwDzG"}
package/dist/engine.js CHANGED
@@ -19,10 +19,11 @@
19
19
  * flags are answered by a single pass.
20
20
  */
21
21
  import { spawn } from 'node:child_process';
22
- import { promises as fs } from 'node:fs';
23
22
  import path from 'node:path';
24
23
  import { createCommentMask } from './comments.js';
25
- import { createExcludeMatcher, createGlobMatcher, statOrNull, toPosix, walkFiles, } from './glob.js';
24
+ import { createExcludeMatcher, createGlobMatcher, toPosix, walkFiles } from './glob.js';
25
+ import { nodeIo } from './io.js';
26
+ import { NO_MEMO } from './memo.js';
26
27
  import { isBinary, LedgerBuilder, UNCERTAIN_REASONS } from './scope.js';
27
28
  import { lineStarts, locate } from './text.js';
28
29
  /** Files larger than this are skipped by both engines, keeping them in sync. */
@@ -409,8 +410,33 @@ export function buildJsRegExp(symbol, options) {
409
410
  const source = options.word ? `(?<![\\p{L}\\p{N}_])(?:${escaped})(?![\\p{L}\\p{N}_])` : escaped;
410
411
  return new RegExp(source, `${flags}u`);
411
412
  }
413
+ /**
414
+ * Scans one file's bytes for every pattern of a pass.
415
+ *
416
+ * A pure function of the bytes, the path (which decides the comment syntax),
417
+ * the patterns and how they match, which is what lets a watch session remember
418
+ * it (ADR-0014).
419
+ */
420
+ function scanFile(buffer, relativePath, regexps, ignoreComments) {
421
+ const content = buffer.toString('utf8');
422
+ let mask;
423
+ const getMask = ignoreComments ? () => (mask ??= createCommentMask(content, relativePath)) : undefined;
424
+ const tallies = new Map();
425
+ for (const [pattern, regexp] of regexps) {
426
+ tallies.set(pattern, scanContent(content, relativePath, regexp, getMask));
427
+ }
428
+ // Read after the loop, where every pattern has had its chance to ask.
429
+ const built = mask;
430
+ return { tallies, binary: isBinary(buffer), classified: built?.classified };
431
+ }
412
432
  class JavaScriptEngine {
433
+ io;
434
+ memo;
413
435
  name = 'javascript';
436
+ constructor(io, memo) {
437
+ this.io = io;
438
+ this.memo = memo;
439
+ }
414
440
  async search(request) {
415
441
  const [result] = await this.searchBatch([request]);
416
442
  return result;
@@ -424,7 +450,7 @@ class JavaScriptEngine {
424
450
  const [first] = requests;
425
451
  if (!first)
426
452
  return [];
427
- const enumeration = await enumerateCandidates(first);
453
+ const enumeration = await enumerateCandidates(first, undefined, this.io);
428
454
  return this.searchFiles(enumeration.files, requests, [], enumeration.skipped);
429
455
  }
430
456
  /**
@@ -438,48 +464,68 @@ class JavaScriptEngine {
438
464
  const [first] = requests;
439
465
  if (!first)
440
466
  return [];
441
- const ledger = new LedgerBuilder();
442
- for (const entry of skipped)
443
- ledger.add(entry.path, entry.reason, entry.matches);
444
- for (const entry of unreadable)
445
- ledger.add(entry, 'unreadable');
446
467
  const patterns = [...new Set(requests.map((request) => request.symbol))];
447
468
  const regexps = new Map(patterns.map((pattern) => [pattern, buildJsRegExp(pattern, first.options)]));
448
469
  const tallies = new Map(patterns.map((pattern) => [pattern, emptyTally()]));
470
+ // One ledger and one count of unclassified files per pattern, not one for
471
+ // the pass. A pass answers every rule over the same scope at once, and a
472
+ // binary file holding one rule's symbol is not a gap in another rule's
473
+ // answer - yet a shared ledger reported it against every rule in the pass,
474
+ // with all their matches added up, and --strict failed rules over a file
475
+ // that did not hold their symbol. Found by holding ADR-0014's watch
476
+ // session, which runs each rule alone, to a plain run.
477
+ const ledgers = new Map(patterns.map((pattern) => [pattern, new LedgerBuilder()]));
478
+ const unclassified = new Map(patterns.map((pattern) => [pattern, 0]));
479
+ for (const ledger of ledgers.values()) {
480
+ // What every rule shares: what the walk could not list, and what ripgrep
481
+ // could not read.
482
+ for (const entry of skipped)
483
+ ledger.add(entry.path, entry.reason, entry.matches);
484
+ for (const entry of unreadable)
485
+ ledger.add(entry, 'unreadable');
486
+ }
487
+ // Everything a file's scan depends on besides its bytes and its path, which
488
+ // decides its comment syntax. See memo.ts for why a missing input here is
489
+ // the one defect a memo can have.
490
+ const { regex, word, ignoreCase, ignoreComments } = first.options;
491
+ const scanInputs = [JSON.stringify([regex, word, ignoreCase, ignoreComments]), JSON.stringify(patterns)];
449
492
  // No floor of 1: an empty file list starts no readers, and a loop that
450
493
  // never runs produces the same empty tallies as one that runs once and
451
494
  // finds nothing. The guard was defending against an outcome it shared.
452
495
  const concurrency = readConcurrency(files.length);
453
496
  let cursor = 0;
454
- const perFile = new Map();
455
- // Per pattern, how many matches each file holds. Only files that matched
456
- // appear, so this is smaller than perFile, which already holds an entry for
457
- // every file scanned - there is no new memory shape here.
458
- const byFile = new Map(patterns.map((pattern) => [pattern, new Map()]));
459
- let unclassifiedFiles = 0;
497
+ // What each reader found, by file. Entered in the ledgers and the tallies
498
+ // in walk order below, rather than by each reader as its read ends: that
499
+ // was the order reads happened to finish in, which differed between two
500
+ // runs of one tree - and past a ledger's cap, so did which paths it named.
501
+ const scans = new Map();
460
502
  const worker = async () => {
461
503
  while (cursor < files.length) {
462
504
  const file = files[cursor++];
463
505
  /* c8 ignore next -- cursor is bounded by files.length */
464
506
  if (!file)
465
507
  return;
466
- const buffer = await fs.readFile(file.absolutePath).catch(() => null);
467
- if (!buffer) {
468
- // A file we cannot open might hold anything, so it is recorded rather
469
- // than passed over as though it had been read and found clean.
508
+ const buffer = await this.io.readFile(file.absolutePath).catch(() => null);
509
+ // A file we cannot open might hold anything, so it is recorded rather
510
+ // than passed over as though it had been read and found clean.
511
+ scans.set(file.relativePath, buffer === null
512
+ ? null
513
+ : this.memo.remember(buffer, [...scanInputs, file.relativePath], () => scanFile(buffer, file.relativePath, regexps, ignoreComments)));
514
+ }
515
+ };
516
+ await Promise.all(Array.from({ length: concurrency }, worker));
517
+ // Merge in walk order so snippets come out sorted by path, like ripgrep's,
518
+ // and so do the ledgers.
519
+ const byFile = new Map(patterns.map((pattern) => [pattern, new Map()]));
520
+ for (const file of files) {
521
+ const scan = scans.get(file.relativePath);
522
+ if (scan === null) {
523
+ for (const ledger of ledgers.values())
470
524
  ledger.add(file.relativePath, 'unreadable');
471
- continue;
472
- }
473
- const content = buffer.toString('utf8');
474
- let mask;
475
- const getMask = first.options.ignoreComments
476
- ? () => (mask ??= createCommentMask(content, file.relativePath))
477
- : undefined;
478
- const scanned = new Map();
479
- for (const [pattern, regexp] of regexps) {
480
- scanned.set(pattern, scanContent(content, file.relativePath, regexp, getMask));
481
- }
482
- if (isBinary(buffer)) {
525
+ continue;
526
+ }
527
+ for (const [pattern, tally] of scan.tallies) {
528
+ if (scan.binary) {
483
529
  // Searched, but not counted. Skipping binary files silently was a way
484
530
  // to pass an assertion by never looking; searching them and saying
485
531
  // what was found leaves the decision with the reader.
@@ -489,26 +535,17 @@ class JavaScriptEngine {
489
535
  // found clean - and leaving it out is also what keeps the two engines
490
536
  // reporting the same thing, since ripgrep only ever hands the scanner
491
537
  // files that matched.
492
- const found = [...scanned.values()].reduce((total, tally) => total + tally.count, 0);
493
- if (found > 0)
494
- ledger.add(file.relativePath, 'binary', found);
538
+ if (tally.count > 0)
539
+ ledgers.get(pattern).add(file.relativePath, 'binary', tally.count);
495
540
  continue;
496
541
  }
497
- // A mask exists only if some pattern matched, since that is the only
498
- // thing that calls getMask - so reaching here already means this file
499
- // matched and its language was not understood.
500
- if (mask && !mask.classified)
501
- unclassifiedFiles += 1;
502
- perFile.set(file.relativePath, scanned);
503
- }
504
- };
505
- await Promise.all(Array.from({ length: concurrency }, worker));
506
- // Merge in walk order so snippets come out sorted by path, like ripgrep's.
507
- for (const file of files) {
508
- const scanned = perFile.get(file.relativePath);
509
- if (!scanned)
510
- continue;
511
- for (const [pattern, tally] of scanned) {
542
+ // A mask is built only once some pattern has matched, and a file this
543
+ // pattern did not match is not one whose comments it counted as code.
544
+ // No comment count here: a file whose comments cannot be told from its
545
+ // code has none, so every match it holds is in the count.
546
+ if (scan.classified === false && tally.count > 0) {
547
+ unclassified.set(pattern, unclassified.get(pattern) + 1);
548
+ }
512
549
  const total = tallies.get(pattern);
513
550
  total.count += tally.count;
514
551
  total.commentCount += tally.commentCount;
@@ -520,16 +557,15 @@ class JavaScriptEngine {
520
557
  }
521
558
  }
522
559
  }
523
- const scope = ledger.build();
524
560
  return requests.map((request) => {
525
- const tally = tallies.get(request.symbol) ?? emptyTally();
561
+ const tally = tallies.get(request.symbol);
526
562
  return {
527
563
  count: tally.count,
528
564
  commentMatches: tally.commentCount,
529
- unclassifiedFiles,
565
+ unclassifiedFiles: unclassified.get(request.symbol),
530
566
  matches: tally.locations,
531
- fileCounts: byFile.get(request.symbol) ?? new Map(),
532
- scope,
567
+ fileCounts: byFile.get(request.symbol),
568
+ scope: ledgers.get(request.symbol).build(),
533
569
  engine: this.name,
534
570
  };
535
571
  });
@@ -554,13 +590,13 @@ export const ANY_FILE_PROBE = { maxFiles: 0, maxBytes: 0 };
554
590
  */
555
591
  export async function enumerateCandidates(request, budget,
556
592
  /**
557
- * Directory reader, forwarded to the walk. Exists for the same reason
558
- * `walkFiles` takes one: the interesting behaviour here is what the
559
- * enumeration does with a directory it *cannot* read, and there is no
560
- * portable way to create one - Windows has no chmod, and a permission bit set
561
- * by a test is a permission bit a failed test leaves behind.
593
+ * The door the enumeration reads through, forwarded to the walk. Injectable
594
+ * for the same reason `walkFiles` takes one: the interesting behaviour here
595
+ * is what the enumeration does with a directory it *cannot* read, and there
596
+ * is no portable way to create one - Windows has no chmod, and a permission
597
+ * bit set by a test is a permission bit a failed test leaves behind.
562
598
  */
563
- readDirectory) {
599
+ io = nodeIo) {
564
600
  const matcher = createGlobMatcher(request.options.globs);
565
601
  const excluded = createExcludeMatcher(request.options.excludeGlobs);
566
602
  // excludeFiles is applied here rather than after the walk so that a budgeted
@@ -597,7 +633,7 @@ readDirectory) {
597
633
  };
598
634
  outer: for (const target of targetsOf(request)) {
599
635
  const absoluteTarget = path.resolve(request.root, target);
600
- const stats = await statOrNull(absoluteTarget);
636
+ const stats = await io.stat(absoluteTarget);
601
637
  if (!stats)
602
638
  continue;
603
639
  if (stats.isFile()) {
@@ -612,7 +648,7 @@ readDirectory) {
612
648
  const walkOptions = {
613
649
  scope: request.options.scope,
614
650
  onSkip: (relativePath, reason) => note(prefix ? `${prefix}/${relativePath}` : relativePath, reason),
615
- ...(readDirectory ? { readDirectory } : {}),
651
+ io,
616
652
  };
617
653
  for await (const file of walkFiles(absoluteTarget, walkOptions)) {
618
654
  if (!withinSizeLimit(file.size))
@@ -681,7 +717,18 @@ getMask) {
681
717
  // comparator changed any result.
682
718
  return { count, locations: [...byLine.values()], commentCount };
683
719
  }
684
- export const javascriptEngine = new JavaScriptEngine();
720
+ /**
721
+ * A scanner that reads through the given door, and remembers its scans in the
722
+ * given memo.
723
+ *
724
+ * One per watch session rule, so the session sees what each rule read
725
+ * (ADR-0014). A plain run shares the one below, which reads the filesystem and
726
+ * remembers nothing.
727
+ */
728
+ export function createJavaScriptEngine(io = nodeIo, memo = NO_MEMO) {
729
+ return new JavaScriptEngine(io, memo);
730
+ }
731
+ export const javascriptEngine = createJavaScriptEngine();
685
732
  /**
686
733
  * How much scanning the JavaScript engine may do before ripgrep is worth a
687
734
  * process spawn.
@@ -806,8 +853,12 @@ export async function resolveEngine(preference = 'auto') {
806
853
  /**
807
854
  * Wraps an engine with a de-duplicating cache plus an automatic fallback to the
808
855
  * JS engine, so a ripgrep hiccup degrades to "slower" instead of "broken".
856
+ *
857
+ * The fallback is a parameter because a watch session's scanner reads through a
858
+ * door of its own, and falling back to the shared one would read past it.
859
+ * Nothing falls back from the fallback: its failure is the run's failure.
809
860
  */
810
- export function createCachedEngine(engine) {
861
+ export function createCachedEngine(engine, fallback = javascriptEngine) {
811
862
  const cache = new Map();
812
863
  const fallbacks = [];
813
864
  const keyOf = searchKey;
@@ -826,10 +877,10 @@ export function createCachedEngine(engine) {
826
877
  let result = cache.get(key);
827
878
  if (!result) {
828
879
  result = engine.search(request).catch(async (error) => {
829
- if (engine === javascriptEngine)
880
+ if (engine === fallback)
830
881
  throw error;
831
882
  recordFallback(error);
832
- return javascriptEngine.search(request);
883
+ return fallback.search(request);
833
884
  });
834
885
  cache.set(key, result);
835
886
  }
@@ -844,10 +895,10 @@ export function createCachedEngine(engine) {
844
895
  // does nothing. The guard could not change an outcome, which is why
845
896
  // nothing could be written to hold it in place.
846
897
  const pending = runSearches(engine, uncached).catch(async (error) => {
847
- if (engine === javascriptEngine)
898
+ if (engine === fallback)
848
899
  throw error;
849
900
  recordFallback(error);
850
- return runSearches(javascriptEngine, uncached);
901
+ return runSearches(fallback, uncached);
851
902
  });
852
903
  uncached.forEach((request, index) => {
853
904
  cache.set(keyOf(request), pending.then((results) => results[index]));