@cplieger/deadset-ts 6.1.0 → 7.0.0-dev.1

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 (47) hide show
  1. package/dist/src/analysis.js +39 -16
  2. package/dist/src/conformance-record.js +2 -2
  3. package/dist/src/contextual-uses.js +27 -3
  4. package/dist/src/convention-rows.js +42 -0
  5. package/dist/src/conventions.js +48 -0
  6. package/dist/src/dependencies.js +109 -38
  7. package/dist/src/findings/completion.js +6 -7
  8. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  9. package/dist/src/html-entries.js +29 -14
  10. package/dist/src/indexed-access.js +371 -81
  11. package/dist/src/intra-function-parts.js +57 -11
  12. package/dist/src/kinds.js +6 -6
  13. package/dist/src/references.js +60 -21
  14. package/dist/src/roots.js +96 -6
  15. package/dist/src/session.js +6 -1
  16. package/dist/src/source-files.js +1 -0
  17. package/dist/src/sweep.js +29 -17
  18. package/dist/src/symbol-keys.js +114 -0
  19. package/dist/src/test-joins.js +101 -0
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/workflow-steps.js +21 -15
  22. package/dist/src/workspace-programs.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis.ts +45 -17
  26. package/src/conformance-record.ts +2 -2
  27. package/src/contextual-uses.ts +36 -4
  28. package/src/convention-rows.ts +57 -0
  29. package/src/conventions.ts +59 -0
  30. package/src/dependencies.ts +134 -46
  31. package/src/finding.ts +2 -0
  32. package/src/findings/completion.ts +7 -7
  33. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  34. package/src/html-entries.ts +33 -15
  35. package/src/indexed-access.ts +468 -102
  36. package/src/intra-function-parts.ts +84 -12
  37. package/src/kinds.ts +7 -6
  38. package/src/references.ts +86 -27
  39. package/src/roots.ts +129 -6
  40. package/src/session.ts +7 -1
  41. package/src/source-files.ts +3 -0
  42. package/src/sweep.ts +29 -17
  43. package/src/symbol-keys.ts +172 -0
  44. package/src/test-joins.ts +120 -0
  45. package/src/version.ts +1 -1
  46. package/src/workflow-steps.ts +23 -16
  47. package/src/workspace-programs.ts +1 -1
package/src/roots.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  isQualifiedName,
16
16
  isTypeQueryNode,
17
17
  isTypeReferenceNode,
18
+ isPropertyDeclaration,
19
+ isVariableDeclaration,
18
20
  isVariableStatement,
19
21
  type Node,
20
22
  type SourceFile,
@@ -31,6 +33,7 @@ import { relativePath, resolvePath } from "./paths.ts";
31
33
  import { byPosition } from "./position.ts";
32
34
  import { UNANSWERED } from "./query.ts";
33
35
  import type { ProjectView } from "./session.ts";
36
+ import type { Symbol as TSSymbol, Type } from "@typescript/native/unstable/sync";
34
37
  import { sourceFilesOf } from "./source-files.ts";
35
38
  import { typeQueryAliases } from "./type-query-alias.ts";
36
39
 
@@ -128,6 +131,26 @@ export interface RootOptions {
128
131
  readonly parse?: ParseFile | undefined;
129
132
  /** Whether the target is a library, so a consumer outside it reaches its published API. */
130
133
  readonly publishedAPI: boolean;
134
+ /**
135
+ * The directories of the workspace members whose manifest is not private, each a library
136
+ * inside the target whose entries are published API whatever the target's kind.
137
+ */
138
+ readonly publishedMembers?: ReadonlySet<string> | undefined;
139
+ }
140
+
141
+ /** Whether one declaration is written below the directory of a published workspace member. */
142
+ function inPublishedMember(
143
+ held: Inventory,
144
+ id: string,
145
+ root: string,
146
+ members: ReadonlySet<string> | undefined,
147
+ ): boolean {
148
+ const path = held.symbols.find((symbol) => symbol.id === id)?.position.path;
149
+ if (path === undefined || members === undefined) {
150
+ return false;
151
+ }
152
+ const file = resolvePath(root, path);
153
+ return [...members].some((dir) => file.startsWith(`${dir}/`));
131
154
  }
132
155
 
133
156
  /** The kind of root each role of a manifest entry makes. */
@@ -305,10 +328,16 @@ export function roots<Brand>(
305
328
  ...options.manifest.entries.map((entry) => ({
306
329
  entry,
307
330
  publishes:
331
+ options.publishedAPI &&
308
332
  entry.published &&
309
333
  (options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
310
334
  })),
311
- ...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
335
+ ...(options.otherPackages ?? []).map((entry) => ({
336
+ entry,
337
+ publishes:
338
+ entry.published &&
339
+ (options.publishedAPI || options.publishedMembers?.has(entry.dir) === true),
340
+ })),
312
341
  ];
313
342
  const scripted = entries.flatMap(({ entry }) =>
314
343
  entry.role === "script" ? files.named(entry.path) : [],
@@ -324,7 +353,7 @@ export function roots<Brand>(
324
353
  : readBackByName(options.host, entry, readBackTo);
325
354
  };
326
355
  for (const { entry, publishes } of entries) {
327
- if (options.publishedAPI && publishes) {
356
+ if (publishes) {
328
357
  published.push(...entryFiles(entry));
329
358
  }
330
359
  }
@@ -380,9 +409,14 @@ export function roots<Brand>(
380
409
  }
381
410
  }
382
411
 
383
- const api = options.publishedAPI
384
- ? publishedAPI(project, published, exportsOf, held, files.byPath, releaseTagged(held, files))
385
- : { ids: [], guessed: [] };
412
+ const tagged = releaseTagged(held, files).filter(
413
+ (id) =>
414
+ options.publishedAPI || inPublishedMember(held, id, targetRoot, options.publishedMembers),
415
+ );
416
+ const api =
417
+ published.length > 0 || tagged.length > 0
418
+ ? publishedAPI(project, published, exportsOf, held, files.byPath, tagged)
419
+ : { ids: [], guessed: [] };
386
420
  for (const id of api.ids) {
387
421
  add(id, "published-api", "");
388
422
  }
@@ -560,6 +594,18 @@ function publishedAPI<Brand>(
560
594
  const symbol = byId.get(id);
561
595
  return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
562
596
  });
597
+ for (const symbol of inferredTypeSymbols(
598
+ project,
599
+ round.flatMap((id) => {
600
+ const symbol = byId.get(id);
601
+ const node = symbol === undefined ? undefined : nodes.nodeOf(symbol);
602
+ return node === undefined ? [] : [node];
603
+ }),
604
+ )) {
605
+ for (const target of chains.chainOf(symbol)) {
606
+ reach(target.id);
607
+ }
608
+ }
563
609
  project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
564
610
  if (symbol === UNANSWERED) {
565
611
  for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
@@ -580,7 +626,10 @@ function publishedAPI<Brand>(
580
626
  function declarationNodes(
581
627
  held: Inventory,
582
628
  byPath: ReadonlyMap<string, SourceFile>,
583
- ): { typeNamesOf(symbol: InventorySymbol): readonly Node[] } {
629
+ ): {
630
+ typeNamesOf(symbol: InventorySymbol): readonly Node[];
631
+ nodeOf(symbol: InventorySymbol): Node | undefined;
632
+ } {
584
633
  const walked = new Map<string, { byId: Map<string, Node>; nodes: Set<Node> }>();
585
634
  const nodesIn = (path: string): { byId: Map<string, Node>; nodes: Set<Node> } => {
586
635
  const known = walked.get(path);
@@ -611,7 +660,81 @@ function declarationNodes(
611
660
  const declaration = byId.get(symbol.id);
612
661
  return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
613
662
  },
663
+ nodeOf: (symbol) => nodesIn(symbol.position.path).byId.get(symbol.id),
664
+ };
665
+ }
666
+
667
+ /** How deep the type arguments and constituents of an inferred type are followed. */
668
+ const INFERRED_DEPTH = 4;
669
+
670
+ /**
671
+ * The declarations of the types the compiler infers where a declaration writes none: the
672
+ * type of a variable or a property with no annotation, and the result of a function or a
673
+ * method with no result annotation, each with the types its type arguments and its union
674
+ * or intersection constituents name, to a fixed depth. A question the checker leaves
675
+ * unanswered names nothing.
676
+ */
677
+ function inferredTypeSymbols<Brand>(
678
+ project: ProjectView<Brand>,
679
+ declarations: readonly Node[],
680
+ ): readonly TSSymbol[] {
681
+ const { queries } = project;
682
+ const valued = declarations.flatMap((node) =>
683
+ (isVariableDeclaration(node) || isPropertyDeclaration(node)) &&
684
+ node.type === undefined &&
685
+ !isFunctionLikeDeclaration(node.initializer ?? node)
686
+ ? [node.name]
687
+ : [],
688
+ );
689
+ const types: Type[] = [];
690
+ queries.typesAt(valued).forEach((type) => {
691
+ if (type !== UNANSWERED && type !== undefined) {
692
+ types.push(type);
693
+ }
694
+ });
695
+ for (const node of declarations) {
696
+ const signature = isFunctionLikeDeclaration(node)
697
+ ? node
698
+ : isVariableDeclaration(node) &&
699
+ node.type === undefined &&
700
+ node.initializer !== undefined &&
701
+ isFunctionLikeDeclaration(node.initializer)
702
+ ? node.initializer
703
+ : undefined;
704
+ if (signature === undefined || signature.type !== undefined || signature.body === undefined) {
705
+ continue;
706
+ }
707
+ const typed = queries.signatureOf(signature);
708
+ const result =
709
+ typed === UNANSWERED || typed === undefined ? undefined : queries.returnTypeOf(typed);
710
+ if (result !== UNANSWERED && result !== undefined) {
711
+ types.push(result);
712
+ }
713
+ }
714
+ const found = new Map<number, TSSymbol>();
715
+ const seen = new Set<Type>();
716
+ const visit = (type: Type, depth: number): void => {
717
+ if (depth > INFERRED_DEPTH || seen.has(type)) {
718
+ return;
719
+ }
720
+ seen.add(type);
721
+ const symbol = queries.symbolOfType(type);
722
+ if (symbol !== UNANSWERED && symbol !== undefined) {
723
+ found.set(symbol.id, symbol);
724
+ }
725
+ const parts = queries.constituents(type);
726
+ const args = queries.typeArguments(type);
727
+ for (const next of [
728
+ ...(parts === UNANSWERED || (parts.length === 1 && parts[0] === type) ? [] : parts),
729
+ ...(args === UNANSWERED ? [] : args),
730
+ ]) {
731
+ visit(next, depth + 1);
732
+ }
614
733
  };
734
+ types.forEach((type) => {
735
+ visit(type, 0);
736
+ });
737
+ return [...found.values()];
615
738
  }
616
739
 
617
740
  /**
package/src/session.ts CHANGED
@@ -347,7 +347,13 @@ function viewOf<Brand>(
347
347
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
348
348
  return undefined;
349
349
  }
350
- const held = queries.immediateAliased(symbol);
350
+ // An alias the checker synthesizes declares nothing, the default a module that assigns
351
+ // its export gets under esModuleInterop, and the immediate step fails on it while the
352
+ // whole resolution answers.
353
+ const held =
354
+ symbol.declarations.length === 0
355
+ ? queries.aliased(symbol)
356
+ : queries.immediateAliased(symbol);
351
357
  // An alias that resolves to nothing answers with the checker's own unknown
352
358
  // symbol, which declares nothing; a symbol that is no alias answers with
353
359
  // nothing at all.
@@ -38,6 +38,8 @@ export interface SourceFiles {
38
38
  * nothing.
39
39
  */
40
40
  named(path: string): readonly SourceFile[];
41
+ /** The directories the project writes its output below, absolute: its output and declaration directories. */
42
+ readonly outputDirs: readonly string[];
41
43
  }
42
44
 
43
45
  /**
@@ -76,6 +78,7 @@ export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: st
76
78
  return {
77
79
  byPath,
78
80
  byName,
81
+ outputDirs: [...new Set([outDir, declarationDir])].filter((dir) => dir !== undefined),
79
82
  named: (path) => {
80
83
  if (!path.includes(SUBPATH_WILDCARD)) {
81
84
  for (const candidate of candidates(path)) {
package/src/sweep.ts CHANGED
@@ -278,34 +278,46 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
278
278
  /**
279
279
  * Admits every test declaration whose set of referenced production declarations is
280
280
  * non-empty and wholly dead, and returns which it admitted. The set is read from every
281
- * reference the test makes, not the ones the mode counts: a production sweep counts none,
282
- * which is what makes the targets dead. A test referencing one live target is never
283
- * admitted, and neither is a marked or exempt test.
281
+ * reference the test makes, which a production sweep does not count. A test-support
282
+ * declaration that a test referencing a live target also references is a live target of
283
+ * every test referencing it. A marked or exempt test is never admitted.
284
284
  */
285
285
  function admitTestsOfDeadCode(
286
286
  graph: Graph,
287
287
  dead: boolean[],
288
288
  held: readonly boolean[],
289
289
  ): readonly boolean[] {
290
- const admitted = graph.symbols.map(() => false);
291
- graph.symbols.forEach((_symbol, at) => {
292
- const testCode = graph.test[at] === true || graph.support[at] === true;
293
- if (!testCode || graph.subject[at] !== true || held[at] === true) {
294
- return;
295
- }
290
+ const testCode = (at: number): boolean => graph.test[at] === true || graph.support[at] === true;
291
+ const judged = graph.symbols.map((_symbol, at) => {
296
292
  let targets = 0;
297
293
  let live = 0;
298
- for (const edge of graph.out[at] ?? []) {
299
- const target = graph.test[edge.to] === true || graph.support[edge.to] === true;
300
- if (target || graph.subject[edge.to] !== true) {
301
- continue;
294
+ if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
295
+ for (const edge of graph.out[at] ?? []) {
296
+ if (testCode(edge.to) || graph.subject[edge.to] !== true) {
297
+ continue;
298
+ }
299
+ targets += 1;
300
+ if (dead[edge.to] !== true) {
301
+ live += 1;
302
+ }
302
303
  }
303
- targets += 1;
304
- if (dead[edge.to] !== true) {
305
- live += 1;
304
+ }
305
+ return { targets, live };
306
+ });
307
+ const shared = new Set<number>();
308
+ judged.forEach(({ live }, at) => {
309
+ if (live > 0) {
310
+ for (const edge of graph.out[at] ?? []) {
311
+ if (graph.support[edge.to] === true) {
312
+ shared.add(edge.to);
313
+ }
306
314
  }
307
315
  }
308
- if (targets > 0 && live === 0) {
316
+ });
317
+ const admitted = graph.symbols.map(() => false);
318
+ judged.forEach(({ targets, live }, at) => {
319
+ const sharing = (graph.out[at] ?? []).some((edge) => shared.has(edge.to) && edge.to !== at);
320
+ if (targets > 0 && live === 0 && !sharing) {
309
321
  dead[at] = true;
310
322
  admitted[at] = true;
311
323
  }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The members a unique symbol keys, used through element accesses. `value[key]`, where
3
+ * `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
4
+ * whose computed name is that same constant, and a store through the access writes it.
5
+ * No name node resolves to such a member: the key's name resolves to the constant.
6
+ */
7
+
8
+ import {
9
+ isComputedPropertyName,
10
+ isIdentifier,
11
+ isNoSubstitutionTemplateLiteral,
12
+ isNumericLiteral,
13
+ isParenthesizedExpression,
14
+ isPropertyAccessExpression,
15
+ isStringLiteral,
16
+ type ElementAccessExpression,
17
+ type Node,
18
+ } from "@typescript/native/unstable/ast";
19
+ import type { Symbol as TSSymbol, Type } from "@typescript/native/unstable/sync";
20
+ import type { AliasChains } from "./alias-chain.ts";
21
+ import type { InventorySymbol } from "./inventory.ts";
22
+ import { isAnswered, type PropertyTables } from "./query.ts";
23
+ import type { ProjectView } from "./session.ts";
24
+
25
+ /** One element access whose key is a name, and how the access uses the member it selects. */
26
+ export interface KeyedAccess {
27
+ readonly access: ElementAccessExpression;
28
+ /** The identifier whose symbol is the key: the argument, or the name its property access ends in. */
29
+ readonly key: Node;
30
+ readonly from: string;
31
+ readonly use: "read" | "write" | "both";
32
+ }
33
+
34
+ /** One keyed access with the symbol its key resolved to. */
35
+ interface ResolvedKey {
36
+ readonly access: KeyedAccess;
37
+ readonly symbol: TSSymbol;
38
+ }
39
+
40
+ /** The members one keyed access selects, by their inventory identifiers. */
41
+ interface KeyedUse {
42
+ readonly access: KeyedAccess;
43
+ readonly ids: readonly string[];
44
+ }
45
+
46
+ /** The identifier an element access's argument names its key by, where it is a name. */
47
+ export function keyNameOf(argument: Node): Node | undefined {
48
+ let at = argument;
49
+ while (isParenthesizedExpression(at)) {
50
+ at = at.expression;
51
+ }
52
+ if (isIdentifier(at)) {
53
+ return at;
54
+ }
55
+ return isPropertyAccessExpression(at) && isIdentifier(at.name) ? at.name : undefined;
56
+ }
57
+
58
+ /**
59
+ * The last identifier of each computed member name the inventory holds, `mark` for
60
+ * `[mark]` and `iterator` for `[Symbol.iterator]`: an access whose key is spelled by no
61
+ * such identifier selects no member a unique symbol keys, and is not asked about.
62
+ */
63
+ export function computedKeyNames(symbols: readonly InventorySymbol[]): ReadonlySet<string> {
64
+ const names = new Set<string>();
65
+ for (const symbol of symbols) {
66
+ const key = /\[([^\]]+)\]$/u.exec(symbol.name)?.[1];
67
+ const last = key?.split(".").pop()?.trim();
68
+ if (last !== undefined && /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(last)) {
69
+ names.add(last);
70
+ }
71
+ }
72
+ return names;
73
+ }
74
+
75
+ /** Whether a computed name's expression is literal text, which names a member by its text. */
76
+ function isLiteralKey(expression: Node): boolean {
77
+ return (
78
+ isStringLiteral(expression) ||
79
+ isNumericLiteral(expression) ||
80
+ isNoSubstitutionTemplateLiteral(expression)
81
+ );
82
+ }
83
+
84
+ /**
85
+ * The members each keyed access selects. The receivers' types are asked in one batch,
86
+ * each object type's properties once, and the key of every property declared under a
87
+ * computed name in one batch more; a property is selected where its key and the
88
+ * access's key are one symbol.
89
+ */
90
+ export function keyedUses<Brand>(
91
+ project: ProjectView<Brand>,
92
+ chains: AliasChains,
93
+ resolved: readonly ResolvedKey[],
94
+ properties: PropertyTables,
95
+ cap: number,
96
+ ): readonly KeyedUse[] {
97
+ if (resolved.length === 0) {
98
+ return [];
99
+ }
100
+ const queries = project.queries;
101
+ const ownPaths = project.ownPaths();
102
+ const types = queries.typesAt(
103
+ resolved.map((one) => one.access.access.expression),
104
+ cap,
105
+ );
106
+
107
+ const parts = new Map<number, readonly Type[]>();
108
+ const partsOf = (type: Type): readonly Type[] => {
109
+ const known = parts.get(type.id);
110
+ if (known !== undefined) {
111
+ return known;
112
+ }
113
+ let found: readonly Type[] = [];
114
+ if (type.isUnionType() || type.isIntersectionType()) {
115
+ const constituents = queries.constituents(type);
116
+ found = isAnswered(constituents) ? constituents.flatMap(partsOf) : [];
117
+ } else if (type.isObjectType()) {
118
+ found = [type];
119
+ }
120
+ parts.set(type.id, found);
121
+ return found;
122
+ };
123
+
124
+ /** Each receiver's properties declared in the target under a computed name, with that name's expression. */
125
+ const candidates = new Map<number, { property: TSSymbol; key: Node }[]>();
126
+ const keyNodes: Node[] = [];
127
+ const candidatesOf = (type: Type): readonly { property: TSSymbol; key: Node }[] => {
128
+ const known = candidates.get(type.id);
129
+ if (known !== undefined) {
130
+ return known;
131
+ }
132
+ const found: { property: TSSymbol; key: Node }[] = [];
133
+ const read = properties.of(type);
134
+ for (const property of isAnswered(read.properties) ? read.properties : []) {
135
+ const handle = property.declarations.find((one) => ownPaths.has(one.path));
136
+ const node = handle === undefined ? undefined : project.declarationAt(handle)?.node;
137
+ const name = (node as { readonly name?: Node } | undefined)?.name;
138
+ if (name !== undefined && isComputedPropertyName(name) && !isLiteralKey(name.expression)) {
139
+ found.push({ property, key: name.expression });
140
+ keyNodes.push(name.expression);
141
+ }
142
+ }
143
+ candidates.set(type.id, found);
144
+ return found;
145
+ };
146
+
147
+ const perAccess = resolved.map((_one, index) => {
148
+ const type = types[index];
149
+ return type === undefined || !isAnswered(type) ? [] : partsOf(type).flatMap(candidatesOf);
150
+ });
151
+ const keyOf = new Map<Node, number>();
152
+ const asked = [...new Set(keyNodes)];
153
+ (asked.length === 0
154
+ ? []
155
+ : project.symbolsAt(
156
+ asked.map((node) => project.handle(node)),
157
+ cap,
158
+ )
159
+ ).forEach((symbol, index) => {
160
+ const node = asked[index];
161
+ if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
162
+ keyOf.set(node, symbol.id);
163
+ }
164
+ });
165
+
166
+ return resolved.flatMap((one, index) => {
167
+ const ids = (perAccess[index] ?? [])
168
+ .filter((candidate) => keyOf.get(candidate.key) === one.symbol.id)
169
+ .flatMap((candidate) => chains.declarationsOf(candidate.property));
170
+ return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
171
+ });
172
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The test files a derived matrix holds although no configuration's file list names them.
3
+ * The test runner compiles such a file with the options of the nearest compiler
4
+ * configuration at or above it, so the file joins that configuration's program as a file
5
+ * its list names, unless a `package.json` below that configuration's directory makes the
6
+ * file another package's, which its own test runner compiles.
7
+ */
8
+
9
+ import { sourcePaths } from "./file-facts.ts";
10
+ import type { Host } from "./host.ts";
11
+ import { dirnamePath, joinPath, relativePath } from "./paths.ts";
12
+ import type { Engine } from "./session.ts";
13
+ import { writtenPath, type WorkspaceProgram } from "./workspace-programs.ts";
14
+
15
+ const PREFERRED = "/tsconfig.json";
16
+
17
+ /** The configurations of one directory in the order a file joins them: `tsconfig.json` first. */
18
+ function byPreference(a: string, b: string): number {
19
+ const first = a.endsWith(PREFERRED) ? 0 : 1;
20
+ const second = b.endsWith(PREFERRED) ? 0 : 1;
21
+ return first - second || (a < b ? -1 : a > b ? 1 : 0);
22
+ }
23
+
24
+ /**
25
+ * Per configuration of `configFiles`, the absolute paths of the test files that join it:
26
+ * each file below `root` a pattern of `patterns` names, that no configuration's file list
27
+ * holds, joins the configuration nearest at or above its directory when no `package.json`
28
+ * sits between them. A configuration the compiler cannot read lists nothing and is joined
29
+ * by nothing.
30
+ */
31
+ export function joinedTestFiles(
32
+ engine: Engine,
33
+ host: Host,
34
+ root: string,
35
+ configFiles: readonly string[],
36
+ patterns: readonly RegExp[],
37
+ ): ReadonlyMap<string, readonly string[]> {
38
+ const listed = new Set<string>();
39
+ const byDir = new Map<string, string[]>();
40
+ for (const configFile of configFiles) {
41
+ let fileNames: readonly string[];
42
+ try {
43
+ fileNames = engine.parseConfigFile(configFile).fileNames;
44
+ } catch {
45
+ continue;
46
+ }
47
+ fileNames.forEach((name) => listed.add(name));
48
+ const dir = dirnamePath(configFile);
49
+ byDir.set(dir, [...(byDir.get(dir) ?? []), configFile].sort(byPreference));
50
+ }
51
+ const joins = new Map<string, string[]>();
52
+ for (const path of sourcePaths(host, root)) {
53
+ const file = joinPath(root, path);
54
+ if (listed.has(file) || !patterns.some((pattern) => pattern.test(path))) {
55
+ continue;
56
+ }
57
+ for (let dir = dirnamePath(file); ; dir = dirnamePath(dir)) {
58
+ const [nearest] = byDir.get(dir) ?? [];
59
+ if (nearest !== undefined) {
60
+ joins.set(nearest, [...(joins.get(nearest) ?? []), file]);
61
+ break;
62
+ }
63
+ if (
64
+ host.kindOf(joinPath(dir, "package.json")) === "file" ||
65
+ dir === root ||
66
+ relativePath(root, dir) === undefined
67
+ ) {
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ return joins;
73
+ }
74
+
75
+ /**
76
+ * How the snapshot reads each configuration a test file joins: the program `programs`
77
+ * describes with the files added to its root files, or a configuration only the snapshot
78
+ * holds that extends the one `programs` writes, or the configuration itself, and lists
79
+ * every file the configuration lists and the joined ones.
80
+ */
81
+ export function withJoinedTests(
82
+ engine: Engine,
83
+ host: Host,
84
+ programs: ReadonlyMap<string, WorkspaceProgram>,
85
+ joins: ReadonlyMap<string, readonly string[]>,
86
+ ): ReadonlyMap<string, WorkspaceProgram> {
87
+ const found = new Map(programs);
88
+ for (const [configFile, files] of joins) {
89
+ const program = programs.get(configFile);
90
+ if (program?.kind === "program") {
91
+ found.set(configFile, {
92
+ kind: "program",
93
+ params: {
94
+ ...program.params,
95
+ rootFiles: [...(program.params.rootFiles ?? []), ...files],
96
+ },
97
+ });
98
+ continue;
99
+ }
100
+ let listed: readonly string[];
101
+ try {
102
+ listed =
103
+ program?.kind === "configuration"
104
+ ? engine.parseConfigJson(JSON.parse(program.text), program.file).fileNames
105
+ : engine.parseConfigFile(configFile).fileNames;
106
+ } catch {
107
+ continue;
108
+ }
109
+ const value =
110
+ program?.kind === "configuration"
111
+ ? { ...(JSON.parse(program.text) as Record<string, unknown>), files: [...listed, ...files] }
112
+ : { extends: configFile, files: [...listed, ...files] };
113
+ found.set(configFile, {
114
+ kind: "configuration",
115
+ file: program?.kind === "configuration" ? program.file : writtenPath(host, configFile),
116
+ text: `${JSON.stringify({ ...value, include: [] }, null, 2)}\n`,
117
+ });
118
+ }
119
+ return found;
120
+ }
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "6.1.0";
7
+ export const CONTRACT_VERSION = "7.0.0";
@@ -69,6 +69,24 @@ export function runCommands(text: string): readonly string[] {
69
69
  return found;
70
70
  }
71
71
 
72
+ /** Every token the `run:` steps of the workflow files in the target root's `.github/workflows` hold. */
73
+ export function workflowTokens(host: Host, targetRoot: string): readonly string[] {
74
+ const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
75
+ const found: string[] = [];
76
+ for (const path of workflowFiles(host, dir)) {
77
+ let text: string;
78
+ try {
79
+ text = host.readFile(path);
80
+ } catch {
81
+ continue;
82
+ }
83
+ for (const command of runCommands(text)) {
84
+ found.push(...scriptTokens(command));
85
+ }
86
+ }
87
+ return found;
88
+ }
89
+
72
90
  /**
73
91
  * The own files the `run:` steps of every workflow file in the target root's
74
92
  * `.github/workflows` name: a token equal to a file's path below the target root, with or
@@ -79,25 +97,14 @@ export function workflowEntries(
79
97
  targetRoot: string,
80
98
  files: SourceFiles,
81
99
  ): readonly WorkflowEntry[] {
82
- const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
83
100
  const found: WorkflowEntry[] = [];
84
- for (const path of workflowFiles(host, dir)) {
85
- let text: string;
86
- try {
87
- text = host.readFile(path);
88
- } catch {
101
+ for (const token of workflowTokens(host, targetRoot)) {
102
+ if (COMPUTED.test(token)) {
89
103
  continue;
90
104
  }
91
- for (const command of runCommands(text)) {
92
- for (const token of scriptTokens(command)) {
93
- if (COMPUTED.test(token)) {
94
- continue;
95
- }
96
- const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
97
- if (file !== undefined) {
98
- found.push({ file, source: token });
99
- }
100
- }
105
+ const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
106
+ if (file !== undefined) {
107
+ found.push({ file, source: token });
101
108
  }
102
109
  }
103
110
  return found;
@@ -126,7 +126,7 @@ function unstatable(reading: Reading): string | undefined {
126
126
  }
127
127
 
128
128
  /** The path beside `configFile` its written configuration takes, which no file has. */
129
- function writtenPath(host: Host, configFile: string): string {
129
+ export function writtenPath(host: Host, configFile: string): string {
130
130
  const stem = configFile.replace(/\.json$/u, "");
131
131
  for (let counter = 0; ; counter += 1) {
132
132
  const path = `${stem}.deadset-ts${counter === 0 ? "" : `-${String(counter)}`}.json`;