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

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