@cplieger/deadset-ts 5.2.3 → 6.0.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 (84) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +28 -5
  3. package/dist/src/augmentations.js +50 -0
  4. package/dist/src/components.js +15 -15
  5. package/dist/src/conformance-record.js +2 -2
  6. package/dist/src/convention-rows.js +17 -2
  7. package/dist/src/conventions.js +82 -1
  8. package/dist/src/dependencies.js +42 -26
  9. package/dist/src/entry-points.js +28 -14
  10. package/dist/src/exempt.js +1 -0
  11. package/dist/src/findings/completion.js +9 -4
  12. package/dist/src/findings/dependencies-and-module-machinery.js +2 -2
  13. package/dist/src/findings/emitters.js +64 -25
  14. package/dist/src/findings/interfaces.js +4 -3
  15. package/dist/src/findings/intra-function.js +15 -2
  16. package/dist/src/findings/reads-and-writes.js +3 -2
  17. package/dist/src/findings/unused-declarations.js +13 -1
  18. package/dist/src/findings/visibility-narrowing.js +2 -1
  19. package/dist/src/findings-pass.js +1 -0
  20. package/dist/src/graph.js +6 -6
  21. package/dist/src/implementations.js +1 -1
  22. package/dist/src/indexed-access.js +215 -0
  23. package/dist/src/interface-satisfaction.js +85 -23
  24. package/dist/src/intra-function-parts.js +139 -30
  25. package/dist/src/inventory.js +6 -1
  26. package/dist/src/kinds.js +13 -12
  27. package/dist/src/matrix.js +34 -9
  28. package/dist/src/module-declarations.js +180 -0
  29. package/dist/src/outside-tests.js +17 -15
  30. package/dist/src/overrides.js +70 -9
  31. package/dist/src/query.js +7 -0
  32. package/dist/src/ref.js +9 -0
  33. package/dist/src/references.js +6 -0
  34. package/dist/src/report.js +6 -1
  35. package/dist/src/reporters/sarif.js +6 -2
  36. package/dist/src/reporters/text.js +23 -3
  37. package/dist/src/roots.js +50 -10
  38. package/dist/src/suppress-file.js +3 -5
  39. package/dist/src/version.js +1 -1
  40. package/dist/src/workflow-steps.js +82 -0
  41. package/dist/src/workspace-programs.js +6 -31
  42. package/jsr.json +1 -1
  43. package/package.json +8 -2
  44. package/src/analysis.ts +40 -4
  45. package/src/augmentations.ts +72 -0
  46. package/src/components.ts +15 -13
  47. package/src/conformance-record.ts +2 -2
  48. package/src/convention-rows.ts +25 -3
  49. package/src/conventions.ts +106 -2
  50. package/src/dependencies.ts +51 -31
  51. package/src/entry-points.ts +32 -19
  52. package/src/exempt.ts +4 -1
  53. package/src/findings/completion.ts +11 -4
  54. package/src/findings/dependencies-and-module-machinery.ts +2 -2
  55. package/src/findings/emitters.ts +89 -27
  56. package/src/findings/interfaces.ts +7 -6
  57. package/src/findings/intra-function.ts +20 -2
  58. package/src/findings/reads-and-writes.ts +7 -2
  59. package/src/findings/unused-declarations.ts +16 -1
  60. package/src/findings/visibility-narrowing.ts +2 -1
  61. package/src/findings-pass.ts +4 -1
  62. package/src/graph.ts +6 -6
  63. package/src/implementations.ts +1 -1
  64. package/src/indexed-access.ts +295 -0
  65. package/src/interface-satisfaction.ts +118 -27
  66. package/src/intra-function-parts.ts +196 -35
  67. package/src/inventory.ts +8 -0
  68. package/src/kinds.ts +14 -12
  69. package/src/matrix.ts +51 -8
  70. package/src/module-declarations.ts +241 -0
  71. package/src/outside-tests.ts +18 -16
  72. package/src/overrides.ts +80 -14
  73. package/src/query.ts +28 -0
  74. package/src/ref.ts +10 -0
  75. package/src/references.ts +6 -0
  76. package/src/report.ts +11 -1
  77. package/src/reporters/sarif.ts +9 -2
  78. package/src/reporters/text.ts +25 -3
  79. package/src/roots.ts +56 -7
  80. package/src/suppress-file.ts +3 -6
  81. package/src/sweep.ts +12 -0
  82. package/src/version.ts +1 -1
  83. package/src/workflow-steps.ts +104 -0
  84. package/src/workspace-programs.ts +6 -35
@@ -7,6 +7,7 @@
7
7
  import { jsonChunks } from "./json-chunks.js";
8
8
  import { joinPath } from "./paths.js";
9
9
  import { positionAt, walkDocument } from "./json-document.js";
10
+ import { declarationKey } from "./ref.js";
10
11
  import { CODE_FORM, SUPPRESSION_WITHOUT_REASON, SuppressionError, UNSCOPED_ENTRY, blankReason, isEntryPath, isSymbolRef, siteText, } from "./suppress.js";
11
12
  /** The name and the location the grammar fixes for the ignore file. */
12
13
  export const IGNORE_FILE = "deadset-ignore.json";
@@ -38,9 +39,6 @@ function documentSite(shape) {
38
39
  function isObject(value) {
39
40
  return typeof value === "object" && value !== null && !Array.isArray(value);
40
41
  }
41
- function referenceKey(ref, path) {
42
- return `${ref}\u0000${path}`;
43
- }
44
42
  /**
45
43
  * Reads one entry or row: the records it binds, the refusals it carries, or the
46
44
  * malformed value that ends the run.
@@ -100,7 +98,7 @@ function readRecord(value, site, byReference, shape) {
100
98
  if (refusals.length > 0) {
101
99
  return { records: [], refusals };
102
100
  }
103
- const named = byReference.get(referenceKey(symbol, written.path)) ?? [];
101
+ const named = byReference.get(declarationKey(symbol, written.path)) ?? [];
104
102
  if (named.length === 0) {
105
103
  return { records: [{ ...written, bound: "" }], refusals: [] };
106
104
  }
@@ -109,7 +107,7 @@ function readRecord(value, site, byReference, shape) {
109
107
  function byReferenceOf(symbols) {
110
108
  const byReference = new Map();
111
109
  for (const symbol of symbols) {
112
- const key = referenceKey(symbol.ref, symbol.position.path);
110
+ const key = declarationKey(symbol.ref, symbol.position.path);
113
111
  byReference.set(key, [...(byReference.get(key) ?? []), symbol]);
114
112
  }
115
113
  return byReference;
@@ -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 = "5.3.0";
7
+ export const CONTRACT_VERSION = "6.0.0";
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The source files a CI workflow's `run:` steps name by their path from the target root.
3
+ * A workflow file is read as text, line by line, with no YAML parsed.
4
+ */
5
+ import { scriptTokens } from "./manifest.js";
6
+ import { joinPath, resolvePath } from "./paths.js";
7
+ const WORKFLOWS = ".github/workflows";
8
+ /** A line holding a run command: indentation and one sequence marker before `run:`. */
9
+ const RUN_LINE = /^(\s*(?:- )?)run:(.*)$/u;
10
+ const BLOCK_SCALAR = /^[|>][+-]?$/u;
11
+ const QUOTED = /^(["'])(.*)\1$/su;
12
+ /** A token holding one of these is computed or a pattern, so it names no file. */
13
+ const COMPUTED = /[${*?[]/u;
14
+ const INDENT = /^\s*/u;
15
+ function workflowFiles(host, dir) {
16
+ let entries;
17
+ try {
18
+ entries = host.readDirectory(dir);
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ return entries
24
+ .filter((entry) => !entry.directory && /\.ya?ml$/u.test(entry.name))
25
+ .map((entry) => joinPath(dir, entry.name))
26
+ .sort();
27
+ }
28
+ /** Every command line one workflow file's `run:` steps hold. */
29
+ export function runCommands(text) {
30
+ const lines = text.split(/\r?\n/u);
31
+ const found = [];
32
+ for (let at = 0; at < lines.length; at += 1) {
33
+ const match = RUN_LINE.exec(lines[at] ?? "");
34
+ if (match === null) {
35
+ continue;
36
+ }
37
+ const column = (match[1] ?? "").length;
38
+ const value = (match[2] ?? "").trim().replace(QUOTED, "$2");
39
+ if (!BLOCK_SCALAR.test(value)) {
40
+ found.push(value);
41
+ continue;
42
+ }
43
+ for (let next = at + 1; next < lines.length; next += 1) {
44
+ const line = lines[next] ?? "";
45
+ if (line.trim() !== "" && (INDENT.exec(line)?.[0].length ?? 0) <= column) {
46
+ break;
47
+ }
48
+ found.push(line);
49
+ }
50
+ }
51
+ return found;
52
+ }
53
+ /**
54
+ * The own files the `run:` steps of every workflow file in the target root's
55
+ * `.github/workflows` name: a token equal to a file's path below the target root, with or
56
+ * without a leading `./`, whatever directory the step runs in.
57
+ */
58
+ export function workflowEntries(host, targetRoot, files) {
59
+ const dir = joinPath(resolvePath(host.workingDirectory(), targetRoot), WORKFLOWS);
60
+ const found = [];
61
+ for (const path of workflowFiles(host, dir)) {
62
+ let text;
63
+ try {
64
+ text = host.readFile(path);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ for (const command of runCommands(text)) {
70
+ for (const token of scriptTokens(command)) {
71
+ if (COMPUTED.test(token)) {
72
+ continue;
73
+ }
74
+ const file = files.byPath.get(token.startsWith("./") ? token.slice(2) : token);
75
+ if (file !== undefined) {
76
+ found.push({ file, source: token });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return found;
82
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { ModuleKind, ModuleResolutionKind } from "@typescript/native/unstable/sync";
9
9
  import { isStringLiteralLikeNode } from "@typescript/native/unstable/ast";
10
- import { dirnamePath, joinPath, resolvePath } from "./paths.js";
10
+ import { dirnamePath, resolvePath } from "./paths.js";
11
11
  /** The most questions one round trip carries. */
12
12
  const BATCH = 2048;
13
13
  /**
@@ -20,31 +20,6 @@ function analysisOptions(parsed) {
20
20
  const { composite, rootDir: _rootDir, outDir: _outDir, declarationDir: _declarationDir, tsBuildInfoFile: _tsBuildInfoFile, ...kept } = parsed.options;
21
21
  return composite === true ? { ...kept, declaration: true } : kept;
22
22
  }
23
- /**
24
- * The references a built program can carry: only where every referenced
25
- * configuration reads and is composite with output, because the compiler reports a
26
- * broken reference at syntax a built program does not have.
27
- */
28
- function carriedReferences(engine, parsed) {
29
- const references = parsed.projectReferences ?? [];
30
- for (const reference of references) {
31
- // A reference that names a directory names the `tsconfig.json` in it.
32
- const configFile = reference.path.endsWith(".json")
33
- ? reference.path
34
- : joinPath(reference.path, "tsconfig.json");
35
- let options;
36
- try {
37
- options = engine.parseConfigFile(configFile).options;
38
- }
39
- catch {
40
- return [];
41
- }
42
- if (options.composite !== true || options.noEmit === true) {
43
- return [];
44
- }
45
- }
46
- return references;
47
- }
48
23
  /** The conditions an `exports` lookup under one configuration and one resolution mode reads. */
49
24
  function conditionsOf(parsed, mode) {
50
25
  const resolution = parsed.options.moduleResolution;
@@ -94,7 +69,7 @@ function configurationOf(engine, host, reading) {
94
69
  if (reason !== undefined) {
95
70
  return { kind: "unread", reason };
96
71
  }
97
- const { options } = reading.parsed;
72
+ const { options, projectReferences: references = [] } = reading.parsed;
98
73
  const raw = options["pathsBasePath"];
99
74
  const base = typeof raw === "string" ? raw : dirnamePath(reading.configFile);
100
75
  const paths = {};
@@ -117,8 +92,8 @@ function configurationOf(engine, host, reading) {
117
92
  ...(options.composite === true ? { composite: false, declaration: true } : {}),
118
93
  paths,
119
94
  },
120
- ...(reading.references.length > 0
121
- ? { references: reading.references.map((reference) => ({ path: reference.path })) }
95
+ ...(references.length > 0
96
+ ? { references: references.map((reference) => ({ path: reference.path })) }
122
97
  : {}),
123
98
  ...(exclude === undefined ? {} : { exclude }),
124
99
  });
@@ -150,6 +125,7 @@ function configurationOf(engine, host, reading) {
150
125
  /** The program one configuration is built as, under the answers it holds so far. */
151
126
  function programOf(engine, reading) {
152
127
  const compilerOptions = analysisOptions(reading.parsed);
128
+ const references = reading.parsed.projectReferences ?? [];
153
129
  const resolver = engine.createModuleResolver(compilerOptions, reading.entries);
154
130
  return {
155
131
  resolver,
@@ -159,7 +135,7 @@ function programOf(engine, reading) {
159
135
  options: {
160
136
  moduleResolver: resolver,
161
137
  configFileParsingDiagnostics: reading.parsed.errors,
162
- ...(reading.references.length > 0 ? { projectReferences: reading.references } : {}),
138
+ ...(references.length > 0 ? { projectReferences: references } : {}),
163
139
  },
164
140
  },
165
141
  };
@@ -292,7 +268,6 @@ export function workspacePrograms(engine, host, configFiles, resolver) {
292
268
  {
293
269
  configFile,
294
270
  parsed,
295
- references: carriedReferences(engine, parsed),
296
271
  entries: [],
297
272
  seen: new Set(),
298
273
  bound: new Map(),
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.2.3",
3
+ "version": "6.0.1",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "5.2.3",
3
+ "version": "6.0.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -29,10 +29,16 @@
29
29
  "lint:fix": "eslint . --fix && prettier --write ."
30
30
  },
31
31
  "dependencies": {
32
- "@typescript/native": "npm:typescript@7.1.0-dev.20261006.1"
32
+ "@typescript/native": "npm:typescript@7.1.0-dev.20261007.1"
33
+ },
34
+ "overrides": {
35
+ "qs": "^6.15.2"
33
36
  },
34
37
  "devDependencies": {
35
38
  "@eslint/js": "10.0.1",
39
+ "@stryker-mutator/core": "10.0.0",
40
+ "@stryker-mutator/typescript-checker": "10.0.0",
41
+ "@stryker-mutator/vitest-runner": "10.0.0",
36
42
  "@types/node": "24.19.1",
37
43
  "eslint": "10.12.0",
38
44
  "fast-check": "4.10.2",
package/src/analysis.ts CHANGED
@@ -71,7 +71,7 @@ import { frameworkLifecycle } from "./framework-lifecycle.ts";
71
71
  import type { Host } from "./host.ts";
72
72
  import { implementations, mergeImplementations } from "./implementations.ts";
73
73
  import { injectionContainer } from "./injection-container.ts";
74
- import { interfaceSatisfaction } from "./interface-satisfaction.ts";
74
+ import { interfaceSatisfaction, settledSatisfaction } from "./interface-satisfaction.ts";
75
75
  import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-function-parts.ts";
76
76
  import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
77
77
  import { packageEntries, readManifest } from "./manifest.ts";
@@ -224,6 +224,8 @@ interface ReadProjects<Answer> {
224
224
  readonly conventionsApplied: readonly AppliedConvention[];
225
225
  /** The dependencies the applied rows' short names use. */
226
226
  readonly conventionUses: readonly string[];
227
+ /** The directory of every workspace member, absolute. */
228
+ readonly memberDirs: readonly string[];
227
229
  /** The test files below the target root no project the run reads holds, parsed. */
228
230
  readonly outsideTests: readonly OutsideTest[];
229
231
  }
@@ -508,6 +510,7 @@ function readProjects<Answer>(
508
510
  componentsUnread: workspace?.componentsUnread ?? [],
509
511
  conventionsApplied: conventions.applied,
510
512
  conventionUses: conventions.uses,
513
+ memberDirs: workspace?.memberDirs ?? [],
511
514
  outsideTests,
512
515
  };
513
516
  }
@@ -694,6 +697,8 @@ interface ReadRun<Extra> {
694
697
  readonly conventionsApplied: readonly AppliedConvention[];
695
698
  /** The dependencies the applied rows' short names use. */
696
699
  readonly conventionUses: readonly string[];
700
+ /** The directory of every workspace member, absolute. */
701
+ readonly memberDirs: readonly string[];
697
702
  }
698
703
 
699
704
  /**
@@ -777,15 +782,20 @@ function readRun<Extra>(
777
782
  withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability),
778
783
  withOutside.flatMap((one) => one.testFiles),
779
784
  );
780
- const answers = withOutside.map((one): SweptProject<Extra> => {
785
+ const settled = settledSatisfaction(
786
+ withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })),
787
+ [...withOutside.flatMap((one) => one.references), ...read.consumers.references],
788
+ );
789
+ const answers = withOutside.map((one, index): SweptProject<Extra> => {
781
790
  const { projectRead } = one;
782
- const detected = heldRecords(one.evidence, {
791
+ const satisfied = settled[index];
792
+ const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
783
793
  disabled,
784
794
  mode,
785
795
  testFiles: new Set([...one.testFiles, ...support]),
786
796
  });
787
797
  const exempt = detected.records;
788
- const made = supportReferences(one.references, support);
798
+ const made = supportReferences([...one.references, ...(satisfied?.references ?? [])], support);
789
799
  const held = new Set(projectRead.held.symbols.map((symbol) => symbol.position.path));
790
800
  const supportFiles = [...support].filter((path) => held.has(path)).sort();
791
801
  const configured: Configured = {
@@ -846,6 +856,7 @@ function readRun<Extra>(
846
856
  componentsUnread: read.componentsUnread,
847
857
  conventionsApplied: read.conventionsApplied,
848
858
  conventionUses: read.conventionUses,
859
+ memberDirs: read.memberDirs,
849
860
  };
850
861
  }
851
862
 
@@ -1070,6 +1081,30 @@ function readEmitterRun(
1070
1081
  : { ...run, unansweredHeld: [...new Set([...run.unansweredHeld, ...unread])] };
1071
1082
  }
1072
1083
 
1084
+ /**
1085
+ * The workspace members whose manifest the run reads whole enough to judge its
1086
+ * dependencies: a built project holds a file below the member's directory, and no derived
1087
+ * configuration at or below it was dropped. A file no built project holds may import any
1088
+ * dependency the member declares.
1089
+ */
1090
+ function membersWhollyRead(
1091
+ host: Host,
1092
+ targetRoot: string,
1093
+ read: ReadRun<EmitterExtra>,
1094
+ ): readonly string[] {
1095
+ const root = resolvePath(host.workingDirectory(), targetRoot);
1096
+ const held = new Set(read.matrix.union.symbols.map((symbol) => symbol.position.path));
1097
+ const dropped = read.notBuilt.map((one) => relativePath(root, one.configFile) ?? "");
1098
+ return read.memberDirs.filter((dir) => {
1099
+ const below = relativePath(root, dir);
1100
+ if (below === undefined) {
1101
+ return false;
1102
+ }
1103
+ const inside = (path: string): boolean => path.startsWith(`${below}/`);
1104
+ return [...held].some(inside) && !dropped.some(inside);
1105
+ });
1106
+ }
1107
+
1073
1108
  /**
1074
1109
  * The input every emitter reads over one sweep of a read run: the facts beside the sweep
1075
1110
  * each kind family reads, every per-project one read in the same session as the sweep.
@@ -1102,6 +1137,7 @@ function emitterInputOver(
1102
1137
  targetRoot,
1103
1138
  [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)],
1104
1139
  swept.sweep.candidates.map((candidate) => candidate.id),
1140
+ membersWhollyRead(host, targetRoot, read),
1105
1141
  ),
1106
1142
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
1107
1143
  files: {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The references a use of an augmentation's declaration makes to what holds it. No name
3
+ * spells a `declare global` block, so a use of a declaration it holds is a use of the
4
+ * block. An interface or namespace an augmentation block holds merges with the one of its
5
+ * name, which the language or a library may type a value by where the target never names
6
+ * it, as it types `import.meta` or `process.env`, so a use of its member is a use of it.
7
+ */
8
+
9
+ import type { Inventory, InventorySymbol } from "./inventory.ts";
10
+ import type { Reference } from "./references.ts";
11
+
12
+ /** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
13
+ function isGlobalBlock(
14
+ symbol: InventorySymbol,
15
+ held: Inventory,
16
+ files: ReadonlySet<string>,
17
+ ): boolean {
18
+ return (
19
+ symbol.kind === "namespace" &&
20
+ files.has(symbol.parent) &&
21
+ symbol.ref.endsWith("#global") &&
22
+ !held.modules.has(symbol.id)
23
+ );
24
+ }
25
+
26
+ /**
27
+ * For each reference to a declaration an augmentation block holds, a read at the same place
28
+ * of every interface and namespace between it and the block, and of a `declare global` block
29
+ * itself.
30
+ */
31
+ export function augmentationReferences(
32
+ held: Inventory,
33
+ found: readonly Reference[],
34
+ ): readonly Reference[] {
35
+ const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
36
+ const globals = new Set(
37
+ held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id),
38
+ );
39
+ if (globals.size === 0 && held.modules.size === 0) {
40
+ return [];
41
+ }
42
+ const byId = new Map(held.symbols.map((one) => [one.id, one]));
43
+ const holders = (id: string): string[] => {
44
+ const ids: string[] = [];
45
+ for (
46
+ let at = byId.get(byId.get(id)?.parent ?? "");
47
+ at !== undefined;
48
+ at = byId.get(at.parent)
49
+ ) {
50
+ if (globals.has(at.id)) {
51
+ return [...ids, at.id];
52
+ }
53
+ if (held.modules.has(at.id)) {
54
+ return ids;
55
+ }
56
+ if (at.kind === "interface" || at.kind === "namespace") {
57
+ ids.push(at.id);
58
+ }
59
+ }
60
+ return [];
61
+ };
62
+ return found.flatMap((reference) =>
63
+ holders(reference.to).map((to) => ({
64
+ from: reference.from,
65
+ to,
66
+ position: reference.position,
67
+ use: "read" as const,
68
+ resolution: reference.resolution,
69
+ test: reference.test,
70
+ })),
71
+ );
72
+ }
package/src/components.ts CHANGED
@@ -62,17 +62,18 @@ export function listing(component: Component, cascade: Cascade): Listing {
62
62
 
63
63
  /**
64
64
  * Groups the declarations `dead` marks into components, ordered by their first member's
65
- * site; `dead` and `testOfDeadCode` hold one flag per declaration of the graph. Root
66
- * members sit on the cycles no other cycle references. A cycle two roots both reach is
67
- * dead only through both, so a component is a weakly connected part of the dead
68
- * subgraph: no dead declaration of one references a declaration of another.
65
+ * site; `dead`, `testOfDeadCode` and `unreferencedTest` hold one flag per declaration of
66
+ * the graph. Root members sit on the cycles no other cycle references. A cycle two roots
67
+ * both reach is dead only through both, so a component is a weakly connected part of the
68
+ * dead subgraph: no dead declaration of one references a declaration of another.
69
69
  */
70
70
  export function componentsOf(
71
71
  graph: Graph,
72
72
  dead: readonly boolean[],
73
73
  testOfDeadCode: readonly boolean[],
74
+ unreferencedTest: readonly boolean[] = [],
74
75
  ): readonly Component[] {
75
- const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode);
76
+ const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest);
76
77
  if (at.length === 0) {
77
78
  return [];
78
79
  }
@@ -133,21 +134,22 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
133
134
 
134
135
  /**
135
136
  * The dead declarations by their place in the subgraph, a test file's declaration only
136
- * where it is a test of dead code, and the edges between them: each
137
- * reference one makes to another, an edge each way between a dead member and its dead
138
- * container (one direction alone leaves the member a component the container reaches),
139
- * and an edge back from each production target of an admitted test, which puts the test in
140
- * the component of the code it exercises because a subject never references its test.
137
+ * where it is a test of dead code or nothing the tests reach, and the edges between them:
138
+ * each reference one makes to another, except an unreferenced test-file declaration's
139
+ * reference to production code, which decides nothing of its target, an edge each way
140
+ * between a dead member and its dead container, and an edge back from each production
141
+ * target of an admitted test, which puts the test in its subject's component.
141
142
  */
142
143
  function deadSubgraph(
143
144
  graph: Graph,
144
145
  dead: readonly boolean[],
145
146
  testOfDeadCode: readonly boolean[],
147
+ unreferencedTest: readonly boolean[],
146
148
  ): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
147
149
  const at: number[] = [];
148
150
  const position = graph.symbols.map((_symbol, index) => {
149
- // A test file's declaration belongs to a component only as a test of dead code.
150
- if (dead[index] !== true || (graph.test[index] === true && testOfDeadCode[index] !== true)) {
151
+ const judged = testOfDeadCode[index] === true || unreferencedTest[index] === true;
152
+ if (dead[index] !== true || (graph.test[index] === true && !judged)) {
151
153
  return UNVISITED;
152
154
  }
153
155
  at.push(index);
@@ -157,7 +159,7 @@ function deadSubgraph(
157
159
  at.forEach((index, from) => {
158
160
  for (const edge of graph.out[index] ?? []) {
159
161
  const to = position[edge.to] ?? UNVISITED;
160
- if (to === UNVISITED) {
162
+ if (to === UNVISITED || (unreferencedTest[index] === true && graph.test[edge.to] !== true)) {
161
163
  continue;
162
164
  }
163
165
  adjacent[from]?.push(to);
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "3.0.0",
5
+ corpusVersion: "4.0.0",
6
6
  result: "pass",
7
- digest: "sha256:db52e3164347cab70f45f438b4ac9ce89573be9303503daeb237a3c40361b001",
7
+ digest: "sha256:8250b61deb71467139bf4cbcaf10802cdabb1a2c2261a11a571fc1ff55bfc3dc",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -16,7 +16,7 @@ export interface OptionsCall {
16
16
  }
17
17
 
18
18
  /** One configuration property, and the configuration files it is read from. */
19
- interface MoveReading {
19
+ export interface MoveReading {
20
20
  /** The property's path through nested object literals, its keys joined by `.`. */
21
21
  readonly property: string;
22
22
  /** Globs naming the configuration files, matched in the manifest's directory alone. */
@@ -64,6 +64,13 @@ export interface ConventionRow {
64
64
  readonly moves: readonly DirectoryMove[];
65
65
  /** The properties that name a package the configuration uses by a short name. */
66
66
  readonly shortNames?: readonly ShortNameReading[];
67
+ /**
68
+ * The properties whose value only selects files to test, mutate, cover or ignore, so a
69
+ * string there names no file the tool runs or loads.
70
+ */
71
+ readonly selections?: readonly MoveReading[];
72
+ /** The members of a `package.json` the tool reads its configuration from. */
73
+ readonly manifestKeys?: readonly string[];
67
74
  }
68
75
 
69
76
  const SCRIPT = "{js,jsx,ts,tsx}";
@@ -99,6 +106,8 @@ const SOLIDSTART_PLUGIN = { module: "@solidjs/start/config", export: "solidStart
99
106
  const NUXT_CONFIG = [`nuxt.config.${ANY_MODULE}`];
100
107
 
101
108
  const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
109
+ const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
110
+ const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
102
111
 
103
112
  /** Every row, ordered by name and then by range. */
104
113
  export const CONVENTION_ROWS: readonly ConventionRow[] = [
@@ -214,6 +223,14 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
214
223
  },
215
224
  ],
216
225
  },
226
+ {
227
+ name: "prettier",
228
+ package: "prettier",
229
+ range: ">=3.0.0 <4.0.0",
230
+ entries: [`{prettier.config,.prettierrc}.${ANY_MODULE}`],
231
+ moves: [],
232
+ manifestKeys: ["prettier"],
233
+ },
217
234
  {
218
235
  name: "qwik-city",
219
236
  package: "@builder.io/qwik-city",
@@ -331,8 +348,9 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
331
348
  name: "stryker",
332
349
  package: "@stryker-mutator/core",
333
350
  range: ">=7.0.0 <11.0.0",
334
- entries: [],
351
+ entries: [STRYKER_MODULE],
335
352
  moves: [],
353
+ selections: [{ property: "mutate", files: [STRYKER_CONFIG] }],
336
354
  shortNames: [
337
355
  {
338
356
  property: "testRunner",
@@ -476,9 +494,13 @@ export const CONVENTION_ROWS: readonly ConventionRow[] = [
476
494
  shortNames: [
477
495
  {
478
496
  property: "test.coverage.provider",
479
- files: [`{vitest,vite}.config.${ANY_MODULE}`],
497
+ files: [VITEST_CONFIG],
480
498
  package: "@vitest/coverage-{}",
481
499
  },
482
500
  ],
501
+ selections: [
502
+ { property: "test.coverage.include", files: [VITEST_CONFIG] },
503
+ { property: "test.coverage.exclude", files: [VITEST_CONFIG] },
504
+ ],
483
505
  },
484
506
  ];