@cplieger/deadset-ts 8.0.0-dev.15 → 8.0.0-dev.16

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 (134) hide show
  1. package/dist/src/analysis.js +41 -37
  2. package/dist/src/commands/read.js +6 -7
  3. package/dist/src/conformance-record.js +1 -1
  4. package/dist/src/consumers.js +3 -1
  5. package/dist/src/contract/config.generated.js +1 -1
  6. package/dist/src/contract/contract.generated.js +119 -2
  7. package/dist/src/dependencies.js +79 -48
  8. package/dist/src/entries/build-scripts.js +222 -0
  9. package/dist/src/{configuration-files.js → entries/configuration-files.js} +20 -58
  10. package/dist/src/{convention-rows.js → entries/convention-rows.js} +120 -27
  11. package/dist/src/entries/conventions.js +739 -0
  12. package/dist/src/{html-entries.js → entries/html-entries.js} +3 -3
  13. package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +2 -2
  14. package/dist/src/entries/run-directories.js +77 -0
  15. package/dist/src/entries/sources.js +144 -0
  16. package/dist/src/entries/static-module.js +669 -0
  17. package/dist/src/entries/test-runners.js +684 -0
  18. package/dist/src/{workflow-steps.js → entries/workflow-steps.js} +18 -8
  19. package/dist/src/extensions.js +37 -0
  20. package/dist/src/facts.js +38 -0
  21. package/dist/src/findings/emitters.js +5 -8
  22. package/dist/src/findings/self-check.js +36 -20
  23. package/dist/src/findings-pass.js +5 -6
  24. package/dist/src/framework-lifecycle.js +1 -1
  25. package/dist/src/fs/manifest.js +15 -20
  26. package/dist/src/generated-file.js +172 -29
  27. package/dist/src/graph.js +2 -4
  28. package/dist/src/index.js +1 -1
  29. package/dist/src/injection-container.js +1 -1
  30. package/dist/src/inventory/commonjs.js +62 -17
  31. package/dist/src/inventory/enumerate.js +2 -1
  32. package/dist/src/javascript-file.js +26 -11
  33. package/dist/src/load/classify.js +28 -9
  34. package/dist/src/load/diagnostics.js +8 -1
  35. package/dist/src/load/discover.js +15 -7
  36. package/dist/src/load/plan.js +2 -2
  37. package/dist/src/load/synthesize.js +5 -5
  38. package/dist/src/load/workspace-manifests.js +1 -1
  39. package/dist/src/matrix.js +6 -13
  40. package/dist/src/outside-tests.js +1 -1
  41. package/dist/src/paths.js +16 -0
  42. package/dist/src/platform-calls.js +78 -0
  43. package/dist/src/platform-symbols.js +140 -0
  44. package/dist/src/references.js +202 -32
  45. package/dist/src/resolve.js +1 -1
  46. package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +4 -4
  47. package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +5 -8
  48. package/dist/src/{manifest.js → roots/manifest.js} +4 -4
  49. package/dist/src/roots/published-reach.js +412 -0
  50. package/dist/src/roots/root-set.js +489 -0
  51. package/dist/src/roots/unseen-loads.js +208 -0
  52. package/dist/src/serialization-contract.js +1 -1
  53. package/dist/src/source-files.js +5 -9
  54. package/dist/src/suppress.js +30 -32
  55. package/dist/src/test-support.js +4 -2
  56. package/dist/src/verbs/analyze.js +13 -1
  57. package/dist/src/verbs/describe.js +5 -3
  58. package/jsr.json +1 -1
  59. package/package.json +2 -2
  60. package/src/analysis.ts +75 -52
  61. package/src/automatic-types.ts +1 -1
  62. package/src/commands/read.ts +6 -8
  63. package/src/conformance-record.ts +1 -1
  64. package/src/consumers.ts +4 -0
  65. package/src/contract/config.generated.ts +1 -1
  66. package/src/contract/contract.generated.ts +138 -2
  67. package/src/dependencies.ts +96 -49
  68. package/src/entries/build-scripts.ts +292 -0
  69. package/src/{configuration-files.ts → entries/configuration-files.ts} +23 -75
  70. package/src/{convention-rows.ts → entries/convention-rows.ts} +167 -28
  71. package/src/{conventions.ts → entries/conventions.ts} +347 -522
  72. package/src/{html-entries.ts → entries/html-entries.ts} +5 -5
  73. package/src/{mdx-entries.ts → entries/mdx-entries.ts} +4 -4
  74. package/src/entries/run-directories.ts +108 -0
  75. package/src/entries/sources.ts +266 -0
  76. package/src/entries/static-module.ts +857 -0
  77. package/src/entries/test-runners.ts +868 -0
  78. package/src/{workflow-steps.ts → entries/workflow-steps.ts} +29 -11
  79. package/src/exempt.ts +3 -2
  80. package/src/extensions.ts +45 -0
  81. package/src/facts.ts +44 -0
  82. package/src/file-facts.ts +1 -1
  83. package/src/findings/emitters.ts +14 -10
  84. package/src/findings/self-check.ts +41 -22
  85. package/src/findings/visibility-narrowing.ts +1 -1
  86. package/src/findings-pass.ts +5 -12
  87. package/src/framework-lifecycle.ts +1 -1
  88. package/src/fs/manifest.ts +25 -21
  89. package/src/generated-file.ts +240 -36
  90. package/src/graph.ts +3 -5
  91. package/src/index.ts +1 -1
  92. package/src/injection-container.ts +1 -1
  93. package/src/inventory/commonjs.ts +102 -17
  94. package/src/inventory/enumerate.ts +2 -1
  95. package/src/javascript-file.ts +29 -12
  96. package/src/load/classify.ts +49 -13
  97. package/src/load/diagnostics.ts +11 -1
  98. package/src/load/discover.ts +27 -11
  99. package/src/load/plan.ts +11 -2
  100. package/src/load/synthesize.ts +6 -6
  101. package/src/load/test-globals.ts +1 -1
  102. package/src/load/workspace-manifests.ts +1 -1
  103. package/src/load/workspace-run.ts +1 -1
  104. package/src/matrix.ts +7 -14
  105. package/src/outside-tests.ts +1 -1
  106. package/src/paths.ts +18 -0
  107. package/src/platform-calls.ts +119 -0
  108. package/src/platform-symbols.ts +205 -0
  109. package/src/references.ts +281 -34
  110. package/src/report.ts +1 -1
  111. package/src/resolve.ts +1 -1
  112. package/src/{configured-declarations.ts → roots/configured-declarations.ts} +7 -7
  113. package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +5 -8
  114. package/src/{manifest.ts → roots/manifest.ts} +5 -5
  115. package/src/roots/published-reach.ts +481 -0
  116. package/src/{roots.ts → roots/root-set.ts} +203 -630
  117. package/src/roots/unseen-loads.ts +307 -0
  118. package/src/serialization-contract.ts +1 -1
  119. package/src/source-files.ts +5 -10
  120. package/src/suppress.ts +48 -33
  121. package/src/test-support.ts +8 -3
  122. package/src/verbs/analyze.ts +17 -4
  123. package/src/verbs/describe.ts +5 -2
  124. package/src/verbs/explain.ts +1 -1
  125. package/dist/src/build-scripts.js +0 -418
  126. package/dist/src/conventions.js +0 -907
  127. package/dist/src/entry-points.js +0 -444
  128. package/dist/src/own-body.js +0 -28
  129. package/dist/src/roots.js +0 -865
  130. package/dist/src/unseen-loads.js +0 -189
  131. package/src/build-scripts.ts +0 -536
  132. package/src/entry-points.ts +0 -584
  133. package/src/own-body.ts +0 -42
  134. package/src/unseen-loads.ts +0 -282
@@ -6,38 +6,8 @@
6
6
  * No configuration file is run.
7
7
  */
8
8
 
9
- import {
10
- isArrayLiteralExpression,
11
- isBinaryExpression,
12
- isCallExpression,
13
- isClassDeclaration,
14
- isComputedPropertyName,
15
- isExportAssignment,
16
- isExportDeclaration,
17
- isExpressionStatement,
18
- isFunctionDeclaration,
19
- isIdentifier,
20
- isImportDeclaration,
21
- isNamedExports,
22
- isNamespaceImport,
23
- isNoSubstitutionTemplateLiteral,
24
- isNumericLiteral,
25
- isObjectBindingPattern,
26
- isObjectLiteralExpression,
27
- isPropertyAccessExpression,
28
- isPropertyAssignment,
29
- isShorthandPropertyAssignment,
30
- isSpreadAssignment,
31
- isStringLiteral,
32
- isVariableStatement,
33
- SyntaxKind,
34
- type Identifier,
35
- type Node,
36
- type ObjectBindingPattern,
37
- type ObjectLiteralExpression,
38
- type SourceFile,
39
- } from "@typescript/native/ast";
40
- import { lineOf } from "./component-files.ts";
9
+ import type { SourceFile } from "@typescript/native/ast";
10
+ import { lineOf } from "../component-files.ts";
41
11
  import {
42
12
  CONVENTION_ROWS,
43
13
  type ConventionRow,
@@ -49,10 +19,27 @@ import {
49
19
  type PackageKey,
50
20
  type PrefixedNameReading,
51
21
  type ShortNameReading,
22
+ type TestDirectoryReading,
52
23
  type TestFileReading,
53
24
  } from "./convention-rows.ts";
54
25
  import { documentStrings } from "./configuration-files.ts";
55
- import type { Host } from "./host.ts";
26
+ import { readCommands } from "../commands/read.ts";
27
+ import {
28
+ nodeTestGlobs,
29
+ type RunnerInput,
30
+ type ScriptCommand,
31
+ type TestGlob,
32
+ } from "./test-runners.ts";
33
+ import {
34
+ documentModule,
35
+ membersNamed,
36
+ staticModule,
37
+ textsOf,
38
+ valuesAt,
39
+ type StaticModule,
40
+ type Value,
41
+ } from "./static-module.ts";
42
+ import type { Host } from "../host.ts";
56
43
  import {
57
44
  ancestors,
58
45
  dirnamePath,
@@ -60,17 +47,17 @@ import {
60
47
  normalizePath,
61
48
  relativePath,
62
49
  resolvePath,
63
- } from "./paths.ts";
64
- import type { Failure } from "./failure.ts";
65
- import type { SourceFiles } from "./source-files.ts";
66
- import { satisfies } from "./version-range.ts";
67
- import { bytewise } from "./text/compare.ts";
68
- import { isRecord } from "./json-document.ts";
69
- import { unwrapped } from "./unwrap.ts";
70
- import { filesIn } from "./fs/tree.ts";
71
- import { DEPENDENCY_SECTIONS, INSTALLED_PACKAGES, MANIFEST, readJSON } from "./fs/manifest.ts";
72
- import { globExpression } from "./text/glob.ts";
73
- import { yamlDocument } from "./text/yaml.ts";
50
+ } from "../paths.ts";
51
+ import type { Failure } from "../failure.ts";
52
+ import type { SourceFiles } from "../source-files.ts";
53
+ import { satisfies } from "../version-range.ts";
54
+ import { bytewise } from "../text/compare.ts";
55
+ import { isRecord } from "../json-document.ts";
56
+ import { filesIn } from "../fs/tree.ts";
57
+ import { DEPENDENCY_SECTIONS, INSTALLED_PACKAGES, MANIFEST, readJSON } from "../fs/manifest.ts";
58
+ import { globExpression } from "../text/glob.ts";
59
+ import { yamlDocument } from "../text/yaml.ts";
60
+ import { APPENDED_EXTENSIONS, braced } from "../extensions.ts";
74
61
 
75
62
  /** One manifest the analysis reads. */
76
63
  export interface ConventionManifest {
@@ -132,8 +119,10 @@ export interface Conventions {
132
119
  readonly uses: readonly string[];
133
120
  /** Per configuration file, absolute, the strings its applied rows' selection keys hold. */
134
121
  readonly selected: ReadonlyMap<string, Selected>;
135
- /** The files, absolute, an applied row's test-file properties name. */
136
- readonly testFiles: ReadonlySet<string>;
122
+ /** The files, absolute, an applied row's test-file properties name, each with the string naming it. */
123
+ readonly testFiles: ReadonlyMap<string, string>;
124
+ /** The test-file patterns of the applied rows' test directory readings. */
125
+ readonly testGlobs: readonly TestGlob[];
137
126
  }
138
127
 
139
128
  /** The strings one configuration file holds at selection keys, which root no file. */
@@ -154,17 +143,11 @@ interface ConventionEntry {
154
143
  /** Parses one file's text with no program. */
155
144
  type ParseSource = (fileName: string, text: string) => SourceFile;
156
145
 
157
- const JSON_EXTENSION = ".json";
158
-
159
- /**
160
- * A configuration file its tool reads as a YAML document, read as the JSON document it
161
- * denotes. Its nodes' offsets are that JSON text's, so no move may read one: a failure's
162
- * line would name the wrong line.
163
- */
146
+ /** A configuration file its tool reads as a YAML document. */
164
147
  const YAML_NAME = /\.ya?ml$/u;
165
148
 
166
- /** A configuration file with no extension that its tool reads as a JSON document. */
167
- const JSON_RC = /^\.[\w-]+rc$/u;
149
+ /** A configuration file its tool reads as a JSON document whose name does not end with `.json`. */
150
+ const JSON_NAME = /^\.[\w-]+rc$|\.jsonc$/u;
168
151
 
169
152
  /** The sections an installed package is required by, so its absence fails the run. */
170
153
  const REQUIRED_SECTIONS = ["dependencies", "devDependencies"] as const;
@@ -200,319 +183,48 @@ function installedVersion(host: Host, dir: string, name: string): string | undef
200
183
  return undefined;
201
184
  }
202
185
 
203
- /** The key one property is written with, where it is written as a name or as literal text. */
204
- function keyOf(property: Node): string | undefined {
205
- if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property)) {
206
- return undefined;
207
- }
208
- const { name } = property;
209
- const key = isComputedPropertyName(name) ? name.expression : name;
210
- if (
211
- (isIdentifier(key) && key === name) ||
212
- isStringLiteral(key) ||
213
- isNoSubstitutionTemplateLiteral(key)
214
- ) {
215
- return key.text;
216
- }
217
- return undefined;
218
- }
219
-
220
- /** Whether one member's computed name is no literal, so it may set any key. */
221
- function computesKey(member: Node): boolean {
222
- const name = (member as { readonly name?: Node }).name;
223
- if (name === undefined || !isComputedPropertyName(name)) {
224
- return false;
225
- }
226
- const key = name.expression;
227
- return !(isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key));
228
- }
229
-
230
- /** The values one file's top-level variable declarations bind, by name. */
231
- function declaredValues(file: SourceFile): ReadonlyMap<string, Node> {
232
- const values = new Map<string, Node>();
233
- for (const statement of file.statements) {
234
- if (isVariableStatement(statement)) {
235
- for (const declaration of statement.declarationList.declarations) {
236
- if (isIdentifier(declaration.name) && declaration.initializer !== undefined) {
237
- values.set(declaration.name.text, declaration.initializer);
238
- }
239
- }
240
- }
241
- }
242
- return values;
243
- }
244
-
245
- function isModuleExports(node: Node): boolean {
246
- return (
247
- isPropertyAccessExpression(node) &&
248
- isIdentifier(node.expression) &&
249
- node.expression.text === "module" &&
250
- node.name.text === "exports"
251
- );
252
- }
253
-
254
- /**
255
- * Every expression or declaration one file exports as its default, in any module syntax.
256
- * A JSON module's default export is its document's value.
257
- */
258
- function defaultExports(file: SourceFile): readonly Node[] {
259
- if (file.fileName.endsWith(JSON_EXTENSION)) {
260
- return file.statements.flatMap((statement) =>
261
- isExpressionStatement(statement) ? [statement.expression] : [],
262
- );
263
- }
264
- const found: Node[] = [];
265
- for (const statement of file.statements) {
266
- if (isExportAssignment(statement)) {
267
- found.push(statement.expression);
268
- } else if (
269
- (isFunctionDeclaration(statement) || isClassDeclaration(statement)) &&
270
- statement.modifiers?.some((one) => one.kind === SyntaxKind.DefaultKeyword) === true
271
- ) {
272
- found.push(statement);
273
- } else if (
274
- isExportDeclaration(statement) &&
275
- statement.moduleSpecifier === undefined &&
276
- statement.exportClause !== undefined &&
277
- isNamedExports(statement.exportClause)
278
- ) {
279
- for (const element of statement.exportClause.elements) {
280
- if (element.name.text === "default") {
281
- found.push(element.propertyName ?? element.name);
282
- }
283
- }
284
- } else if (
285
- isExpressionStatement(statement) &&
286
- isBinaryExpression(statement.expression) &&
287
- statement.expression.operatorToken.kind === SyntaxKind.EqualsToken &&
288
- isModuleExports(statement.expression.left)
289
- ) {
290
- found.push(statement.expression.right);
291
- }
292
- }
293
- return found;
294
- }
295
-
296
- /** The declarations whose name is a binding of the scope they are written in. */
297
- const BINDING_KINDS: ReadonlySet<SyntaxKind> = new Set([
298
- SyntaxKind.VariableDeclaration,
299
- SyntaxKind.Parameter,
300
- SyntaxKind.BindingElement,
301
- SyntaxKind.FunctionDeclaration,
302
- SyntaxKind.ClassDeclaration,
303
- SyntaxKind.ImportClause,
304
- SyntaxKind.ImportSpecifier,
305
- SyntaxKind.NamespaceImport,
306
- SyntaxKind.ImportEqualsDeclaration,
307
- ]);
308
-
309
- /** Whether one file declares a binding named `require` at any depth, which shadows the global. */
310
- function declaresRequire(file: SourceFile): boolean {
311
- let found = false;
312
- const visit = (node: Node): void => {
313
- const name = (node as { readonly name?: Node }).name;
314
- if (
315
- name !== undefined &&
316
- isIdentifier(name) &&
317
- name.text === "require" &&
318
- BINDING_KINDS.has(node.kind)
319
- ) {
320
- found = true;
321
- }
322
- if (!found) {
323
- node.forEachChild(visit);
324
- }
325
- };
326
- file.forEachChild(visit);
327
- return found;
328
- }
329
-
330
- /**
331
- * The top-level `const`, `let` and `var` declarations that bind a `require` of one module,
332
- * written with the module's name as its one string-literal argument, in a file that
333
- * declares no binding named `require`: each binds as an import of the module does.
334
- */
335
- function requireBindings(
336
- file: SourceFile,
337
- module: string,
338
- ): readonly (Identifier | ObjectBindingPattern)[] {
339
- const found: (Identifier | ObjectBindingPattern)[] = [];
340
- for (const statement of file.statements) {
341
- if (!isVariableStatement(statement)) {
342
- continue;
343
- }
344
- for (const declaration of statement.declarationList.declarations) {
345
- const value =
346
- declaration.initializer === undefined ? undefined : unwrapped(declaration.initializer);
347
- const [argument, ...rest] =
348
- value !== undefined && isCallExpression(value) ? value.arguments : [];
349
- if (
350
- value === undefined ||
351
- !isCallExpression(value) ||
352
- !isIdentifier(value.expression) ||
353
- value.expression.text !== "require" ||
354
- argument === undefined ||
355
- rest.length > 0 ||
356
- !isStringLiteral(argument) ||
357
- argument.text !== module
358
- ) {
359
- continue;
360
- }
361
- if (isIdentifier(declaration.name) || isObjectBindingPattern(declaration.name)) {
362
- found.push(declaration.name);
363
- }
364
- }
365
- }
366
- return found.length > 0 && declaresRequire(file) ? [] : found;
367
- }
368
-
369
- /**
370
- * The first argument of every call one file makes to one module's export, through a
371
- * named or a namespace import; `undefined` for a call with no argument.
372
- */
373
- function optionsArguments(file: SourceFile, call: OptionsCall): readonly (Node | undefined)[] {
374
- const names = new Set<string>();
375
- const namespaces = new Set<string>();
376
- for (const name of requireBindings(file, call.module)) {
377
- if (isIdentifier(name)) {
378
- namespaces.add(name.text);
379
- continue;
380
- }
381
- for (const element of name.elements) {
382
- const local = element.name;
383
- const exported = element.propertyName ?? local;
384
- if (
385
- element.dotDotDotToken === undefined &&
386
- local !== undefined &&
387
- isIdentifier(local) &&
388
- exported !== undefined &&
389
- (isIdentifier(exported) || isStringLiteral(exported)) &&
390
- exported.text === call.export
391
- ) {
392
- names.add(local.text);
393
- }
394
- }
395
- }
396
- for (const statement of file.statements) {
397
- const bindings = isImportDeclaration(statement)
398
- ? statement.importClause?.namedBindings
399
- : undefined;
400
- if (
401
- bindings === undefined ||
402
- !isImportDeclaration(statement) ||
403
- !isStringLiteral(statement.moduleSpecifier) ||
404
- statement.moduleSpecifier.text !== call.module
405
- ) {
406
- continue;
407
- }
408
- if (isNamespaceImport(bindings)) {
409
- namespaces.add(bindings.name.text);
410
- continue;
411
- }
412
- for (const element of bindings.elements) {
413
- if ((element.propertyName ?? element.name).text === call.export) {
414
- names.add(element.name.text);
415
- }
416
- }
417
- }
418
- const found: (Node | undefined)[] = [];
419
- const visit = (node: Node): void => {
420
- if (isCallExpression(node)) {
421
- const callee = unwrapped(node.expression);
422
- const named = isIdentifier(callee) && names.has(callee.text);
423
- const throughNamespace =
424
- isPropertyAccessExpression(callee) &&
425
- isIdentifier(callee.expression) &&
426
- namespaces.has(callee.expression.text) &&
427
- callee.name.text === call.export;
428
- if (named || throughNamespace) {
429
- found.push(node.arguments[0]);
430
- }
431
- }
432
- node.forEachChild(visit);
433
- };
434
- file.forEachChild(visit);
435
- return found;
436
- }
437
-
438
- /**
439
- * The object literal one expression stands for, read through the file's top-level
440
- * declarations and, where `throughCalls`, through a call's first argument: `undefined`
441
- * for a call with no argument, and any other node where the expression is not written
442
- * out as an object literal.
443
- */
444
- function objectOf(
445
- node: Node,
446
- declared: ReadonlyMap<string, Node>,
447
- throughCalls: boolean,
448
- seen: ReadonlySet<string> = new Set(),
449
- ): Node | undefined {
450
- const at = unwrapped(node);
451
- if (isIdentifier(at)) {
452
- const value = declared.get(at.text);
453
- if (value !== undefined && !seen.has(at.text)) {
454
- return objectOf(value, declared, throughCalls, new Set([...seen, at.text]));
455
- }
456
- } else if (throughCalls && isCallExpression(at)) {
457
- const [first] = at.arguments;
458
- return first === undefined ? undefined : objectOf(first, declared, throughCalls, seen);
459
- }
460
- return at;
186
+ /** The objects a reading reads in one configuration file: each named call's options, or the default export. */
187
+ function readObjects(module: StaticModule, call: OptionsCall | undefined): readonly Value[] {
188
+ return call === undefined ? module.defaultExport : module.options(call.module, call.export);
461
189
  }
462
190
 
463
- /** The text of a string literal or a template literal with no substitution, or undefined. */
464
- function literalText(node: Node): string | undefined {
465
- const at = unwrapped(node);
466
- return isStringLiteral(at) || isNoSubstitutionTemplateLiteral(at) ? at.text : undefined;
191
+ /** Where a value that a reading cannot take as a literal is written, undefined in a document with no positions. */
192
+ interface NotLiteral {
193
+ readonly at: number | undefined;
467
194
  }
468
195
 
469
196
  /**
470
- * The literal texts one value at a path's end writes: the literal itself, or, where
471
- * `elements`, each element of an array literal of literals. Undefined for any other value.
472
- */
473
- function literalTexts(held: Node, elements: boolean): readonly string[] | undefined {
474
- const one = literalText(held);
475
- if (one !== undefined) {
476
- return [one];
477
- }
478
- if (!elements || !isArrayLiteralExpression(held)) {
479
- return undefined;
480
- }
481
- const texts = held.elements.map(literalText);
482
- return texts.every((text) => text !== undefined) ? texts : undefined;
483
- }
484
-
485
- /**
486
- * Reads the values at one path of keys below one object literal into `values`, and
487
- * answers the first member that does not write its value out: a spread, a value that is
488
- * not a literal at the path's end, or one that is not an object literal before it.
197
+ * Reads the values at one path of keys below one object into `values`, and answers the
198
+ * first place that does not write its value out: an object that is no object literal, a
199
+ * spread or a computed key, a value at the path's end that is no literal, or one before
200
+ * it that is no object literal.
489
201
  */
490
202
  function readPath(
491
- object: ObjectLiteralExpression,
203
+ object: Value,
492
204
  keys: readonly string[],
493
205
  values: string[],
494
206
  elements: boolean,
495
- ): Node | undefined {
207
+ ): NotLiteral | undefined {
208
+ if (object.kind !== "object") {
209
+ return object;
210
+ }
496
211
  const [key, ...rest] = keys;
497
- for (const member of object.properties) {
498
- if (isSpreadAssignment(member) || computesKey(member)) {
212
+ for (const member of object.members) {
213
+ if (member.key === undefined) {
499
214
  return member;
500
215
  }
501
- if (keyOf(member) !== key) {
216
+ if (member.key !== key) {
502
217
  continue;
503
218
  }
504
- const held = isPropertyAssignment(member) ? unwrapped(member.initializer) : undefined;
505
219
  if (rest.length === 0) {
506
- const texts = held === undefined ? undefined : literalTexts(held, elements);
220
+ const texts = textsOf(member.value, elements);
507
221
  if (texts === undefined) {
508
222
  return member;
509
223
  }
510
224
  values.push(...texts);
511
225
  } else {
512
- if (held === undefined || !isObjectLiteralExpression(held)) {
513
- return member;
514
- }
515
- const failed = readPath(held, rest, values, elements);
226
+ const failed =
227
+ member.value.kind === "object" ? readPath(member.value, rest, values, elements) : member;
516
228
  if (failed !== undefined) {
517
229
  return failed;
518
230
  }
@@ -521,80 +233,45 @@ function readPath(
521
233
  return undefined;
522
234
  }
523
235
 
524
- /**
525
- * Every value one file writes at one property's path inside the objects it is read from,
526
- * as {@link readProperty} reads them, whatever else the path holds.
527
- */
528
- function valuesAt(file: SourceFile, property: string, call?: OptionsCall): readonly Node[] {
529
- const declared = declaredValues(file);
530
- const objects =
531
- call === undefined
532
- ? defaultExports(file).map((one) => objectOf(one, declared, true))
533
- : optionsArguments(file, call).map((one) =>
534
- one === undefined ? undefined : objectOf(one, declared, false),
535
- );
536
- const found: Node[] = [];
537
- const visit = (object: Node, keys: readonly string[]): void => {
538
- if (!isObjectLiteralExpression(object)) {
539
- return;
540
- }
541
- const [key, ...rest] = keys;
542
- for (const member of object.properties) {
543
- if (!isPropertyAssignment(member) || keyOf(member) !== key) {
544
- continue;
545
- }
546
- const held = unwrapped(member.initializer);
547
- if (rest.length > 0) {
548
- visit(held, rest);
549
- } else {
550
- found.push(held);
551
- }
552
- }
553
- };
554
- for (const object of objects) {
555
- if (object !== undefined) {
556
- visit(object, property.split("."));
557
- }
558
- }
559
- return found;
560
- }
561
-
562
- /** The string literals among values: each value itself, or each element of an array value. */
563
- function stringsAt(file: SourceFile, property: string, call?: OptionsCall): readonly Node[] {
564
- return valuesAt(file, property, call).flatMap((held) => {
565
- if (literalText(held) !== undefined) {
566
- return [held];
567
- }
568
- return isArrayLiteralExpression(held)
569
- ? held.elements.map(unwrapped).filter((one) => literalText(one) !== undefined)
570
- : [];
571
- });
236
+ /** The text values among the values at one path: each value itself, or each element of an array value. */
237
+ function stringsAt(
238
+ objects: readonly Value[],
239
+ property: string,
240
+ ): readonly { readonly text: string; readonly at: number | undefined }[] {
241
+ return valuesAt(objects, property.split(".")).flatMap((value) =>
242
+ (value.kind === "array" ? value.elements : [value]).flatMap((one) =>
243
+ one.kind === "text" ? [one] : [],
244
+ ),
245
+ );
572
246
  }
573
247
 
574
248
  /** Whether one value holds a helper setting's value, as {@link HelperReading} states it. */
575
- function holdsValue(held: Node, value: true | string): boolean {
249
+ function holdsValue(held: Value, value: true | string): boolean {
576
250
  if (value === true) {
577
- return held.kind === SyntaxKind.TrueKeyword;
251
+ return held.kind === "boolean" && held.value;
578
252
  }
579
- if (literalText(held) === value) {
580
- return true;
253
+ if (held.kind === "text") {
254
+ return held.text === value;
581
255
  }
582
256
  return (
583
- isArrayLiteralExpression(held) &&
257
+ held.kind === "array" &&
584
258
  held.elements.some((element) => {
585
- const one = unwrapped(element);
586
- const first = isArrayLiteralExpression(one) ? one.elements[0] : one;
587
- return first !== undefined && literalText(first) === value;
259
+ const first = element.kind === "array" ? element.elements[0] : element;
260
+ return first?.kind === "text" && first.text === value;
588
261
  })
589
262
  );
590
263
  }
591
264
 
592
- /** The directory one file's test-file strings are read against, as {@link TestFileReading} states it. */
593
- function testFileRoot(file: SourceFile, dir: string, reading: TestFileReading): string | undefined {
594
- for (const property of reading.roots) {
595
- const values = valuesAt(file, property, reading.call);
265
+ /** The directory one group of objects' test-file strings are read against, as {@link TestFileReading} states it. */
266
+ function testFileRoot(
267
+ objects: readonly Value[],
268
+ dir: string,
269
+ roots: readonly string[],
270
+ ): string | undefined {
271
+ for (const property of roots) {
272
+ const values = valuesAt(objects, property.split("."));
596
273
  if (values.length > 0) {
597
- const texts = new Set(values.map(literalText));
274
+ const texts = new Set(values.map((one) => (one.kind === "text" ? one.text : undefined)));
598
275
  const [text] = texts;
599
276
  return texts.size === 1 && text !== undefined ? resolvePath(dir, text) : undefined;
600
277
  }
@@ -602,45 +279,146 @@ function testFileRoot(file: SourceFile, dir: string, reading: TestFileReading):
602
279
  return dir;
603
280
  }
604
281
 
605
- /** The files an applied row's test-file properties name. */
282
+ /**
283
+ * The files an applied row's test-file properties name, each string also added to
284
+ * `selected`, since a test file is rooted by no configuration string (conventions.11). A
285
+ * property's path up to its last `*` selects objects that each resolve their strings
286
+ * against their own roots.
287
+ */
606
288
  function testFilesNamed(
607
- host: Host,
608
- parse: ParseSource,
289
+ read: ReadFiles,
609
290
  dir: string,
610
291
  readings: readonly TestFileReading[],
611
- into: Set<string>,
292
+ into: Map<string, string>,
293
+ selected: Map<string, { starts: Set<number>; texts: string[] }>,
612
294
  ): void {
613
295
  for (const reading of readings) {
614
- for (const { file } of readingFiles(host, parse, dir, reading.files)) {
615
- const at = testFileRoot(file, dir, reading);
616
- if (at === undefined) {
617
- continue;
296
+ const star = reading.property.lastIndexOf("*.");
297
+ const each = star < 0 ? undefined : reading.property.slice(0, star + 1).split(".");
298
+ const property = star < 0 ? reading.property : reading.property.slice(star + 2);
299
+ for (const { name, module } of read(dir, reading.files)) {
300
+ const path = joinPath(dir, name);
301
+ const held = selected.get(path) ?? { starts: new Set<number>(), texts: [] };
302
+ selected.set(path, held);
303
+ const objects = readObjects(module, reading.call);
304
+ const groups = each === undefined ? [objects] : valuesAt(objects, each).map((one) => [one]);
305
+ for (const group of groups) {
306
+ const at = testFileRoot(group, dir, reading.roots);
307
+ if (at === undefined) {
308
+ continue;
309
+ }
310
+ for (const { text, at: start } of stringsAt(group, property)) {
311
+ if (start !== undefined) {
312
+ held.starts.add(start);
313
+ }
314
+ held.texts.push(text);
315
+ if (text !== "") {
316
+ into.set(resolvePath(at, text), text);
317
+ }
318
+ }
618
319
  }
619
- for (const node of stringsAt(file, reading.property, reading.call)) {
620
- const text = literalText(node);
621
- if (text !== undefined && text !== "") {
622
- into.add(resolvePath(at, text));
320
+ }
321
+ }
322
+ }
323
+
324
+ /** The directory and patterns one configuration object's tests are read from, undefined where its text does not state them. */
325
+ interface TestDirectory {
326
+ readonly directory: string;
327
+ readonly patterns: readonly string[];
328
+ }
329
+
330
+ /** The one value an object's last write of a key gives, `null` where it writes none, undefined where it may hide one. */
331
+ function lastMember(object: Value, key: string): Value | null | undefined {
332
+ const values = membersNamed(object, key);
333
+ return values === undefined ? undefined : (values.at(-1) ?? null);
334
+ }
335
+
336
+ /** The test directory one object states, each member it does not write taken from `inherited`. */
337
+ function testDirectoryOf(
338
+ object: Value,
339
+ reading: TestDirectoryReading,
340
+ inherited: TestDirectory,
341
+ ): TestDirectory | undefined {
342
+ if (object.kind !== "object") {
343
+ return undefined;
344
+ }
345
+ const directory = lastMember(object, reading.directory);
346
+ const match = lastMember(object, reading.match);
347
+ if (directory === undefined || match === undefined) {
348
+ return undefined;
349
+ }
350
+ const dirs = directory === null ? [inherited.directory] : textsOf(directory, false);
351
+ const patterns = match === null ? inherited.patterns : textsOf(match, true);
352
+ const [one] = dirs ?? [];
353
+ return one === undefined || patterns === undefined ? undefined : { directory: one, patterns };
354
+ }
355
+
356
+ /**
357
+ * The test-file patterns an applied row's test directory readings give: each configuration
358
+ * object's own, or, where it lists projects, each project's, a member a project does not
359
+ * write taken from the configuration. An object whose text does not state them gives none.
360
+ */
361
+ function testDirectoryGlobs(
362
+ read: ReadFiles,
363
+ dir: string,
364
+ readings: readonly TestDirectoryReading[],
365
+ into: TestGlob[],
366
+ ): void {
367
+ for (const reading of readings) {
368
+ const base = { directory: ".", patterns: [reading.defaultMatch] };
369
+ for (const { module } of read(dir, reading.files)) {
370
+ for (const config of module.defaultExport) {
371
+ const top = testDirectoryOf(config, reading, base);
372
+ const projects = lastMember(config, reading.projects);
373
+ if (top === undefined || projects === undefined) {
374
+ continue;
375
+ }
376
+ const listed =
377
+ projects === null ? [] : projects.kind === "array" ? projects.elements : [projects];
378
+ const each =
379
+ listed.length === 0 ? [top] : listed.map((one) => testDirectoryOf(one, reading, top));
380
+ for (const found of each) {
381
+ for (const pattern of found?.patterns ?? []) {
382
+ const expression = anywhereBelow(pattern);
383
+ into.push({
384
+ dir: resolvePath(dir, found?.directory ?? "."),
385
+ pattern,
386
+ matches: (name) => expression.test(name.replace(/^\/+/u, "")),
387
+ });
388
+ }
623
389
  }
624
390
  }
625
391
  }
626
392
  }
627
393
  }
628
394
 
395
+ /**
396
+ * The expression a test-file pattern denotes over a file's whole path, as Playwright's
397
+ * matcher reads it: a pattern gains a leading `**` segment unless it starts with one, and
398
+ * case is not compared (https://github.com/microsoft/playwright/blob/main/packages/playwright/src/util.ts,
399
+ * `createFileMatcher`).
400
+ */
401
+ function anywhereBelow(pattern: string): RegExp {
402
+ const expression = globExpression(pattern.startsWith("**/") ? pattern : `**/${pattern}`);
403
+ return new RegExp(expression.source, "iu");
404
+ }
405
+
629
406
  /** The strings an applied row's selection keys hold, added per configuration file to `into`. */
630
407
  function selectedStrings(
631
- host: Host,
632
- parse: ParseSource,
408
+ read: ReadFiles,
633
409
  dir: string,
634
410
  readings: readonly MoveReading[],
635
411
  into: Map<string, { starts: Set<number>; texts: string[] }>,
636
412
  ): void {
637
413
  for (const reading of readings) {
638
- for (const { name, file } of readingFiles(host, parse, dir, reading.files)) {
414
+ for (const { name, module } of read(dir, reading.files)) {
639
415
  const path = joinPath(dir, name);
640
416
  const held = into.get(path) ?? { starts: new Set<number>(), texts: [] };
641
- for (const node of stringsAt(file, reading.property, reading.call)) {
642
- held.starts.add(node.getStart());
643
- held.texts.push(literalText(node) ?? "");
417
+ for (const { text, at } of stringsAt(readObjects(module, reading.call), reading.property)) {
418
+ if (at !== undefined) {
419
+ held.starts.add(at);
420
+ }
421
+ held.texts.push(text);
644
422
  }
645
423
  into.set(path, held);
646
424
  }
@@ -666,7 +444,7 @@ function manifestKeyUses(manifest: unknown, keys: readonly string[]): readonly s
666
444
  }
667
445
 
668
446
  /** The extensions an entry a manifest entry key names without one may carry. */
669
- const ENTRY_EXTENSIONS = "{js,jsx,mjs,cjs,ts,tsx,mts,cts}";
447
+ const ENTRY_EXTENSIONS = braced(APPENDED_EXTENSIONS);
670
448
 
671
449
  /** The characters that make a manifest string a pattern rather than a directory's name. */
672
450
  const PATTERN = /[*?{}[\]]/u;
@@ -710,12 +488,12 @@ function manifestEntryGlobs(manifest: unknown, keys: readonly ManifestEntryKey[]
710
488
  interface PropertyReading {
711
489
  /** The literal values the file sets the property to. */
712
490
  readonly values: readonly string[];
713
- /** The position of the first value that is not a literal, where there is one. */
714
- readonly notLiteral?: Node;
491
+ /** Where the first value that is not a literal is written, where there is one. */
492
+ readonly notLiteral?: NotLiteral;
715
493
  }
716
494
 
717
495
  /**
718
- * Every value one file writes at one property's path inside the object it is read from:
496
+ * Every value one file writes at one property's path inside the objects it is read from:
719
497
  * the options argument of every call `call` names, or the file's default export. A string
720
498
  * literal or a template literal with no substitution is the value, and so is each element
721
499
  * of an array literal of them where `elements`. A read object, or an object along the
@@ -723,26 +501,14 @@ interface PropertyReading {
723
501
  * itself, is not a literal, and neither is any other value at the path.
724
502
  */
725
503
  export function readProperty(
726
- file: SourceFile,
504
+ module: StaticModule,
727
505
  property: string,
728
506
  call?: OptionsCall,
729
507
  elements = false,
730
508
  ): PropertyReading {
731
- const declared = declaredValues(file);
732
- const objects =
733
- call === undefined
734
- ? defaultExports(file).map((one) => objectOf(one, declared, true))
735
- : optionsArguments(file, call).map((one) =>
736
- one === undefined ? undefined : objectOf(one, declared, false),
737
- );
738
509
  const values: string[] = [];
739
- for (const object of objects) {
740
- if (object === undefined) {
741
- continue;
742
- }
743
- const notLiteral = isObjectLiteralExpression(object)
744
- ? readPath(object, property.split("."), values, elements)
745
- : object;
510
+ for (const object of readObjects(module, call)) {
511
+ const notLiteral = readPath(object, property.split("."), values, elements);
746
512
  if (notLiteral !== undefined) {
747
513
  return { values, notLiteral };
748
514
  }
@@ -793,8 +559,7 @@ function configurationFiles(host: Host, dir: string, glob: string): readonly str
793
559
  type MoveResult = { readonly dirs: readonly (string | undefined)[] } | { readonly failure: string };
794
560
 
795
561
  function resolveMove(
796
- host: Host,
797
- parse: ParseSource,
562
+ read: ReadFiles,
798
563
  dir: string,
799
564
  row: ConventionRow,
800
565
  move: DirectoryMove,
@@ -802,13 +567,14 @@ function resolveMove(
802
567
  ): MoveResult {
803
568
  const set: string[] = [];
804
569
  for (const reading of move.readings) {
805
- for (const { name, text, file } of readingFiles(host, parse, dir, reading.files)) {
806
- const read = readProperty(file, reading.property, reading.call);
570
+ for (const { name, text, module } of read(dir, reading.files)) {
571
+ const read = readProperty(module, reading.property, reading.call);
807
572
  if (read.notLiteral !== undefined) {
808
- const line = lineOf(text, read.notLiteral.getStart());
573
+ const { at } = read.notLiteral;
574
+ const where = at === undefined ? name : `${name}:${String(lineOf(text, at))}`;
809
575
  return {
810
576
  failure:
811
- `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
577
+ `${where} sets ${reading.property}, which moves a directory ` +
812
578
  `the convention row ${row.name} reads, to a value that is not a string literal: ` +
813
579
  `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
814
580
  `files in ts.entry_files`,
@@ -828,39 +594,44 @@ function resolveMove(
828
594
  return { dirs: [...new Set(dirs)] };
829
595
  }
830
596
 
831
- /** The texts each configuration file a reading names holds, parsed, in name order. */
832
- function readingFiles(
833
- host: Host,
834
- parse: ParseSource,
835
- dir: string,
836
- files: readonly string[],
837
- ): readonly { readonly name: string; readonly text: string; readonly file: SourceFile }[] {
838
- const found = [];
839
- for (const glob of files) {
840
- for (const name of configurationFiles(host, dir, glob)) {
841
- const path = joinPath(dir, name);
842
- let text: string;
843
- try {
844
- text = host.readFile(path);
845
- } catch {
846
- continue;
847
- }
597
+ /** One configuration file a reading names, read as a static module. */
598
+ interface ReadFile {
599
+ readonly name: string;
600
+ readonly text: string;
601
+ readonly module: StaticModule;
602
+ }
603
+
604
+ /** The configuration files in one directory a reading's globs name, in name order. */
605
+ type ReadFiles = (dir: string, files: readonly string[]) => readonly ReadFile[];
606
+
607
+ /** A reader that reads and parses each configuration file once for every row and reading of a run. */
608
+ function readFilesOnce(host: Host, parse: ParseSource): ReadFiles {
609
+ const read = new Map<string, ReadFile | undefined>();
610
+ const readOne = (dir: string, name: string): ReadFile | undefined => {
611
+ const path = joinPath(dir, name);
612
+ if (read.has(path)) {
613
+ return read.get(path);
614
+ }
615
+ let found: ReadFile | undefined;
616
+ try {
617
+ const text = host.readFile(path);
848
618
  if (YAML_NAME.test(name)) {
849
619
  const value = yamlDocument(text);
850
- if (value !== undefined) {
851
- const json = JSON.stringify(value);
852
- found.push({ name, text: json, file: parse(`${path}${JSON_EXTENSION}`, json) });
853
- }
854
- continue;
620
+ found = value === undefined ? undefined : { name, text, module: documentModule(value) };
621
+ } else {
622
+ const file = parse(JSON_NAME.test(name) ? `${path}.json` : path, text);
623
+ found = { name, text, module: staticModule(file) };
855
624
  }
856
- found.push({
857
- name,
858
- text,
859
- file: parse(JSON_RC.test(name) ? `${path}${JSON_EXTENSION}` : path, text),
860
- });
625
+ } catch {
626
+ found = undefined;
861
627
  }
862
- }
863
- return found;
628
+ read.set(path, found);
629
+ return found;
630
+ };
631
+ return (dir, files) =>
632
+ files.flatMap((glob) =>
633
+ configurationFiles(host, dir, glob).flatMap((name) => readOne(dir, name) ?? []),
634
+ );
864
635
  }
865
636
 
866
637
  /**
@@ -869,8 +640,7 @@ function readingFiles(
869
640
  * declared dependency the row's package name stands for with some value.
870
641
  */
871
642
  function shortNameUses(
872
- host: Host,
873
- parse: ParseSource,
643
+ read: ReadFiles,
874
644
  dir: string,
875
645
  manifest: unknown,
876
646
  readings: readonly ShortNameReading[],
@@ -884,8 +654,8 @@ function shortNameUses(
884
654
  const used = new Set<string>();
885
655
  for (const reading of readings) {
886
656
  const [before = "", after = ""] = reading.package.split("{}");
887
- for (const { file } of readingFiles(host, parse, dir, reading.files)) {
888
- const read = readProperty(file, reading.property, reading.call, true);
657
+ for (const { module } of read(dir, reading.files)) {
658
+ const read = readProperty(module, reading.property, reading.call, true);
889
659
  for (const value of read.values) {
890
660
  used.add(`${before}${value}${after}`);
891
661
  }
@@ -939,8 +709,7 @@ function prefixedPackage(value: string, reading: PrefixedNameReading): string |
939
709
 
940
710
  /** The declared dependencies an applied row's prefixed names use. */
941
711
  function prefixedNameUses(
942
- host: Host,
943
- parse: ParseSource,
712
+ read: ReadFiles,
944
713
  dir: string,
945
714
  manifest: unknown,
946
715
  readings: readonly PrefixedNameReading[],
@@ -953,8 +722,8 @@ function prefixedNameUses(
953
722
  );
954
723
  const used = new Set<string>();
955
724
  for (const reading of readings) {
956
- for (const { file } of readingFiles(host, parse, dir, reading.files)) {
957
- for (const value of readProperty(file, reading.property, reading.call, true).values) {
725
+ for (const { module } of read(dir, reading.files)) {
726
+ for (const value of readProperty(module, reading.property, reading.call, true).values) {
958
727
  const named = prefixedPackage(value, reading);
959
728
  if (named !== undefined && declared.has(named)) {
960
729
  used.add(named);
@@ -967,8 +736,7 @@ function prefixedNameUses(
967
736
 
968
737
  /** The declared dependencies an applied row's helper settings use. */
969
738
  function helperUses(
970
- host: Host,
971
- parse: ParseSource,
739
+ read: ReadFiles,
972
740
  dir: string,
973
741
  manifest: unknown,
974
742
  readings: readonly HelperReading[],
@@ -983,8 +751,8 @@ function helperUses(
983
751
  .filter(
984
752
  (reading) =>
985
753
  declared.has(reading.package) &&
986
- readingFiles(host, parse, dir, reading.files).some(({ file }) =>
987
- valuesAt(file, reading.property, reading.call).some((held) =>
754
+ read(dir, reading.files).some(({ module }) =>
755
+ valuesAt(readObjects(module, reading.call), reading.property.split(".")).some((held) =>
988
756
  holdsValue(held, reading.value),
989
757
  ),
990
758
  ),
@@ -998,8 +766,7 @@ function helperUses(
998
766
  * by `:` and a name. A member's own name is no such string.
999
767
  */
1000
768
  function packageKeyUses(
1001
- host: Host,
1002
- parse: ParseSource,
769
+ read: ReadFiles,
1003
770
  dir: string,
1004
771
  manifest: unknown,
1005
772
  keys: readonly PackageKey[],
@@ -1009,31 +776,31 @@ function packageKeyUses(
1009
776
  return isRecord(held) ? Object.keys(held) : [];
1010
777
  });
1011
778
  const strings: string[] = [];
1012
- const collect = (node: Node): void => {
1013
- const text = literalText(node);
1014
- if (text !== undefined) {
1015
- strings.push(text);
1016
- } else if (isPropertyAssignment(node)) {
1017
- collect(node.initializer);
1018
- } else {
1019
- node.forEachChild(collect);
779
+ const children = (value: Value): readonly Value[] =>
780
+ value.kind === "object"
781
+ ? value.members.map((member) => member.value)
782
+ : value.kind === "array"
783
+ ? value.elements
784
+ : [];
785
+ const collect = (value: Value): void => {
786
+ if (value.kind === "text") {
787
+ strings.push(value.text);
1020
788
  }
789
+ children(value).forEach(collect);
1021
790
  };
1022
791
  for (const { key, files } of keys) {
1023
- const visit = (node: Node): void => {
1024
- if (isPropertyAssignment(node) && keyOf(node) === key) {
1025
- collect(node.initializer);
792
+ const visit = (value: Value): void => {
793
+ if (value.kind === "object") {
794
+ value.members
795
+ .filter((member) => member.key === key)
796
+ .forEach((member) => {
797
+ collect(member.value);
798
+ });
1026
799
  }
1027
- node.forEachChild(visit);
800
+ children(value).forEach(visit);
1028
801
  };
1029
- for (const { file } of readingFiles(host, parse, dir, files)) {
1030
- const values = declaredValues(file);
1031
- for (const one of defaultExports(file)) {
1032
- const object = objectOf(one, values, true);
1033
- if (object !== undefined) {
1034
- visit(object);
1035
- }
1036
- }
802
+ for (const { module } of read(dir, files)) {
803
+ module.defaultExport.forEach(visit);
1037
804
  }
1038
805
  }
1039
806
  return declared.filter((name) =>
@@ -1046,6 +813,16 @@ function packageKeyUses(
1046
813
  );
1047
814
  }
1048
815
 
816
+ /** Every simple command of a manifest's `scripts`, as the command reader reads it in the manifest's directory. */
817
+ function scriptCommands(manifest: unknown, dir: string): readonly ScriptCommand[] {
818
+ const scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
819
+ return isRecord(scripts)
820
+ ? Object.values(scripts).flatMap((text) =>
821
+ typeof text === "string" ? readCommands(text, dir).commands : [],
822
+ )
823
+ : [];
824
+ }
825
+
1049
826
  /**
1050
827
  * What the rows decide over the manifests the analysis reads. A row `disabled` names
1051
828
  * is skipped before anything is read for it.
@@ -1063,9 +840,30 @@ export function readConventions(
1063
840
  const failures: ConventionFailure[] = [];
1064
841
  const uses = new Set<string>();
1065
842
  const selected = new Map<string, { starts: Set<number>; texts: string[] }>();
1066
- const testFiles = new Set<string>();
843
+ const testFiles = new Map<string, string>();
844
+ const testGlobs: TestGlob[] = [];
845
+ const read = readFilesOnce(host, parse);
1067
846
  for (const manifest of manifests) {
1068
847
  const declared = readJSON(host, joinPath(manifest.dir, MANIFEST));
848
+ let input: RunnerInput | undefined;
849
+ const runnerInput = (): RunnerInput => {
850
+ input ??= {
851
+ host,
852
+ dir: manifest.dir,
853
+ manifest: declared,
854
+ configuration: (dir, names) => {
855
+ for (const name of names) {
856
+ const [found] = read(dir, [name]);
857
+ if (found !== undefined) {
858
+ return { dir, module: found.module };
859
+ }
860
+ }
861
+ return undefined;
862
+ },
863
+ scripts: scriptCommands(declared, manifest.dir),
864
+ };
865
+ return input;
866
+ };
1069
867
  const versions = new Map<string, string | undefined>();
1070
868
  const missing = new Set<string>();
1071
869
  for (const row of rows) {
@@ -1102,7 +900,7 @@ export function readConventions(
1102
900
  const resolved = new Map<string, readonly (string | undefined)[]>();
1103
901
  let failed: string | undefined;
1104
902
  for (const move of row.moves) {
1105
- const result = resolveMove(host, parse, manifest.dir, row, move, resolved);
903
+ const result = resolveMove(read, manifest.dir, row, move, resolved);
1106
904
  if ("failure" in result) {
1107
905
  failed = result.failure;
1108
906
  break;
@@ -1126,25 +924,27 @@ export function readConventions(
1126
924
  version,
1127
925
  manifest: manifest.path,
1128
926
  });
1129
- for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
927
+ for (const name of shortNameUses(read, manifest.dir, declared, row.shortNames ?? [])) {
1130
928
  uses.add(name);
1131
929
  }
1132
930
  const prefixed = row.prefixedNames ?? [];
1133
- for (const name of prefixedNameUses(host, parse, manifest.dir, declared, prefixed)) {
931
+ for (const name of prefixedNameUses(read, manifest.dir, declared, prefixed)) {
1134
932
  uses.add(name);
1135
933
  }
1136
934
  const keys = row.packageKeys ?? [];
1137
- for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
935
+ for (const name of packageKeyUses(read, manifest.dir, declared, keys)) {
1138
936
  uses.add(name);
1139
937
  }
1140
938
  for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
1141
939
  uses.add(name);
1142
940
  }
1143
- for (const name of helperUses(host, parse, manifest.dir, declared, row.helpers ?? [])) {
941
+ for (const name of helperUses(read, manifest.dir, declared, row.helpers ?? [])) {
1144
942
  uses.add(name);
1145
943
  }
1146
- selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
1147
- testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
944
+ selectedStrings(read, manifest.dir, row.selections ?? [], selected);
945
+ testFilesNamed(read, manifest.dir, row.testFiles ?? [], testFiles, selected);
946
+ testDirectoryGlobs(read, manifest.dir, row.testDirectories ?? [], testGlobs);
947
+ testGlobs.push(...(row.testRunner?.(runnerInput()) ?? []));
1148
948
  const excludes = [
1149
949
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
1150
950
  ].map(globExpression);
@@ -1164,6 +964,7 @@ export function readConventions(
1164
964
  });
1165
965
  }
1166
966
  }
967
+ testGlobs.push(...nodeTestGlobs(runnerInput().scripts));
1167
968
  }
1168
969
  const unique = [...new Map(applied.map((one) => [JSON.stringify(one), one])).values()];
1169
970
  unique.sort(
@@ -1180,9 +981,33 @@ export function readConventions(
1180
981
  uses: [...uses].sort(bytewise),
1181
982
  selected,
1182
983
  testFiles,
984
+ testGlobs,
1183
985
  };
1184
986
  }
1185
987
 
988
+ /**
989
+ * The files, absolute, of one project an applied row names as test files, each with the
990
+ * string or the pattern that names it.
991
+ */
992
+ export function conventionTestFiles(
993
+ conventions: Conventions,
994
+ files: SourceFiles,
995
+ ): ReadonlyMap<string, string> {
996
+ const found = new Map(conventions.testFiles);
997
+ for (const { dir, pattern, matches } of conventions.testGlobs) {
998
+ for (const file of files.byName.values()) {
999
+ if (
1000
+ relativePath(dir, file.fileName) !== undefined &&
1001
+ matches(file.fileName) &&
1002
+ !found.has(file.fileName)
1003
+ ) {
1004
+ found.set(file.fileName, pattern);
1005
+ }
1006
+ }
1007
+ }
1008
+ return found;
1009
+ }
1010
+
1186
1011
  /**
1187
1012
  * Every own file of one project below a directory an applied row names as generated, by
1188
1013
  * its path below the target root, with the name of the first such row.