@cplieger/deadset-ts 3.12.1 → 4.0.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 (120) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/node-host.js +10 -7
  3. package/dist/src/alias-chain.js +59 -7
  4. package/dist/src/analysis.js +217 -48
  5. package/dist/src/calls.js +23 -22
  6. package/dist/src/class-members.js +10 -11
  7. package/dist/src/components.js +4 -2
  8. package/dist/src/configured-declarations.js +56 -7
  9. package/dist/src/conformance-record.js +2 -2
  10. package/dist/src/dependencies.js +34 -17
  11. package/dist/src/deprecation.js +35 -24
  12. package/dist/src/destructuring.js +37 -29
  13. package/dist/src/discover.js +105 -28
  14. package/dist/src/edges.js +4 -1
  15. package/dist/src/entry-points.js +7 -0
  16. package/dist/src/enum-group.js +46 -40
  17. package/dist/src/exempt.js +42 -1
  18. package/dist/src/findings/completion.js +37 -8
  19. package/dist/src/findings/emitters.js +20 -6
  20. package/dist/src/findings/interfaces.js +7 -1
  21. package/dist/src/findings/intra-function.js +91 -2
  22. package/dist/src/findings/self-check.js +30 -7
  23. package/dist/src/findings/unused-declarations.js +15 -3
  24. package/dist/src/findings-pass.js +18 -11
  25. package/dist/src/framework-lifecycle.js +8 -6
  26. package/dist/src/implementations.js +45 -23
  27. package/dist/src/interface-satisfaction.js +30 -27
  28. package/dist/src/intra-function-parts.js +660 -0
  29. package/dist/src/inventory.js +85 -4
  30. package/dist/src/json-chunks.js +76 -0
  31. package/dist/src/kinds.js +337 -35
  32. package/dist/src/partial.js +23 -0
  33. package/dist/src/query.js +207 -0
  34. package/dist/src/references.js +92 -20
  35. package/dist/src/reflective-lookup.js +46 -4
  36. package/dist/src/report.js +14 -3
  37. package/dist/src/reporters/annotations.js +9 -5
  38. package/dist/src/reporters/json.js +3 -2
  39. package/dist/src/reporters/sarif.js +22 -16
  40. package/dist/src/reporters/template.js +112 -12
  41. package/dist/src/reporters/text.js +8 -6
  42. package/dist/src/roots.js +27 -11
  43. package/dist/src/serialization-contract.js +53 -32
  44. package/dist/src/session.js +79 -39
  45. package/dist/src/signatures.js +32 -0
  46. package/dist/src/suppress-file.js +29 -7
  47. package/dist/src/suppress-inline.js +8 -1
  48. package/dist/src/suppress.js +27 -0
  49. package/dist/src/sweep.js +4 -1
  50. package/dist/src/value-reach.js +44 -25
  51. package/dist/src/verbs/analyze.js +39 -14
  52. package/dist/src/verbs/describe.js +12 -8
  53. package/dist/src/verbs/explain.js +22 -9
  54. package/dist/src/verbs/print-projects.js +40 -7
  55. package/dist/src/verbs/print-retained.js +6 -2
  56. package/dist/src/verbs/print-roots.js +5 -1
  57. package/dist/src/version.js +1 -1
  58. package/jsr.json +1 -1
  59. package/package.json +3 -3
  60. package/src/alias-chain.ts +75 -11
  61. package/src/analysis.ts +385 -104
  62. package/src/calls.ts +10 -9
  63. package/src/class-members.ts +10 -11
  64. package/src/components.ts +4 -2
  65. package/src/configured-declarations.ts +80 -8
  66. package/src/conformance-record.ts +2 -2
  67. package/src/dependencies.ts +42 -20
  68. package/src/deprecation.ts +53 -27
  69. package/src/destructuring.ts +50 -39
  70. package/src/discover.ts +177 -33
  71. package/src/edges.ts +4 -1
  72. package/src/entry-points.ts +7 -0
  73. package/src/enum-group.ts +56 -42
  74. package/src/exempt.ts +50 -2
  75. package/src/finding.ts +5 -1
  76. package/src/findings/completion.ts +46 -10
  77. package/src/findings/emitter.ts +3 -0
  78. package/src/findings/emitters.ts +27 -6
  79. package/src/findings/interfaces.ts +7 -1
  80. package/src/findings/intra-function.ts +106 -3
  81. package/src/findings/self-check.ts +48 -7
  82. package/src/findings/unused-declarations.ts +18 -4
  83. package/src/findings-pass.ts +26 -11
  84. package/src/framework-lifecycle.ts +10 -6
  85. package/src/host.ts +5 -3
  86. package/src/implementations.ts +59 -23
  87. package/src/interface-satisfaction.ts +30 -27
  88. package/src/intra-function-parts.ts +889 -0
  89. package/src/inventory.ts +97 -4
  90. package/src/json-chunks.ts +87 -0
  91. package/src/kinds.ts +355 -41
  92. package/src/partial.ts +40 -0
  93. package/src/query.ts +522 -0
  94. package/src/references.ts +119 -38
  95. package/src/reflective-lookup.ts +56 -6
  96. package/src/report.ts +32 -4
  97. package/src/reporters/annotations.ts +23 -21
  98. package/src/reporters/json.ts +3 -2
  99. package/src/reporters/reporter.ts +2 -2
  100. package/src/reporters/sarif.ts +27 -18
  101. package/src/reporters/template.ts +119 -14
  102. package/src/reporters/text.ts +12 -12
  103. package/src/roots.ts +34 -12
  104. package/src/serialization-contract.ts +59 -40
  105. package/src/session.ts +146 -58
  106. package/src/signatures.ts +55 -0
  107. package/src/suppress-file.ts +40 -8
  108. package/src/suppress-inline.ts +8 -1
  109. package/src/suppress.ts +31 -0
  110. package/src/sweep.ts +9 -4
  111. package/src/value-reach.ts +66 -28
  112. package/src/verbs/analyze.ts +51 -14
  113. package/src/verbs/describe.ts +13 -9
  114. package/src/verbs/explain.ts +20 -9
  115. package/src/verbs/print-projects.ts +53 -7
  116. package/src/verbs/print-retained.ts +6 -2
  117. package/src/verbs/print-roots.ts +5 -1
  118. package/src/version.ts +1 -1
  119. package/dist/src/reporters/rules.js +0 -178
  120. package/src/reporters/rules.ts +0 -195
@@ -19,14 +19,35 @@ export class DiscoveryError extends Error {
19
19
  this.diagnostics = diagnostics;
20
20
  }
21
21
  }
22
+ /** The category an error diagnostic carries, which the protocol sends as a number. */
23
+ const ERROR_CATEGORY = DiagnosticCategory.Error;
22
24
  /** Whether one diagnostic is an error, which is what fails a run closed. */
23
25
  export function isError(diagnostic) {
24
- return diagnostic.category === DiagnosticCategory.Error;
26
+ return diagnostic.category === ERROR_CATEGORY;
25
27
  }
26
- /** One diagnostic as a line naming its position, its code and its message. */
27
- export function renderDiagnostic(diagnostic) {
28
- const where = diagnostic.fileName ?? "<no file>";
29
- return `${where}:${String(diagnostic.pos)}: TS${String(diagnostic.code)}: ${diagnostic.text}`;
28
+ /**
29
+ * One diagnostic as a line naming its file, its line and column, its code and its
30
+ * message: `path:line:column: TScode: message`, the position left out where the
31
+ * diagnostic names none and the file where it names none. `display` spells the
32
+ * file's path; it answers the path itself where it is not given.
33
+ */
34
+ export function renderDiagnostic(located, display = (path) => path) {
35
+ const { diagnostic, at } = located;
36
+ const message = `TS${String(diagnostic.code)}: ${diagnostic.text}`;
37
+ if (diagnostic.fileName === undefined) {
38
+ return message;
39
+ }
40
+ const where = display(diagnostic.fileName);
41
+ return at === undefined
42
+ ? `${where}: ${message}`
43
+ : `${where}:${String(at.line)}:${String(at.column)}: ${message}`;
44
+ }
45
+ /** Each diagnostic with the line and column its offset falls on. */
46
+ function locatedIn(diagnostics, locate) {
47
+ return diagnostics.map((diagnostic) => ({
48
+ diagnostic,
49
+ at: diagnostic.fileName === undefined ? undefined : locate(diagnostic.fileName, diagnostic.pos),
50
+ }));
30
51
  }
31
52
  /**
32
53
  * Every error a project's three diagnostic sets carry, in the order the compiler
@@ -38,6 +59,32 @@ export function renderDiagnostic(diagnostic) {
38
59
  export function diagnosticErrors(sets) {
39
60
  return [...sets.configParsing, ...sets.syntactic, ...sets.semantic].filter(isError);
40
61
  }
62
+ /** The errors one open project carries, each located in the file it names. */
63
+ export function projectErrors(sets) {
64
+ const load = sets.configParsing.filter(isError);
65
+ return {
66
+ load: locatedIn(load, sets.locate),
67
+ check: load.length > 0 ? [] : locatedIn(diagnosticErrors(sets), sets.locate),
68
+ };
69
+ }
70
+ /**
71
+ * The first line of one error about the target, every mention of the target root
72
+ * spelled relative to it: a path below the root loses the root and its separator, and
73
+ * the root itself is `.`.
74
+ */
75
+ export function targetRelative(text, targetRoot) {
76
+ const root = targetRoot.endsWith("/") ? targetRoot.slice(0, -1) : targetRoot;
77
+ const [first = ""] = text.split(/\r?\n/u);
78
+ const spelled = first.split(`${root}/`).join("").split(root).join(".").trim();
79
+ return spelled === "" ? "the configuration could not be built" : spelled;
80
+ }
81
+ /**
82
+ * Whether one configuration is the analyzer's own guess: discovery derived it and the
83
+ * scope did not name it. A guess that does not load is dropped; any other ends the run.
84
+ */
85
+ export function isGuess(discovery, configFile) {
86
+ return discovery.derived && !discovery.fromScope.includes(configFile);
87
+ }
41
88
  function isRecord(value) {
42
89
  return typeof value === "object" && value !== null && !Array.isArray(value);
43
90
  }
@@ -130,9 +177,11 @@ function scopeConfigFiles(host, scope) {
130
177
  /**
131
178
  * Every configuration file reached from `pending`, in the order discovery reads them:
132
179
  * each once, each followed by the configurations it references, transitively. A
133
- * configuration the compiler cannot read ends the run naming it.
180
+ * configuration the compiler cannot read ends the run naming it, unless `unreadable`
181
+ * is given, which is then told the configuration and why, and the walk goes on
182
+ * without it.
134
183
  */
135
- function derivedFrom(engine, host, pending) {
184
+ function derivedFrom(engine, host, pending, unreadable) {
136
185
  const seen = new Set();
137
186
  const found = [];
138
187
  while (pending.length > 0) {
@@ -145,7 +194,14 @@ function derivedFrom(engine, host, pending) {
145
194
  continue;
146
195
  }
147
196
  seen.add(path);
148
- parseOrRefuse(engine, path, path);
197
+ const reason = unreadableReason(engine, path);
198
+ if (reason !== undefined) {
199
+ if (unreadable === undefined) {
200
+ throw new DiscoveryError(`${path} cannot be read as a compiler configuration: ${reason}`, []);
201
+ }
202
+ unreadable(path, reason);
203
+ continue;
204
+ }
149
205
  found.push(path);
150
206
  pending.push(...referencesOf(host, path));
151
207
  }
@@ -179,18 +235,25 @@ export function consumerProjects(engine, host, root) {
179
235
  function derivedId(targetRoot, configFile) {
180
236
  return relativePath(targetRoot, configFile) ?? configFile;
181
237
  }
238
+ /** Why the compiler cannot read one configuration file, or undefined where it can. */
239
+ function unreadableReason(engine, configFile) {
240
+ try {
241
+ engine.parseConfigFile(configFile);
242
+ return undefined;
243
+ }
244
+ catch (error) {
245
+ return error instanceof Error ? error.message : String(error);
246
+ }
247
+ }
182
248
  /**
183
249
  * Resolves one compiler configuration file, or ends the run naming it: a project
184
250
  * opened from a configuration the compiler cannot read would be analyzed with no
185
251
  * type information.
186
252
  */
187
253
  function parseOrRefuse(engine, configFile, named) {
188
- try {
189
- engine.parseConfigFile(configFile);
190
- }
191
- catch (error) {
192
- throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ` +
193
- (error instanceof Error ? error.message : String(error)), []);
254
+ const reason = unreadableReason(engine, configFile);
255
+ if (reason !== undefined) {
256
+ throw new DiscoveryError(`${named} cannot be read as a compiler configuration: ${reason}`, []);
194
257
  }
195
258
  }
196
259
  /**
@@ -232,25 +295,41 @@ function declaredProjects(engine, host, targetRoot, entries) {
232
295
  "into the program that references it, so the matrix names it too", []);
233
296
  }
234
297
  }
235
- return { projects, configFiles: projects.map((project) => project.configFile), fromScope: [] };
298
+ return {
299
+ projects,
300
+ derived: false,
301
+ notBuilt: [],
302
+ configFiles: projects.map((project) => project.configFile),
303
+ fromScope: [],
304
+ };
236
305
  }
237
306
  /**
238
307
  * The projects a run derives where the matrix declares none, in the order discovery
239
308
  * reads them: the paths the scope names, then every compiler configuration under
240
309
  * the target root that is not inside an ignored directory, then the configurations
241
- * each of those references, transitively. Each configuration appears once.
310
+ * each of those references, transitively. Each configuration appears once. A
311
+ * configuration the scope names directly is the caller's own and ends the run where
312
+ * it cannot be read; one the walk found is dropped and named.
242
313
  */
243
314
  function derivedProjects(engine, host, scope) {
315
+ const targetRoot = scope.target.path;
244
316
  const fromScope = scopeConfigFiles(host, scope);
245
- const configFiles = derivedFrom(engine, host, [
246
- ...fromScope,
247
- ...configFilesUnder(host, scope.target.path),
248
- ]);
317
+ const notBuilt = [];
318
+ const configFiles = derivedFrom(engine, host, [...fromScope, ...configFilesUnder(host, targetRoot)], (configFile, reason) => {
319
+ if (fromScope.includes(configFile)) {
320
+ throw new DiscoveryError(`${configFile} cannot be read as a compiler configuration: ${reason}`, []);
321
+ }
322
+ notBuilt.push({
323
+ id: derivedId(targetRoot, configFile),
324
+ configFile,
325
+ error: targetRelative(reason, resolvePath(host.workingDirectory(), targetRoot)),
326
+ });
327
+ });
249
328
  const projects = configFiles.map((configFile) => ({
250
- id: derivedId(scope.target.path, configFile),
329
+ id: derivedId(targetRoot, configFile),
251
330
  configFile,
252
331
  }));
253
- return { projects, configFiles, fromScope };
332
+ return { projects, derived: true, notBuilt, configFiles, fromScope };
254
333
  }
255
334
  /**
256
335
  * Every project one run analyzes.
@@ -262,12 +341,10 @@ function derivedProjects(engine, host, scope) {
262
341
  * scope and the target tree. A platform entry names a configuration of another
263
342
  * language and plays no part here.
264
343
  *
265
- * A configuration file the compiler cannot read ends the run with the failure code,
266
- * so a project that would have been analyzed with no type information is never
267
- * analyzed at all, whether the matrix declared it or discovery derived it. What a
268
- * configuration's own contents say is refused once its project is open, by
269
- * {@link diagnosticErrors} over the three sets a program reports, because a
270
- * resolution carries no diagnostics.
344
+ * A configuration file the compiler cannot read is never analyzed. One the matrix or
345
+ * the scope names ends the run with the failure code; a guess is dropped and named in
346
+ * {@link Discovery.notBuilt}. What a configuration's own contents say is read once its
347
+ * project is open, by {@link diagnosticErrors}, because a resolution carries none.
271
348
  *
272
349
  * Nothing reads a build output: a project is the files its configuration names, so a
273
350
  * package whose sources ship as TypeScript is analyzed as it stands.
package/dist/src/edges.js CHANGED
@@ -8,6 +8,7 @@ import { ConfigError } from "./config.js";
8
8
  import { positionAt, walkDocument } from "./json-document.js";
9
9
  import { joinPath } from "./paths.js";
10
10
  import { positionKey } from "./position.js";
11
+ import { isPartSubject } from "./suppress.js";
11
12
  /** The name and the location the Contract fixes for the edges document. */
12
13
  export const EDGES_FILE = "deadset-edges.json";
13
14
  /** The language prefix of every reference this analyzer evaluates. */
@@ -169,7 +170,9 @@ export function evaluateEdges(findings, sides, symbols) {
169
170
  }
170
171
  const held = new Map();
171
172
  findings.forEach((finding, at) => {
172
- held.set(positionKey(finding.position), at);
173
+ if (!isPartSubject(finding.symbol.kind)) {
174
+ held.set(positionKey(finding.position), at);
175
+ }
173
176
  });
174
177
  const pending = new Set();
175
178
  const evaluations = sides.map((side) => {
@@ -14,6 +14,7 @@ import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAss
14
14
  import { SymbolFlags } from "@typescript/native/unstable/sync";
15
15
  import { globExpression } from "./glob.js";
16
16
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
17
+ import { UNANSWERED } from "./query.js";
17
18
  /** The extensions the test runner and the browser-test runner load a configuration from. */
18
19
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
19
20
  /** The extensions the linter's flat configuration and the mutation tester's are loaded from. */
@@ -331,8 +332,14 @@ function workersOf(project, file, files) {
331
332
  const names = sites.flatMap((site) => site.platformNames);
332
333
  const symbols = project.symbolsAt(names.map((name) => project.handle(name)));
333
334
  const platform = new Set();
335
+ // A name the checker could not resolve is read as the platform's, so a worker it
336
+ // stands on is rooted rather than reported.
334
337
  names.forEach((name, index) => {
335
338
  const symbol = symbols[index];
339
+ if (symbol === UNANSWERED) {
340
+ platform.add(name);
341
+ return;
342
+ }
336
343
  if (symbol !== undefined &&
337
344
  (symbol.flags & SymbolFlags.Alias) === 0 &&
338
345
  symbol.declarations.length > 0 &&
@@ -11,7 +11,7 @@ import { TypeFlags } from "@typescript/native/unstable/sync";
11
11
  import { aliasChains } from "./alias-chain.js";
12
12
  import { nodeKey } from "./inventory.js";
13
13
  import { renderPosition } from "./position.js";
14
- import { DEFAULT_BATCH_CAP } from "./references.js";
14
+ import { isAnswered, UNANSWERED } from "./query.js";
15
15
  /** The operand types a conversion produces an enum value from. */
16
16
  const NUMBER = TypeFlags.NumberLike;
17
17
  const STRING = TypeFlags.StringLike;
@@ -104,24 +104,31 @@ function sitesOf(file) {
104
104
  * union, and itself otherwise. An enum's own value, or one of its members, is a value the
105
105
  * source names, whichever number or string it also is.
106
106
  */
107
- function partsOf(type) {
108
- const parts = type.isUnionType() ? type.getTypes() : [type];
109
- return parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0);
107
+ function partsOf(project, type) {
108
+ const parts = type.isUnionType() ? project.queries.constituents(type) : [type];
109
+ return isAnswered(parts)
110
+ ? parts.filter((part) => (part.flags & TypeFlags.EnumLike) === 0)
111
+ : parts;
110
112
  }
113
+ /** The detail a site records whose operand's type the checker could not answer. */
114
+ const UNTYPED_DETAIL = "converted from a value of unanswered type";
111
115
  /**
112
116
  * The detail one site records about the enum it produces, or undefined where the
113
117
  * operand's type converts nothing: a number, or a string an assertion converts, is
114
118
  * converted, a value of type `any` or `unknown` is decoded, and a value of an enum is one
115
119
  * the source names.
116
120
  */
117
- function detailOf(site, enumName, operand) {
121
+ function detailOf(project, site, enumName, operand) {
118
122
  if (site.form === "lookup") {
119
123
  return `looked up by an element access on ${enumName}`;
120
124
  }
121
125
  if (operand === undefined) {
122
126
  return undefined;
123
127
  }
124
- const parts = partsOf(operand);
128
+ const parts = operand === UNANSWERED ? UNANSWERED : partsOf(project, operand);
129
+ if (parts === UNANSWERED) {
130
+ return UNTYPED_DETAIL;
131
+ }
125
132
  if (parts.some((part) => (part.flags & NUMBER) !== 0)) {
126
133
  return "converted from number";
127
134
  }
@@ -160,32 +167,33 @@ export function enumGroup(input) {
160
167
  const sites = files.flatMap(sitesOf);
161
168
  const named = new Map();
162
169
  const chains = aliasChains(project, held);
163
- for (let from = 0; from < sites.length; from += DEFAULT_BATCH_CAP) {
164
- const run = sites.slice(from, from + DEFAULT_BATCH_CAP);
165
- project.symbolsAt(run.map((site) => project.handle(site.name))).forEach((symbol, index) => {
166
- const site = run[index];
167
- if (symbol === undefined || site === undefined) {
168
- return;
169
- }
170
- const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
171
- if (target !== undefined) {
172
- named.set(site, target.id);
170
+ // A name the checker could not resolve may stand for any enum of the target it spells.
171
+ const enumsNamed = (text) => [...enums].flatMap(([id, name]) => (name === text ? [id] : []));
172
+ project.symbolsAt(sites.map((site) => project.handle(site.name))).forEach((symbol, index) => {
173
+ const site = sites[index];
174
+ if (symbol === undefined || site === undefined) {
175
+ return;
176
+ }
177
+ if (symbol === UNANSWERED) {
178
+ const ids = enumsNamed(site.name.getText());
179
+ if (ids.length > 0) {
180
+ named.set(site, ids);
173
181
  }
174
- });
175
- }
182
+ return;
183
+ }
184
+ const target = chains.chainOf(symbol).find((link) => enums.has(link.id));
185
+ if (target !== undefined) {
186
+ named.set(site, [target.id]);
187
+ }
188
+ });
176
189
  const typed = sites.filter((site) => named.has(site) && site.operand !== undefined);
177
190
  const operandType = new Map();
178
- for (let from = 0; from < typed.length; from += DEFAULT_BATCH_CAP) {
179
- const run = typed.slice(from, from + DEFAULT_BATCH_CAP);
180
- project.checker
181
- .getTypeAtLocation(run.map((site) => site.operand ?? site.at))
182
- .forEach((type, index) => {
183
- const site = run[index];
184
- if (type !== undefined && site !== undefined) {
185
- operandType.set(site, type);
186
- }
187
- });
188
- }
191
+ project.queries.typesAt(typed.map((site) => site.operand ?? site.at)).forEach((type, index) => {
192
+ const site = typed[index];
193
+ if (type !== undefined && site !== undefined) {
194
+ operandType.set(site, type);
195
+ }
196
+ });
189
197
  const members = new Map();
190
198
  for (const symbol of held.symbols) {
191
199
  if (symbol.kind === "enum-member" && enums.has(symbol.parent)) {
@@ -200,17 +208,15 @@ export function enumGroup(input) {
200
208
  }
201
209
  const found = [];
202
210
  for (const site of sites) {
203
- const enumId = named.get(site);
204
- if (enumId === undefined) {
205
- continue;
206
- }
207
- const detail = detailOf(site, enums.get(enumId) ?? "", operandType.get(site));
208
- if (detail === undefined) {
209
- continue;
210
- }
211
- const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
212
- for (const id of members.get(enumId) ?? []) {
213
- found.push({ id, detail, site: position });
211
+ for (const enumId of named.get(site) ?? []) {
212
+ const detail = detailOf(project, site, enums.get(enumId) ?? "", operandType.get(site));
213
+ if (detail === undefined) {
214
+ continue;
215
+ }
216
+ const position = renderPosition(site.at.getSourceFile(), targetRoot, site.at.getStart());
217
+ for (const id of members.get(enumId) ?? []) {
218
+ found.push({ id, detail, site: position });
219
+ }
214
220
  }
215
221
  }
216
222
  return found;
@@ -9,6 +9,7 @@
9
9
  import { ConfigError } from "./config.js";
10
10
  import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, typescriptVisibilityOf, } from "./exempt-classes.js";
11
11
  import { byPosition, positionKey } from "./position.js";
12
+ import { Unanswerable } from "./query.js";
12
13
  import { sweep } from "./sweep.js";
13
14
  /**
14
15
  * The classes `exemptions.disabled` switches off. A name is refused unless the
@@ -39,6 +40,46 @@ function retainable(allowed, visibility) {
39
40
  }
40
41
  return true;
41
42
  }
43
+ /**
44
+ * The kinds of declaration each class can hold back, which a detector that stops holds
45
+ * back whole. A class with no row here holds back every declaration but a file.
46
+ */
47
+ const RETAINABLE = new Map([
48
+ ["interface-satisfaction", new Set(["method", "class-member"])],
49
+ ["enum-group", new Set(["enum-member"])],
50
+ ["injection-container", new Set(["method", "class-member"])],
51
+ ["framework-lifecycle", new Set(["method", "class-member"])],
52
+ [
53
+ "serialization-contract",
54
+ new Set(["method", "class-member", "interface-method", "type-member"]),
55
+ ],
56
+ ]);
57
+ /** The detail of a record a detector that could not finish holds. */
58
+ const UNANSWERED_DETAIL = "kept live by a question the checker did not answer";
59
+ /** Whether one record is held by a detector that could not finish. */
60
+ export function isUnansweredRecord(record) {
61
+ return record.detail === UNANSWERED_DETAIL;
62
+ }
63
+ /**
64
+ * What one detector holds back over one project. A detector that stops because the
65
+ * checker left a question it needs unanswered holds back every declaration of the
66
+ * project its class could hold back, each recorded at its own position, so the gap
67
+ * never yields a finding.
68
+ */
69
+ function detected(exemptionClass, detect, input) {
70
+ try {
71
+ return detect(input);
72
+ }
73
+ catch (error) {
74
+ if (!(error instanceof Unanswerable)) {
75
+ throw error;
76
+ }
77
+ const kinds = RETAINABLE.get(exemptionClass);
78
+ return input.held.symbols
79
+ .filter((symbol) => (kinds === undefined ? symbol.kind !== "file" : kinds.has(symbol.kind)))
80
+ .map((symbol) => ({ id: symbol.id, detail: UNANSWERED_DETAIL, site: symbol.position }));
81
+ }
82
+ }
42
83
  /**
43
84
  * The records one project's detectors found, each class in the vocabulary's order and
44
85
  * none of a switched-off class. Evidence on a private member the class's row says it
@@ -55,7 +96,7 @@ export function computeExemptions(input, detectors, holding) {
55
96
  continue;
56
97
  }
57
98
  const allowed = typescriptVisibilityOf(exemptionClass);
58
- for (const evidence of detect(input)) {
99
+ for (const evidence of detected(exemptionClass, detect, input)) {
59
100
  if (!retainable(allowed, visibility.get(evidence.id))) {
60
101
  continue;
61
102
  }
@@ -6,8 +6,9 @@
6
6
  import { componentId } from "../components.js";
7
7
  import { OUTSIDE } from "../graph.js";
8
8
  import { KINDS } from "../kinds.js";
9
+ import { byPosition } from "../position.js";
9
10
  import { FAMILY_KEY_LENGTH } from "../resolve.js";
10
- import { isRowSubject } from "../suppress.js";
11
+ import { isPartSubject, isRowSubject } from "../suppress.js";
11
12
  /** The classes ranked by the claim each makes, the strongest highest. */
12
13
  const RANK = { certain: 3, probable: 2, possible: 1 };
13
14
  /** The weaker of two classes, which is a class capped by a ceiling. */
@@ -66,14 +67,35 @@ function reachabilityClasses(input) {
66
67
  : "probable";
67
68
  return (id) => (published.has(id) ? outside : "certain");
68
69
  }
69
- /** The component each dead declaration of the run falls in, and none for a live one. */
70
- function findingComponents(swept) {
70
+ /**
71
+ * The component each dead declaration of the run falls in, and none for a live one, with
72
+ * its members listed in the canonical order where the run lists a component in full.
73
+ */
74
+ function findingComponents(swept, cascade) {
71
75
  const held = new Map();
72
76
  for (const component of swept.sweep.components) {
73
77
  for (const member of component.members) {
74
78
  held.set(member, component);
75
79
  }
76
80
  }
81
+ const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
82
+ const listed = (component) => component.members
83
+ .flatMap((member) => {
84
+ const symbol = symbols.get(member);
85
+ return symbol === undefined
86
+ ? []
87
+ : [
88
+ {
89
+ ref: symbol.ref,
90
+ name: symbol.name,
91
+ position: {
92
+ ...symbol.position,
93
+ endLine: Math.max(symbol.position.line, symbol.endLine),
94
+ },
95
+ },
96
+ ];
97
+ })
98
+ .sort((a, b) => byPosition(a.position, b.position) || (a.ref === b.ref ? 0 : a.ref < b.ref ? -1 : 1));
77
99
  return (id) => {
78
100
  const component = held.get(id);
79
101
  return component === undefined
@@ -83,6 +105,7 @@ function findingComponents(swept) {
83
105
  root: component.roots.includes(id),
84
106
  symbolCount: component.members.length,
85
107
  deletableLines: component.deletableLines,
108
+ ...(cascade === "full" ? { members: listed(component) } : {}),
86
109
  };
87
110
  };
88
111
  }
@@ -101,7 +124,7 @@ export function completed(input, findings, kinds = KINDS) {
101
124
  const positions = new Map(union.symbols.map((symbol, at) => [symbol.ref, at]));
102
125
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
103
126
  const classOf = reachabilityClasses(input);
104
- const componentOf = findingComponents(swept);
127
+ const componentOf = findingComponents(swept, input.config.reporters.cascade);
105
128
  let minted = swept.sweep.components.length;
106
129
  return findings.map((finding) => {
107
130
  const row = kinds.get(finding.code);
@@ -113,11 +136,15 @@ export function completed(input, findings, kinds = KINDS) {
113
136
  const at = isRowSubject(finding.symbol.kind)
114
137
  ? OUTSIDE
115
138
  : (positions.get(finding.symbol.ref) ?? OUTSIDE);
139
+ // A part names the declaration that holds it, and is decided inside it rather than by
140
+ // the declaration's liveness, so it carries none of the declaration's sweep facts.
141
+ const part = isPartSubject(finding.symbol.kind);
116
142
  const symbol = union.symbols[at];
117
- const judged = union.subject[at] === true ? symbol : undefined;
143
+ const judged = !part && union.subject[at] === true ? symbol : undefined;
118
144
  const candidate = judged === undefined ? undefined : candidates.get(judged.id);
119
- const found = symbol === undefined ? undefined : componentOf(symbol.id);
120
- if (candidate !== undefined && found === undefined) {
145
+ const found = symbol === undefined || part ? undefined : componentOf(symbol.id);
146
+ // A test file's declaration is a component member only as a test of dead code.
147
+ if (candidate !== undefined && found === undefined && union.test[at] !== true) {
121
148
  throw new Error(`${candidate.id} is judged dead and falls in no dead component`);
122
149
  }
123
150
  let component = found;
@@ -148,7 +175,9 @@ export function completed(input, findings, kinds = KINDS) {
148
175
  component,
149
176
  retainedBy: [],
150
177
  configurations: candidate?.configurations ??
151
- (judged === undefined ? swept.matrix.configurations : (swept.matrix.heldIn[at] ?? [])),
178
+ (judged === undefined && !part
179
+ ? swept.matrix.configurations
180
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
152
181
  consumersLoaded: input.boundary.consumers.loaded,
153
182
  fixability: row.fixability,
154
183
  severity: severityOf(input, finding.code, row.defaultSeverity),
@@ -7,7 +7,7 @@ import { intraFunction } from "./intra-function.js";
7
7
  import { nonCodeArtifacts } from "./non-code-artifacts.js";
8
8
  import { readsAndWrites } from "./reads-and-writes.js";
9
9
  import { selfCheck } from "./self-check.js";
10
- import { unusedDeclarations } from "./unused-declarations.js";
10
+ import { fallenMembers, unusedDeclarations } from "./unused-declarations.js";
11
11
  import { visibilityNarrowing } from "./visibility-narrowing.js";
12
12
  /** Every kind family this analyzer reports, each by its emitter, in code order. */
13
13
  export const EMITTERS = new Map([
@@ -20,9 +20,22 @@ export const EMITTERS = new Map([
20
20
  ["self-check", selfCheck],
21
21
  ["intra-function", intraFunction],
22
22
  ]);
23
- /** Every family's findings over one sweep, completed by one step. */
24
- function completedOver(input) {
25
- return completed(input, [...EMITTERS.values()].flatMap((emit) => emit(input)));
23
+ /**
24
+ * The references of every declaration an unanswered question could have kept live, which
25
+ * the run reports nothing about: a finding about one would be computed from the gap.
26
+ */
27
+ function heldRefs(input) {
28
+ const held = new Set(input.swept.heldByUnanswered);
29
+ if (held.size === 0) {
30
+ return held;
31
+ }
32
+ return new Set(input.swept.matrix.union.symbols
33
+ .filter((symbol) => held.has(symbol.id))
34
+ .map((symbol) => symbol.ref));
35
+ }
36
+ /** Every family's findings over one sweep, and those of any further emitter, completed by one step. */
37
+ function completedOver(input, ...more) {
38
+ return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)));
26
39
  }
27
40
  /**
28
41
  * The dials over the findings a record could name, each read against the sweep that
@@ -47,10 +60,11 @@ function dialsOver(input, found, suppressed, unmarked) {
47
60
  */
48
61
  export function decidedFindings(input, suppressed = { unmarked: input, records: [] }) {
49
62
  const found = completedOver(input);
50
- const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked);
63
+ const unmarked = suppressed.records.length === 0 ? [] : completedOver(suppressed.unmarked, fallenMembers);
51
64
  const would = [...unmarked, ...found];
52
65
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked));
53
- const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding)));
66
+ const unanswered = heldRefs(input);
67
+ const kept = reportable(input, found.filter((finding) => !ledger.withheld(finding) && !unanswered.has(finding.symbol.ref)));
54
68
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
55
69
  return {
56
70
  findings: evaluated.findings,
@@ -88,8 +88,14 @@ function unusedMessage(candidate) {
88
88
  ? UNUSED_TEST_MESSAGE
89
89
  : UNUSED_MESSAGE;
90
90
  }
91
- /** Whether one interface method has implementations and every one has an empty body. */
91
+ /**
92
+ * Whether one interface method has implementations and every one has an empty body, or
93
+ * is one whose implementations the run could not read in full.
94
+ */
92
95
  function marker(input, method) {
96
+ if (input.implementations.unknown.has(method)) {
97
+ return true;
98
+ }
93
99
  const bodies = [...(input.implementations.bodies.get(method)?.values() ?? [])];
94
100
  return bodies.length > 0 && bodies.every((empty) => empty);
95
101
  }