@cplieger/deadset-ts 3.13.0 → 4.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 (92) hide show
  1. package/README.md +67 -49
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +120 -24
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/configured-declarations.js +22 -9
  8. package/dist/src/dependencies.js +34 -17
  9. package/dist/src/deprecation.js +35 -24
  10. package/dist/src/destructuring.js +37 -29
  11. package/dist/src/discover.js +106 -29
  12. package/dist/src/entry-point-gaps.js +2 -2
  13. package/dist/src/entry-points.js +7 -0
  14. package/dist/src/enum-group.js +46 -40
  15. package/dist/src/exempt.js +42 -1
  16. package/dist/src/findings/emitters.js +15 -1
  17. package/dist/src/findings/interfaces.js +7 -1
  18. package/dist/src/framework-lifecycle.js +8 -6
  19. package/dist/src/implementations.js +45 -23
  20. package/dist/src/interface-satisfaction.js +30 -27
  21. package/dist/src/intra-function-parts.js +37 -20
  22. package/dist/src/inventory.js +85 -4
  23. package/dist/src/json-chunks.js +76 -0
  24. package/dist/src/partial.js +23 -0
  25. package/dist/src/query.js +207 -0
  26. package/dist/src/references.js +92 -20
  27. package/dist/src/reflective-lookup.js +18 -9
  28. package/dist/src/report.js +3 -1
  29. package/dist/src/reporters/annotations.js +9 -5
  30. package/dist/src/reporters/json.js +3 -2
  31. package/dist/src/reporters/sarif.js +2 -1
  32. package/dist/src/reporters/template.js +51 -10
  33. package/dist/src/reporters/text.js +8 -6
  34. package/dist/src/roots.js +27 -11
  35. package/dist/src/schema.js +2 -2
  36. package/dist/src/serialization-contract.js +12 -10
  37. package/dist/src/session.js +79 -39
  38. package/dist/src/suppress-file.js +2 -1
  39. package/dist/src/sweep.js +4 -1
  40. package/dist/src/value-reach.js +14 -19
  41. package/dist/src/verbs/analyze.js +20 -3
  42. package/dist/src/verbs/explain.js +22 -9
  43. package/dist/src/verbs/print-projects.js +41 -8
  44. package/dist/src/verbs/print-retained.js +6 -2
  45. package/dist/src/verbs/print-roots.js +5 -1
  46. package/jsr.json +1 -1
  47. package/package.json +3 -3
  48. package/src/alias-chain.ts +75 -11
  49. package/src/analysis.ts +224 -62
  50. package/src/calls.ts +10 -9
  51. package/src/class-members.ts +10 -11
  52. package/src/configured-declarations.ts +29 -13
  53. package/src/dependencies.ts +42 -20
  54. package/src/deprecation.ts +53 -27
  55. package/src/destructuring.ts +50 -39
  56. package/src/discover.ts +178 -34
  57. package/src/entry-point-gaps.ts +2 -2
  58. package/src/entry-points.ts +7 -0
  59. package/src/enum-group.ts +56 -42
  60. package/src/exempt.ts +50 -2
  61. package/src/findings/emitters.ts +18 -1
  62. package/src/findings/interfaces.ts +7 -1
  63. package/src/framework-lifecycle.ts +10 -6
  64. package/src/host.ts +5 -3
  65. package/src/implementations.ts +59 -23
  66. package/src/interface-satisfaction.ts +30 -27
  67. package/src/intra-function-parts.ts +43 -25
  68. package/src/inventory.ts +97 -4
  69. package/src/json-chunks.ts +87 -0
  70. package/src/partial.ts +40 -0
  71. package/src/query.ts +522 -0
  72. package/src/references.ts +119 -38
  73. package/src/reflective-lookup.ts +21 -11
  74. package/src/report.ts +14 -2
  75. package/src/reporters/annotations.ts +23 -21
  76. package/src/reporters/json.ts +3 -2
  77. package/src/reporters/reporter.ts +2 -2
  78. package/src/reporters/sarif.ts +3 -2
  79. package/src/reporters/template.ts +53 -11
  80. package/src/reporters/text.ts +12 -12
  81. package/src/roots.ts +34 -12
  82. package/src/schema.ts +2 -2
  83. package/src/serialization-contract.ts +12 -10
  84. package/src/session.ts +146 -58
  85. package/src/suppress-file.ts +6 -2
  86. package/src/sweep.ts +9 -4
  87. package/src/value-reach.ts +20 -23
  88. package/src/verbs/analyze.ts +25 -3
  89. package/src/verbs/explain.ts +20 -9
  90. package/src/verbs/print-projects.ts +54 -8
  91. package/src/verbs/print-retained.ts +6 -2
  92. package/src/verbs/print-roots.ts +5 -1
@@ -3,6 +3,7 @@ import { listing } from "../components.js";
3
3
  import { ConfigError } from "../config.js";
4
4
  import { OUTSIDE, swept as judged } from "../graph.js";
5
5
  import { referenceKey } from "../matrix.js";
6
+ import { partialNotes } from "../partial.js";
6
7
  import { positionKey } from "../position.js";
7
8
  import { resolve } from "../resolve.js";
8
9
  import { EXIT_CLEAN, EXIT_USAGE } from "./verb.js";
@@ -282,18 +283,22 @@ function explanation(analysis, subject, cascade) {
282
283
  text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
283
284
  }
284
285
  }
286
+ else if (analysis.swept.heldByUnanswered.includes(subject.id)) {
287
+ text +=
288
+ "answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
289
+ }
285
290
  else {
286
291
  text += liveness(analysis, subject, cascade);
287
292
  }
288
293
  return text + namedElsewhere(analysis.result.findings, subject);
289
294
  }
290
295
  /**
291
- * Explains one symbol from the analysis the report is built from, so an explanation cannot
292
- * disagree with a finding: why a reported symbol is reported, which exemption classes held a
293
- * retained one back, and for any other declaration the relations that hold it live and the
294
- * shortest path of references from a root or a loaded consumer, or the references it has where
295
- * no path reaches it. A request naming no one declaration exits with the usage code and names
296
- * what partially matches it.
296
+ * Explains one symbol from the analysis the report is built from: why a reported symbol is
297
+ * reported, which exemption classes held a retained one back, that an unanswered question held
298
+ * a held one, and for any other declaration the relations that hold it live and the shortest
299
+ * path from a root or a loaded consumer, or the references it has where no path reaches it. A
300
+ * request naming no one declaration exits with the usage code and names what partially
301
+ * matches it. Where the analysis was partial, the error stream says so.
297
302
  */
298
303
  export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
299
304
  const named = namedSymbol(option);
@@ -302,6 +307,10 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
302
307
  production: true,
303
308
  });
304
309
  const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
310
+ const notes = partialNotes({
311
+ notBuilt: analysis.run.notBuilt,
312
+ unanswered: analysis.run.unanswered,
313
+ });
305
314
  if (subject === undefined) {
306
315
  err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
307
316
  for (const candidate of candidates) {
@@ -310,8 +319,12 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
310
319
  if (candidates.length === 0) {
311
320
  err.write(" no symbol of the target partially matches it either\n");
312
321
  }
313
- return EXIT_USAGE;
314
322
  }
315
- out.write(explanation(analysis, subject, config.reporters.cascade));
316
- return EXIT_CLEAN;
323
+ else {
324
+ out.write(explanation(analysis, subject, config.reporters.cascade));
325
+ }
326
+ for (const note of notes) {
327
+ err.write(`deadset-ts: ${note}\n`);
328
+ }
329
+ return subject === undefined ? EXIT_USAGE : EXIT_CLEAN;
317
330
  };
@@ -1,4 +1,6 @@
1
- import { diagnosticErrors, discoverProjects, renderDiagnostic } from "../discover.js";
1
+ import { discoverProjects, DiscoveryError, isGuess, projectErrors, renderDiagnostic, targetRelative, } from "../discover.js";
2
+ import { partialNotes } from "../partial.js";
3
+ import { relativePath, resolvePath } from "../paths.js";
2
4
  import { resolveMatrix } from "../resolve.js";
3
5
  import { diagnosticsOf, runSession } from "../session.js";
4
6
  import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
@@ -6,7 +8,8 @@ import { EXIT_CLEAN, EXIT_FAILURE } from "./verb.js";
6
8
  * Writes the name of every project one run analyzes, one per line, having read each
7
9
  * project's diagnostics first: the identifier the build matrix declares for it, or,
8
10
  * where discovery derived the project, its configuration file's path below the
9
- * target root.
11
+ * target root. A derived project that does not load is not one the run analyzes, so it
12
+ * is named on the error stream instead, as the analysis drops it.
10
13
  */
11
14
  export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient }) => {
12
15
  const matrix = resolveMatrix(inputs());
@@ -14,21 +17,51 @@ export const printProjectsVerb = ({ out, err, host, inputs, scope, openClient })
14
17
  const engine = openClient(false);
15
18
  const discovered = discoverProjects(engine, host, scoped, matrix);
16
19
  const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
20
+ const root = resolvePath(host.workingDirectory(), scoped.target.path);
21
+ const notBuilt = [...discovered.notBuilt];
17
22
  const failures = [];
18
- const { projects } = runSession(engine, discovered.configFiles, (project) => {
19
- const errors = diagnosticErrors(diagnosticsOf(project));
20
- failures.push(...errors.map(renderDiagnostic));
21
- return ids.get(project.configFile) ?? project.configFile;
22
- });
23
+ const session = runSession(engine, discovered.configFiles, (project) => {
24
+ const id = ids.get(project.configFile) ?? project.configFile;
25
+ const errors = projectErrors(diagnosticsOf(project));
26
+ const [unloaded] = errors.load;
27
+ if (unloaded !== undefined && isGuess(discovered, project.configFile)) {
28
+ const error = renderDiagnostic(unloaded, (path) => relativePath(root, path) ?? path);
29
+ notBuilt.push({ id, configFile: project.configFile, error: targetRelative(error, root) });
30
+ return undefined;
31
+ }
32
+ failures.push(...[...errors.load, ...errors.check].map((one) => renderDiagnostic(one)));
33
+ return id;
34
+ }, discovered.derived ? "record" : "refuse");
35
+ for (const configFile of session.unopened) {
36
+ if (!isGuess(discovered, configFile)) {
37
+ throw new DiscoveryError(`${configFile} opened no project`, []);
38
+ }
39
+ notBuilt.push({
40
+ id: ids.get(configFile) ?? configFile,
41
+ configFile,
42
+ error: "the compiler opened no project for the configuration",
43
+ });
44
+ }
23
45
  if (failures.length > 0) {
24
46
  for (const line of failures) {
25
47
  err.write(`${line}\n`);
26
48
  }
27
- err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
49
+ err.write(`deadset-ts: ${String(failures.length)} error(s), so no answer was produced\n`);
50
+ return EXIT_FAILURE;
51
+ }
52
+ const projects = session.projects.filter((id) => id !== undefined);
53
+ if (projects.length === 0 && notBuilt.length > 0) {
54
+ for (const note of partialNotes({ notBuilt, unanswered: [] })) {
55
+ err.write(`deadset-ts: ${note}\n`);
56
+ }
57
+ err.write("deadset-ts: no configuration discovery derived could be built\n");
28
58
  return EXIT_FAILURE;
29
59
  }
30
60
  for (const id of projects) {
31
61
  out.write(`${id}\n`);
32
62
  }
63
+ for (const note of partialNotes({ notBuilt, unanswered: [] })) {
64
+ err.write(`deadset-ts: ${note}\n`);
65
+ }
33
66
  return EXIT_CLEAN;
34
67
  };
@@ -1,5 +1,6 @@
1
1
  import { runSweep } from "../analysis.js";
2
2
  import { retainedLines } from "../exempt.js";
3
+ import { partialNotes } from "../partial.js";
3
4
  import { resolve } from "../resolve.js";
4
5
  import { EXIT_CLEAN } from "./verb.js";
5
6
  /**
@@ -7,9 +8,9 @@ import { EXIT_CLEAN } from "./verb.js";
7
8
  * built from, one per line, and nothing where no exemption held one back. One fact
8
9
  * several configurations found is one record, so the line names no configuration. The
9
10
  * line shape is this command's own rather than a Contract format, so that sort and cut
10
- * read it.
11
+ * read it. Where the analysis was partial, the error stream says so.
11
12
  */
12
- export const printRetainedVerb = ({ out, host, inputs, scope, openClient }) => {
13
+ export const printRetainedVerb = ({ out, err, host, inputs, scope, openClient }) => {
13
14
  const { config } = resolve(inputs());
14
15
  const scoped = scope();
15
16
  const swept = runSweep(openClient(false), host, scoped, config, {
@@ -19,5 +20,8 @@ export const printRetainedVerb = ({ out, host, inputs, scope, openClient }) => {
19
20
  for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
20
21
  out.write(`${line}\n`);
21
22
  }
23
+ for (const note of partialNotes({ notBuilt: swept.notBuilt, unanswered: swept.unanswered })) {
24
+ err.write(`deadset-ts: ${note}\n`);
25
+ }
22
26
  return EXIT_CLEAN;
23
27
  };
@@ -1,4 +1,5 @@
1
1
  import { runRoots } from "../analysis.js";
2
+ import { partialNotes } from "../partial.js";
2
3
  import { resolve } from "../resolve.js";
3
4
  import { EXIT_CLEAN, EXIT_FINDINGS } from "./verb.js";
4
5
  /**
@@ -18,7 +19,7 @@ function rootLine(root, several) {
18
19
  /**
19
20
  * Writes the root set of the run, one root per line, and then names on the error
20
21
  * stream, by its issue kind, every configured root or pattern that named nothing;
21
- * one such string fails the run. The line shape is this command's own rather than a
22
+ * one such string fails the run, and then where the analysis was partial. The line shape is this command's own rather than a
22
23
  * Contract format, so that sort and cut read it.
23
24
  */
24
25
  export const printRootsVerb = ({ out, err, host, inputs, scope, openClient }) => {
@@ -32,5 +33,8 @@ export const printRootsVerb = ({ out, err, host, inputs, scope, openClient }) =>
32
33
  for (const finding of answer.findings) {
33
34
  err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
34
35
  }
36
+ for (const note of partialNotes({ notBuilt: answer.notBuilt, unanswered: answer.unanswered })) {
37
+ err.write(`deadset-ts: ${note}\n`);
38
+ }
35
39
  return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
36
40
  };
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.13.0",
3
+ "version": "4.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": "3.13.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "eslint . --fix && prettier --write ."
29
29
  },
30
30
  "dependencies": {
31
- "@typescript/native": "npm:typescript@7.0.2"
31
+ "@typescript/native": "npm:typescript@7.1.0-dev.20261003.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@eslint/js": "10.0.1",
@@ -38,7 +38,7 @@
38
38
  "prettier": "3.9.9",
39
39
  "typescript": "npm:@typescript/typescript6@6.0.2",
40
40
  "typescript-eslint": "8.71.0",
41
- "vitest": "5.0.2"
41
+ "vitest": "5.0.3"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"
@@ -4,8 +4,18 @@
4
4
  * declaration every link of that chain is.
5
5
  */
6
6
 
7
+ import {
8
+ isExportAssignment,
9
+ isExportSpecifier,
10
+ isIdentifier,
11
+ isImportClause,
12
+ isImportSpecifier,
13
+ isNamespaceExport,
14
+ isNamespaceImport,
15
+ } from "@typescript/native/unstable/ast";
7
16
  import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
8
- import { nodeKey, type Inventory } from "./inventory.ts";
17
+ import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
18
+ import { UNANSWERED, type Answer } from "./query.ts";
9
19
  import type { ProjectView } from "./session.ts";
10
20
 
11
21
  /** One declaration a chain reaches, and whether a step along an alias reached it. */
@@ -13,6 +23,11 @@ export interface ChainTarget {
13
23
  readonly id: string;
14
24
  /** False for a declaration the head symbol itself is, true for one a link stands for. */
15
25
  readonly stepped: boolean;
26
+ /**
27
+ * True where the compiler did not answer a step and the declaration is one the link
28
+ * may stand for by the name it carries.
29
+ */
30
+ readonly guessed: boolean;
16
31
  }
17
32
 
18
33
  /** The chain walks over one project's aliases, each step asked of the compiler once. */
@@ -33,7 +48,7 @@ export interface AliasChains {
33
48
  * inventory declares, stepping over the links it does not, or none where no later
34
49
  * link is declared.
35
50
  */
36
- nextDeclared(symbol: TSSymbol): readonly string[];
51
+ nextDeclared(symbol: TSSymbol): readonly ChainTarget[];
37
52
  /** How many steps the compiler was asked for: one per alias, whatever the chains it heads. */
38
53
  readonly steps: number;
39
54
  }
@@ -47,8 +62,8 @@ export interface AliasChains {
47
62
  * library file is never fetched to find out that the inventory does not hold it.
48
63
  */
49
64
  export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory): AliasChains {
50
- const ownFiles = new Set(project.ownSourceFiles().map((file) => file.fileName));
51
- const steps = new Map<number, TSSymbol | undefined>();
65
+ const ownFiles = project.ownPaths();
66
+ const steps = new Map<number, Answer<TSSymbol | undefined>>();
52
67
  const chains = new Map<number, readonly ChainTarget[]>();
53
68
  let asked = 0;
54
69
 
@@ -74,7 +89,7 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
74
89
  * expression is written in an alias form and names nothing declared elsewhere, so its
75
90
  * chain ends at itself.
76
91
  */
77
- const stepOf = (symbol: TSSymbol): TSSymbol | undefined => {
92
+ const stepOf = (symbol: TSSymbol): Answer<TSSymbol | undefined> => {
78
93
  if ((symbol.flags & SymbolFlags.Alias) === 0) {
79
94
  return undefined;
80
95
  }
@@ -87,38 +102,87 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
87
102
  return next;
88
103
  };
89
104
 
105
+ const files = held.symbols.filter((one) => one.kind === "file").map((one) => one.id);
106
+
107
+ /**
108
+ * The declarations an alias whose step went unanswered may stand for: every one the
109
+ * inventory holds under its own name or the name it carries forward, and every file
110
+ * where it carries a module, so a use through it still counts for what it reaches.
111
+ */
112
+ const sameNamed = (alias: TSSymbol): readonly ChainTarget[] => {
113
+ const names = new Set([alias.name]);
114
+ let module = false;
115
+ for (const handle of alias.declarations) {
116
+ const node = ownFiles.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
117
+ if (node === undefined) {
118
+ continue;
119
+ }
120
+ if ((isImportSpecifier(node) || isExportSpecifier(node)) && node.propertyName !== undefined) {
121
+ names.add(node.propertyName.text);
122
+ } else if (isImportClause(node)) {
123
+ names.add("default");
124
+ } else if (isNamespaceImport(node) || isNamespaceExport(node)) {
125
+ module = true;
126
+ } else if (isExportAssignment(node) && isIdentifier(node.expression)) {
127
+ names.add(node.expression.text);
128
+ }
129
+ }
130
+ const byName = declarationsByName(held);
131
+ const ids = [...names].flatMap((name) => byName.get(name) ?? []);
132
+ return [...new Set([...ids, ...(module ? files : [])])].map((id) => ({
133
+ id,
134
+ stepped: true,
135
+ guessed: true,
136
+ }));
137
+ };
138
+
90
139
  const chainOf = (symbol: TSSymbol): readonly ChainTarget[] => {
91
140
  const cached = chains.get(symbol.id);
92
141
  if (cached !== undefined) {
93
142
  return cached;
94
143
  }
95
144
  const targets: ChainTarget[] = [];
145
+ const add = (id: string, stepped: boolean, guessed = false): void => {
146
+ if (!targets.some((target) => target.id === id)) {
147
+ targets.push({ id, stepped, guessed });
148
+ }
149
+ };
96
150
  const walked = new Set<number>();
97
151
  let at: TSSymbol | undefined = symbol;
98
152
  let stepped = false;
99
153
  while (at !== undefined && !walked.has(at.id)) {
100
154
  walked.add(at.id);
101
155
  for (const id of declarationsOf(at)) {
102
- if (!targets.some((target) => target.id === id)) {
103
- targets.push({ id, stepped });
156
+ add(id, stepped);
157
+ }
158
+ const next = stepOf(at);
159
+ if (next === UNANSWERED) {
160
+ for (const target of sameNamed(at)) {
161
+ add(target.id, true, true);
104
162
  }
163
+ break;
105
164
  }
106
- at = stepOf(at);
165
+ at = next;
107
166
  stepped = true;
108
167
  }
109
168
  chains.set(symbol.id, targets);
110
169
  return targets;
111
170
  };
112
171
 
113
- const nextDeclared = (symbol: TSSymbol): readonly string[] => {
172
+ const nextDeclared = (symbol: TSSymbol): readonly ChainTarget[] => {
114
173
  const walked = new Set<number>([symbol.id]);
174
+ let from = symbol;
115
175
  let at = stepOf(symbol);
116
- while (at !== undefined && !walked.has(at.id)) {
176
+ while (at !== undefined && (at === UNANSWERED || !walked.has(at.id))) {
177
+ if (at === UNANSWERED) {
178
+ return sameNamed(from);
179
+ }
117
180
  walked.add(at.id);
118
181
  const ids = declarationsOf(at);
119
182
  if (ids.length > 0) {
120
- return ids;
183
+ return ids.map((id) => ({ id, stepped: true, guessed: false }));
121
184
  }
185
+ from = at;
122
186
  at = stepOf(at);
123
187
  }
124
188
  return [];