@cplieger/deadset-ts 4.0.1 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
@@ -0,0 +1,788 @@
1
+ /**
2
+ * The convention rows a run applies, and the files they root. A row applies to a
3
+ * manifest that declares its enabling package when the installed package's version is
4
+ * in the row's range; its globs are read against that manifest's directory, with every
5
+ * directory a configuration property moves read from the property's literal value.
6
+ * No configuration file is run.
7
+ */
8
+
9
+ import {
10
+ isAsExpression,
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
+ isNonNullExpression,
25
+ isNumericLiteral,
26
+ isObjectBindingPattern,
27
+ isObjectLiteralExpression,
28
+ isParenthesizedExpression,
29
+ isPropertyAccessExpression,
30
+ isPropertyAssignment,
31
+ isSatisfiesExpression,
32
+ isShorthandPropertyAssignment,
33
+ isSpreadAssignment,
34
+ isStringLiteral,
35
+ isVariableStatement,
36
+ SyntaxKind,
37
+ type AsExpression,
38
+ type Identifier,
39
+ type Node,
40
+ type ObjectBindingPattern,
41
+ type NonNullExpression,
42
+ type ObjectLiteralExpression,
43
+ type ParenthesizedExpression,
44
+ type SatisfiesExpression,
45
+ type SourceFile,
46
+ } from "@typescript/native/unstable/ast";
47
+ import { lineOf } from "./component-files.ts";
48
+ import {
49
+ CONVENTION_ROWS,
50
+ type ConventionRow,
51
+ type DirectoryMove,
52
+ type OptionsCall,
53
+ } from "./convention-rows.ts";
54
+ import { globExpression } from "./glob.ts";
55
+ import type { Host } from "./host.ts";
56
+ import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
57
+ import type { SetupFailure } from "./setup-failure.ts";
58
+ import type { SourceFiles } from "./source-files.ts";
59
+ import { satisfies } from "./version-range.ts";
60
+
61
+ /** One manifest the analysis reads. */
62
+ export interface ConventionManifest {
63
+ /** The manifest's directory, absolute. */
64
+ readonly dir: string;
65
+ /** The manifest's path below the target root, `/`-separated. */
66
+ readonly path: string;
67
+ }
68
+
69
+ /** One row a run applied, as the report lists it. */
70
+ export interface AppliedConvention {
71
+ readonly name: string;
72
+ readonly package: string;
73
+ /** The version the installed package's own manifest names. */
74
+ readonly version: string;
75
+ /** The declaring manifest's path below the target root. */
76
+ readonly manifest: string;
77
+ }
78
+
79
+ /** One glob of an applied row, read against its manifest's directory. */
80
+ interface ConventionGlob {
81
+ readonly dir: string;
82
+ readonly row: string;
83
+ readonly expression: RegExp;
84
+ /** The expressions of the row's excludes, read against the same directory. */
85
+ readonly excludes: readonly RegExp[];
86
+ }
87
+
88
+ /** One directory an applied row names as generated, read against its manifest's directory. */
89
+ interface GeneratedDirectory {
90
+ readonly dir: string;
91
+ readonly row: string;
92
+ /** The directory below the manifest's directory, `/`-separated. */
93
+ readonly below: string;
94
+ }
95
+
96
+ /** A setup failure a row met, at the directory of the manifest that declared it. */
97
+ interface ConventionFailure {
98
+ readonly dir: string;
99
+ readonly failure: SetupFailure;
100
+ }
101
+
102
+ /** What the rows decided for one run. */
103
+ export interface Conventions {
104
+ /** The rows applied, ordered by name, then manifest, then their compact encoding. */
105
+ readonly applied: readonly AppliedConvention[];
106
+ readonly globs: readonly ConventionGlob[];
107
+ readonly generated: readonly GeneratedDirectory[];
108
+ readonly failures: readonly ConventionFailure[];
109
+ }
110
+
111
+ /** One file a row roots. */
112
+ interface ConventionEntry {
113
+ readonly file: SourceFile;
114
+ /** The row's name. */
115
+ readonly row: string;
116
+ }
117
+
118
+ /** Parses one file's text with no program. */
119
+ type ParseSource = (fileName: string, text: string) => SourceFile;
120
+
121
+ /** The sections an installed package is required by, so its absence fails the run. */
122
+ const REQUIRED_SECTIONS = ["dependencies", "devDependencies"] as const;
123
+
124
+ const DEPENDENCY_SECTIONS = [
125
+ ...REQUIRED_SECTIONS,
126
+ "peerDependencies",
127
+ "optionalDependencies",
128
+ ] as const;
129
+
130
+ function isRecord(value: unknown): value is Record<string, unknown> {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ }
133
+
134
+ function readJSON(host: Host, path: string): unknown {
135
+ try {
136
+ return JSON.parse(host.readFile(path));
137
+ } catch {
138
+ return undefined;
139
+ }
140
+ }
141
+
142
+ function declares(
143
+ manifest: unknown,
144
+ name: string,
145
+ sections: readonly string[] = DEPENDENCY_SECTIONS,
146
+ ): boolean {
147
+ if (!isRecord(manifest)) {
148
+ return false;
149
+ }
150
+ return sections.some((section) => {
151
+ const held = manifest[section];
152
+ return isRecord(held) && Object.hasOwn(held, name);
153
+ });
154
+ }
155
+
156
+ /**
157
+ * The version the installed package's manifest names, read from the nearest
158
+ * `node_modules` at or above `dir` that holds it, `""` where that manifest names
159
+ * none, or `undefined` where no `node_modules` holds it.
160
+ */
161
+ function installedVersion(host: Host, dir: string, name: string): string | undefined {
162
+ for (let at = normalizePath(dir); ; at = dirnamePath(at)) {
163
+ const path = joinPath(at, "node_modules", name, "package.json");
164
+ if (host.kindOf(path) === "file") {
165
+ const manifest = readJSON(host, path);
166
+ const version = isRecord(manifest) ? manifest["version"] : undefined;
167
+ return typeof version === "string" ? version : "";
168
+ }
169
+ if (dirnamePath(at) === at) {
170
+ return undefined;
171
+ }
172
+ }
173
+ }
174
+
175
+ /** Whether one node wraps an expression without changing its value. */
176
+ function isWrapper(
177
+ node: Node,
178
+ ): node is ParenthesizedExpression | AsExpression | SatisfiesExpression | NonNullExpression {
179
+ return (
180
+ isParenthesizedExpression(node) ||
181
+ isAsExpression(node) ||
182
+ isSatisfiesExpression(node) ||
183
+ isNonNullExpression(node)
184
+ );
185
+ }
186
+
187
+ /** The expression a wrapper that changes no value holds. */
188
+ function unwrapped(node: Node): Node {
189
+ let at = node;
190
+ while (isWrapper(at)) {
191
+ at = at.expression;
192
+ }
193
+ return at;
194
+ }
195
+
196
+ /** The key one property is written with, where it is written as a name or as literal text. */
197
+ function keyOf(property: Node): string | undefined {
198
+ if (!isPropertyAssignment(property) && !isShorthandPropertyAssignment(property)) {
199
+ return undefined;
200
+ }
201
+ const { name } = property;
202
+ const key = isComputedPropertyName(name) ? name.expression : name;
203
+ if (
204
+ (isIdentifier(key) && key === name) ||
205
+ isStringLiteral(key) ||
206
+ isNoSubstitutionTemplateLiteral(key)
207
+ ) {
208
+ return key.text;
209
+ }
210
+ return undefined;
211
+ }
212
+
213
+ /** Whether one member's computed name is no literal, so it may set any key. */
214
+ function computesKey(member: Node): boolean {
215
+ const name = (member as { readonly name?: Node }).name;
216
+ if (name === undefined || !isComputedPropertyName(name)) {
217
+ return false;
218
+ }
219
+ const key = name.expression;
220
+ return !(isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key));
221
+ }
222
+
223
+ /** The values one file's top-level variable declarations bind, by name. */
224
+ function declaredValues(file: SourceFile): ReadonlyMap<string, Node> {
225
+ const values = new Map<string, Node>();
226
+ for (const statement of file.statements) {
227
+ if (isVariableStatement(statement)) {
228
+ for (const declaration of statement.declarationList.declarations) {
229
+ if (isIdentifier(declaration.name) && declaration.initializer !== undefined) {
230
+ values.set(declaration.name.text, declaration.initializer);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ return values;
236
+ }
237
+
238
+ function isModuleExports(node: Node): boolean {
239
+ return (
240
+ isPropertyAccessExpression(node) &&
241
+ isIdentifier(node.expression) &&
242
+ node.expression.text === "module" &&
243
+ node.name.text === "exports"
244
+ );
245
+ }
246
+
247
+ /** Every expression or declaration one file exports as its default, in any module syntax. */
248
+ function defaultExports(file: SourceFile): readonly Node[] {
249
+ const found: Node[] = [];
250
+ for (const statement of file.statements) {
251
+ if (isExportAssignment(statement)) {
252
+ found.push(statement.expression);
253
+ } else if (
254
+ (isFunctionDeclaration(statement) || isClassDeclaration(statement)) &&
255
+ statement.modifiers?.some((one) => one.kind === SyntaxKind.DefaultKeyword) === true
256
+ ) {
257
+ found.push(statement);
258
+ } else if (
259
+ isExportDeclaration(statement) &&
260
+ statement.moduleSpecifier === undefined &&
261
+ statement.exportClause !== undefined &&
262
+ isNamedExports(statement.exportClause)
263
+ ) {
264
+ for (const element of statement.exportClause.elements) {
265
+ if (element.name.text === "default") {
266
+ found.push(element.propertyName ?? element.name);
267
+ }
268
+ }
269
+ } else if (
270
+ isExpressionStatement(statement) &&
271
+ isBinaryExpression(statement.expression) &&
272
+ statement.expression.operatorToken.kind === SyntaxKind.EqualsToken &&
273
+ isModuleExports(statement.expression.left)
274
+ ) {
275
+ found.push(statement.expression.right);
276
+ }
277
+ }
278
+ return found;
279
+ }
280
+
281
+ /** The declarations whose name is a binding of the scope they are written in. */
282
+ const BINDING_KINDS: ReadonlySet<SyntaxKind> = new Set([
283
+ SyntaxKind.VariableDeclaration,
284
+ SyntaxKind.Parameter,
285
+ SyntaxKind.BindingElement,
286
+ SyntaxKind.FunctionDeclaration,
287
+ SyntaxKind.ClassDeclaration,
288
+ SyntaxKind.ImportClause,
289
+ SyntaxKind.ImportSpecifier,
290
+ SyntaxKind.NamespaceImport,
291
+ SyntaxKind.ImportEqualsDeclaration,
292
+ ]);
293
+
294
+ /** Whether one file declares a binding named `require` at any depth, which shadows the global. */
295
+ function declaresRequire(file: SourceFile): boolean {
296
+ let found = false;
297
+ const visit = (node: Node): void => {
298
+ const name = (node as { readonly name?: Node }).name;
299
+ if (
300
+ name !== undefined &&
301
+ isIdentifier(name) &&
302
+ name.text === "require" &&
303
+ BINDING_KINDS.has(node.kind)
304
+ ) {
305
+ found = true;
306
+ }
307
+ if (!found) {
308
+ node.forEachChild(visit);
309
+ }
310
+ };
311
+ file.forEachChild(visit);
312
+ return found;
313
+ }
314
+
315
+ /**
316
+ * The top-level `const`, `let` and `var` declarations that bind a `require` of one module,
317
+ * written with the module's name as its one string-literal argument, in a file that
318
+ * declares no binding named `require`: each binds as an import of the module does.
319
+ */
320
+ function requireBindings(
321
+ file: SourceFile,
322
+ module: string,
323
+ ): readonly (Identifier | ObjectBindingPattern)[] {
324
+ const found: (Identifier | ObjectBindingPattern)[] = [];
325
+ for (const statement of file.statements) {
326
+ if (!isVariableStatement(statement)) {
327
+ continue;
328
+ }
329
+ for (const declaration of statement.declarationList.declarations) {
330
+ const value =
331
+ declaration.initializer === undefined ? undefined : unwrapped(declaration.initializer);
332
+ const [argument, ...rest] =
333
+ value !== undefined && isCallExpression(value) ? value.arguments : [];
334
+ if (
335
+ value === undefined ||
336
+ !isCallExpression(value) ||
337
+ !isIdentifier(value.expression) ||
338
+ value.expression.text !== "require" ||
339
+ argument === undefined ||
340
+ rest.length > 0 ||
341
+ !isStringLiteral(argument) ||
342
+ argument.text !== module
343
+ ) {
344
+ continue;
345
+ }
346
+ if (isIdentifier(declaration.name) || isObjectBindingPattern(declaration.name)) {
347
+ found.push(declaration.name);
348
+ }
349
+ }
350
+ }
351
+ return found.length > 0 && declaresRequire(file) ? [] : found;
352
+ }
353
+
354
+ /**
355
+ * The first argument of every call one file makes to one module's export, through a
356
+ * named or a namespace import; `undefined` for a call with no argument.
357
+ */
358
+ function optionsArguments(file: SourceFile, call: OptionsCall): readonly (Node | undefined)[] {
359
+ const names = new Set<string>();
360
+ const namespaces = new Set<string>();
361
+ for (const name of requireBindings(file, call.module)) {
362
+ if (isIdentifier(name)) {
363
+ namespaces.add(name.text);
364
+ continue;
365
+ }
366
+ for (const element of name.elements) {
367
+ const local = element.name;
368
+ const exported = element.propertyName ?? local;
369
+ if (
370
+ element.dotDotDotToken === undefined &&
371
+ local !== undefined &&
372
+ isIdentifier(local) &&
373
+ exported !== undefined &&
374
+ (isIdentifier(exported) || isStringLiteral(exported)) &&
375
+ exported.text === call.export
376
+ ) {
377
+ names.add(local.text);
378
+ }
379
+ }
380
+ }
381
+ for (const statement of file.statements) {
382
+ const bindings = isImportDeclaration(statement)
383
+ ? statement.importClause?.namedBindings
384
+ : undefined;
385
+ if (
386
+ bindings === undefined ||
387
+ !isImportDeclaration(statement) ||
388
+ !isStringLiteral(statement.moduleSpecifier) ||
389
+ statement.moduleSpecifier.text !== call.module
390
+ ) {
391
+ continue;
392
+ }
393
+ if (isNamespaceImport(bindings)) {
394
+ namespaces.add(bindings.name.text);
395
+ continue;
396
+ }
397
+ for (const element of bindings.elements) {
398
+ if ((element.propertyName ?? element.name).text === call.export) {
399
+ names.add(element.name.text);
400
+ }
401
+ }
402
+ }
403
+ const found: (Node | undefined)[] = [];
404
+ const visit = (node: Node): void => {
405
+ if (isCallExpression(node)) {
406
+ const callee = unwrapped(node.expression);
407
+ const named = isIdentifier(callee) && names.has(callee.text);
408
+ const throughNamespace =
409
+ isPropertyAccessExpression(callee) &&
410
+ isIdentifier(callee.expression) &&
411
+ namespaces.has(callee.expression.text) &&
412
+ callee.name.text === call.export;
413
+ if (named || throughNamespace) {
414
+ found.push(node.arguments[0]);
415
+ }
416
+ }
417
+ node.forEachChild(visit);
418
+ };
419
+ file.forEachChild(visit);
420
+ return found;
421
+ }
422
+
423
+ /**
424
+ * The object literal one expression stands for, read through the file's top-level
425
+ * declarations and, where `throughCalls`, through a call's first argument: `undefined`
426
+ * for a call with no argument, and any other node where the expression is not written
427
+ * out as an object literal.
428
+ */
429
+ function objectOf(
430
+ node: Node,
431
+ declared: ReadonlyMap<string, Node>,
432
+ throughCalls: boolean,
433
+ seen: ReadonlySet<string> = new Set(),
434
+ ): Node | undefined {
435
+ const at = unwrapped(node);
436
+ if (isIdentifier(at)) {
437
+ const value = declared.get(at.text);
438
+ if (value !== undefined && !seen.has(at.text)) {
439
+ return objectOf(value, declared, throughCalls, new Set([...seen, at.text]));
440
+ }
441
+ } else if (throughCalls && isCallExpression(at)) {
442
+ const [first] = at.arguments;
443
+ return first === undefined ? undefined : objectOf(first, declared, throughCalls, seen);
444
+ }
445
+ return at;
446
+ }
447
+
448
+ /**
449
+ * Reads the values at one path of keys below one object literal into `values`, and
450
+ * answers the first member that does not write its value out: a spread, a value that is
451
+ * not a literal at the path's end, or one that is not an object literal before it.
452
+ */
453
+ function readPath(
454
+ object: ObjectLiteralExpression,
455
+ keys: readonly string[],
456
+ values: string[],
457
+ ): Node | undefined {
458
+ const [key, ...rest] = keys;
459
+ for (const member of object.properties) {
460
+ if (isSpreadAssignment(member) || computesKey(member)) {
461
+ return member;
462
+ }
463
+ if (keyOf(member) !== key) {
464
+ continue;
465
+ }
466
+ const held = isPropertyAssignment(member) ? unwrapped(member.initializer) : undefined;
467
+ if (rest.length === 0) {
468
+ if (held === undefined || !(isStringLiteral(held) || isNoSubstitutionTemplateLiteral(held))) {
469
+ return member;
470
+ }
471
+ values.push(held.text);
472
+ } else {
473
+ if (held === undefined || !isObjectLiteralExpression(held)) {
474
+ return member;
475
+ }
476
+ const failed = readPath(held, rest, values);
477
+ if (failed !== undefined) {
478
+ return failed;
479
+ }
480
+ }
481
+ }
482
+ return undefined;
483
+ }
484
+
485
+ /** What one configuration file states about one property. */
486
+ interface PropertyReading {
487
+ /** The literal values the file sets the property to. */
488
+ readonly values: readonly string[];
489
+ /** The position of the first value that is not a literal, where there is one. */
490
+ readonly notLiteral?: Node;
491
+ }
492
+
493
+ /**
494
+ * Every value one file writes at one property's path inside the object it is read from:
495
+ * the options argument of every call `call` names, or the file's default export. A string
496
+ * literal or a template literal with no substitution is the value. A read object, or an
497
+ * object along the path, that is not written out as an object literal, or that spreads
498
+ * another object into itself, is not a literal, and neither is any other value at the path.
499
+ */
500
+ export function readProperty(
501
+ file: SourceFile,
502
+ property: string,
503
+ call?: OptionsCall,
504
+ ): PropertyReading {
505
+ const declared = declaredValues(file);
506
+ const objects =
507
+ call === undefined
508
+ ? defaultExports(file).map((one) => objectOf(one, declared, true))
509
+ : optionsArguments(file, call).map((one) =>
510
+ one === undefined ? undefined : objectOf(one, declared, false),
511
+ );
512
+ const values: string[] = [];
513
+ for (const object of objects) {
514
+ if (object === undefined) {
515
+ continue;
516
+ }
517
+ const notLiteral = isObjectLiteralExpression(object)
518
+ ? readPath(object, property.split("."), values)
519
+ : object;
520
+ if (notLiteral !== undefined) {
521
+ return { values, notLiteral };
522
+ }
523
+ }
524
+ return { values };
525
+ }
526
+
527
+ /** A directory below a manifest's, `.` for the directory itself, `undefined` outside it. */
528
+ function below(dir: string, path: string): string | undefined {
529
+ return normalizePath(path) === normalizePath(dir) ? "." : relativePath(dir, path);
530
+ }
531
+
532
+ /**
533
+ * Every text a template stands for, each placeholder replaced by every directory its move
534
+ * resolved to. A placeholder whose move resolved outside the manifest's directory leaves
535
+ * the text standing for nothing.
536
+ */
537
+ function expand(
538
+ template: string,
539
+ resolved: ReadonlyMap<string, readonly (string | undefined)[]>,
540
+ ): readonly string[] {
541
+ const match = /<([A-Za-z]+)>/u.exec(template);
542
+ if (match === null) {
543
+ return [template];
544
+ }
545
+ const [placeholder, id = ""] = match;
546
+ const out: string[] = [];
547
+ for (const value of resolved.get(id) ?? []) {
548
+ if (value === undefined) {
549
+ continue;
550
+ }
551
+ const replaced =
552
+ value === "." && template.startsWith(`${placeholder}/`, match.index)
553
+ ? template.replace(`${placeholder}/`, "")
554
+ : template.replace(placeholder, value);
555
+ out.push(...expand(replaced, resolved));
556
+ }
557
+ return out;
558
+ }
559
+
560
+ /** The configuration files in one directory that one glob names, by name. */
561
+ function configurationFiles(host: Host, dir: string, glob: string): readonly string[] {
562
+ const expression = globExpression(glob);
563
+ let entries;
564
+ try {
565
+ entries = host.readDirectory(dir);
566
+ } catch {
567
+ return [];
568
+ }
569
+ return entries
570
+ .filter((entry) => !entry.directory && expression.test(entry.name))
571
+ .map((entry) => entry.name)
572
+ .sort();
573
+ }
574
+
575
+ /** What one move resolved to: its directories, or the failure its property met. */
576
+ type MoveResult = { readonly dirs: readonly (string | undefined)[] } | { readonly failure: string };
577
+
578
+ function resolveMove(
579
+ host: Host,
580
+ parse: ParseSource,
581
+ dir: string,
582
+ row: ConventionRow,
583
+ move: DirectoryMove,
584
+ resolved: ReadonlyMap<string, readonly (string | undefined)[]>,
585
+ ): MoveResult {
586
+ const set: string[] = [];
587
+ for (const reading of move.readings) {
588
+ for (const glob of reading.files) {
589
+ for (const name of configurationFiles(host, dir, glob)) {
590
+ const path = joinPath(dir, name);
591
+ let text: string;
592
+ try {
593
+ text = host.readFile(path);
594
+ } catch {
595
+ continue;
596
+ }
597
+ const file = parse(path, text);
598
+ const read = readProperty(file, reading.property, reading.call);
599
+ if (read.notLiteral !== undefined) {
600
+ const line = lineOf(text, read.notLiteral.getStart());
601
+ return {
602
+ failure:
603
+ `${name}:${String(line)} sets ${reading.property}, which moves a directory ` +
604
+ `the convention row ${row.name} reads, to a value that is not a string literal: ` +
605
+ `write it as a literal, or name ${row.name} in ts.disabled_conventions and its ` +
606
+ `files in ts.entry_files`,
607
+ };
608
+ }
609
+ set.push(...read.values);
610
+ }
611
+ }
612
+ }
613
+ const dirs: (string | undefined)[] = [];
614
+ for (const base of expand(move.base ?? ".", resolved)) {
615
+ const against = joinPath(dir, base);
616
+ const texts = set.length > 0 ? set : move.defaults.flatMap((one) => expand(one, resolved));
617
+ for (const text of texts) {
618
+ dirs.push(below(dir, resolvePath(against, text)));
619
+ }
620
+ }
621
+ return { dirs: [...new Set(dirs)] };
622
+ }
623
+
624
+ /** Two strings ordered bytewise. */
625
+ function compare(a: string, b: string): number {
626
+ if (a === b) {
627
+ return 0;
628
+ }
629
+ return a < b ? -1 : 1;
630
+ }
631
+
632
+ /**
633
+ * What the rows decide over the manifests the analysis reads. A row `disabled` names
634
+ * is skipped before anything is read for it.
635
+ */
636
+ export function readConventions(
637
+ host: Host,
638
+ parse: ParseSource,
639
+ manifests: readonly ConventionManifest[],
640
+ disabled: readonly string[],
641
+ rows: readonly ConventionRow[] = CONVENTION_ROWS,
642
+ ): Conventions {
643
+ const applied: AppliedConvention[] = [];
644
+ const globs: ConventionGlob[] = [];
645
+ const generated: GeneratedDirectory[] = [];
646
+ const failures: ConventionFailure[] = [];
647
+ for (const manifest of manifests) {
648
+ const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
649
+ const versions = new Map<string, string | undefined>();
650
+ const missing = new Set<string>();
651
+ for (const row of rows) {
652
+ if (disabled.includes(row.name) || !declares(declared, row.package)) {
653
+ continue;
654
+ }
655
+ if (!versions.has(row.package)) {
656
+ versions.set(row.package, installedVersion(host, manifest.dir, row.package));
657
+ }
658
+ const version = versions.get(row.package);
659
+ if (version === undefined) {
660
+ if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
661
+ continue;
662
+ }
663
+ missing.add(row.package);
664
+ failures.push({
665
+ dir: manifest.dir,
666
+ failure: {
667
+ setupClass: "missing-module",
668
+ detail:
669
+ `${manifest.path} declares ${row.package}, which no node_modules at or ` +
670
+ `above its directory holds: run the package manager's install`,
671
+ },
672
+ });
673
+ continue;
674
+ }
675
+ if (!satisfies(version, row.range)) {
676
+ continue;
677
+ }
678
+ const resolved = new Map<string, readonly (string | undefined)[]>();
679
+ let failed: string | undefined;
680
+ for (const move of row.moves) {
681
+ const result = resolveMove(host, parse, manifest.dir, row, move, resolved);
682
+ if ("failure" in result) {
683
+ failed = result.failure;
684
+ break;
685
+ }
686
+ resolved.set(move.id, result.dirs);
687
+ }
688
+ if (failed !== undefined) {
689
+ const where = dirnamePath(manifest.path);
690
+ failures.push({
691
+ dir: manifest.dir,
692
+ failure: {
693
+ setupClass: "convention-not-literal",
694
+ detail: where === "." ? failed : `${where}/${failed}`,
695
+ },
696
+ });
697
+ continue;
698
+ }
699
+ applied.push({
700
+ name: row.name,
701
+ package: row.package,
702
+ version,
703
+ manifest: manifest.path,
704
+ });
705
+ const excludes = [
706
+ ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
707
+ ].map(globExpression);
708
+ for (const below of new Set((row.generated ?? []).flatMap((one) => expand(one, resolved)))) {
709
+ generated.push({ dir: manifest.dir, row: row.name, below });
710
+ }
711
+ for (const glob of new Set(row.entries.flatMap((entry) => expand(entry, resolved)))) {
712
+ globs.push({
713
+ dir: manifest.dir,
714
+ row: row.name,
715
+ expression: globExpression(glob),
716
+ excludes,
717
+ });
718
+ }
719
+ }
720
+ }
721
+ const unique = [...new Map(applied.map((one) => [JSON.stringify(one), one])).values()];
722
+ unique.sort(
723
+ (a, b) =>
724
+ compare(a.name, b.name) ||
725
+ compare(a.manifest, b.manifest) ||
726
+ compare(JSON.stringify(a), JSON.stringify(b)),
727
+ );
728
+ return { applied: unique, globs, generated, failures };
729
+ }
730
+
731
+ /**
732
+ * Every own file of one project below a directory an applied row names as generated, by
733
+ * its path below the target root, with the name of the first such row.
734
+ */
735
+ export function generatedFiles(
736
+ conventions: Conventions,
737
+ files: SourceFiles,
738
+ ): ReadonlyMap<string, string> {
739
+ const found = new Map<string, string>();
740
+ for (const { dir, row, below } of conventions.generated) {
741
+ const root = joinPath(dir, below);
742
+ for (const [path, file] of files.byPath) {
743
+ if (!found.has(path) && relativePath(root, file.fileName) !== undefined) {
744
+ found.set(path, row);
745
+ }
746
+ }
747
+ }
748
+ return found;
749
+ }
750
+
751
+ /** Every own file of one project an applied row's globs match, once per row. */
752
+ export function conventionEntries(
753
+ conventions: Conventions,
754
+ files: SourceFiles,
755
+ ): readonly ConventionEntry[] {
756
+ const found = new Map<string, ConventionEntry>();
757
+ for (const glob of conventions.globs) {
758
+ for (const file of files.byName.values()) {
759
+ const path = relativePath(glob.dir, file.fileName);
760
+ if (
761
+ path !== undefined &&
762
+ glob.expression.test(path) &&
763
+ !glob.excludes.some((one) => one.test(path))
764
+ ) {
765
+ found.set(`${file.fileName}\u0000${glob.row}`, { file, row: glob.row });
766
+ }
767
+ }
768
+ }
769
+ return [...found.values()];
770
+ }
771
+
772
+ /**
773
+ * The failures the rows met that hold for one configuration: those of a manifest whose
774
+ * directory holds the configuration file or one of the files the project owns.
775
+ */
776
+ export function conventionFailuresFor(
777
+ conventions: Conventions,
778
+ configFile: string,
779
+ ownFiles: readonly string[],
780
+ ): readonly SetupFailure[] {
781
+ return conventions.failures
782
+ .filter(
783
+ ({ dir }) =>
784
+ below(dir, dirnamePath(configFile)) !== undefined ||
785
+ ownFiles.some((file) => relativePath(dir, file) !== undefined),
786
+ )
787
+ .map(({ failure }) => failure);
788
+ }