@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.4

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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/src/analysis.js +61 -10
  3. package/dist/src/augmentations.js +63 -1
  4. package/dist/src/build-scripts.js +437 -0
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +56 -9
  7. package/dist/src/convention-rows.js +39 -2
  8. package/dist/src/conventions.js +15 -0
  9. package/dist/src/dependencies.js +98 -1
  10. package/dist/src/discover.js +14 -82
  11. package/dist/src/entry-points.js +9 -18
  12. package/dist/src/exempt.js +8 -0
  13. package/dist/src/findings/completion.js +66 -28
  14. package/dist/src/findings/intra-function.js +4 -2
  15. package/dist/src/findings/reads-and-writes.js +7 -6
  16. package/dist/src/findings/unused-declarations.js +4 -1
  17. package/dist/src/graph.js +5 -3
  18. package/dist/src/intra-function-parts.js +45 -17
  19. package/dist/src/kinds.js +8 -8
  20. package/dist/src/manifest.js +18 -0
  21. package/dist/src/matrix.js +38 -4
  22. package/dist/src/query.js +1 -0
  23. package/dist/src/report.js +23 -4
  24. package/dist/src/reporters/annotations.js +15 -8
  25. package/dist/src/reporters/sarif.js +12 -8
  26. package/dist/src/roots.js +197 -42
  27. package/dist/src/serialization-contract.js +590 -12
  28. package/dist/src/sweep.js +7 -9
  29. package/dist/src/test-globals.js +234 -0
  30. package/dist/src/type-errors.js +11 -3
  31. package/dist/src/unseen-loads.js +107 -0
  32. package/dist/src/verbs/analyze.js +24 -3
  33. package/dist/src/workflow-steps.js +32 -15
  34. package/dist/src/yaml-document.js +355 -0
  35. package/jsr.json +1 -1
  36. package/package.json +2 -2
  37. package/src/analysis.ts +109 -12
  38. package/src/augmentations.ts +77 -2
  39. package/src/build-scripts.ts +564 -0
  40. package/src/conformance-record.ts +1 -1
  41. package/src/contextual-uses.ts +64 -12
  42. package/src/convention-rows.ts +47 -2
  43. package/src/conventions.ts +16 -0
  44. package/src/dependencies.ts +110 -1
  45. package/src/discover.ts +14 -94
  46. package/src/entry-points.ts +10 -25
  47. package/src/exempt.ts +16 -0
  48. package/src/file-facts.ts +3 -0
  49. package/src/findings/boundary.ts +2 -0
  50. package/src/findings/completion.ts +76 -30
  51. package/src/findings/intra-function.ts +4 -2
  52. package/src/findings/reads-and-writes.ts +7 -6
  53. package/src/findings/unused-declarations.ts +6 -1
  54. package/src/graph.ts +5 -3
  55. package/src/intra-function-parts.ts +55 -16
  56. package/src/kinds.ts +8 -8
  57. package/src/manifest.ts +21 -3
  58. package/src/matrix.ts +51 -4
  59. package/src/query.ts +11 -0
  60. package/src/report.ts +56 -16
  61. package/src/reporters/annotations.ts +21 -10
  62. package/src/reporters/reporter.ts +1 -3
  63. package/src/reporters/sarif.ts +16 -8
  64. package/src/roots.ts +226 -46
  65. package/src/serialization-contract.ts +758 -12
  66. package/src/sweep.ts +23 -9
  67. package/src/test-globals.ts +275 -0
  68. package/src/type-errors.ts +14 -3
  69. package/src/unseen-loads.ts +145 -0
  70. package/src/verbs/analyze.ts +28 -3
  71. package/src/workflow-steps.ts +35 -17
  72. package/src/yaml-document.ts +377 -0
@@ -14,7 +14,7 @@ import { scriptTokens } from "./manifest.js";
14
14
  import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
15
15
  import { UNANSWERED } from "./query.js";
16
16
  import { sourceFilesOf } from "./source-files.js";
17
- import { workflowTokens } from "./workflow-steps.js";
17
+ import { workflowCommands, workflowTokens } from "./workflow-steps.js";
18
18
  const MANIFEST = "package.json";
19
19
  const MODULES_DIR = "node_modules";
20
20
  const MODULES_SEGMENT = `/${MODULES_DIR}/`;
@@ -664,6 +664,98 @@ function scriptTokensOf(manifest) {
664
664
  }
665
665
  return found;
666
666
  }
667
+ /**
668
+ * The runtime flags whose value is a module loaded before the entry, as `--flag value` or
669
+ * `--flag=value`: https://nodejs.org/api/cli.html#--importmodule,
670
+ * https://nodejs.org/api/cli.html#--experimental-loadermodule,
671
+ * https://nodejs.org/api/cli.html#-r---require-module
672
+ */
673
+ const PRELOAD_FLAGS = new Set([
674
+ "--experimental-loader",
675
+ "--import",
676
+ "--loader",
677
+ "--require",
678
+ "-r",
679
+ ]);
680
+ /**
681
+ * The programs that pass a preload flag to the runtime they start: Node itself, and the
682
+ * runners that forward Node's flags or read the same `--require`:
683
+ * https://mochajs.org/#-require-module-r-module, https://tsx.is/node-enhancement,
684
+ * https://typestrong.org/ts-node/docs/options#require,
685
+ * https://github.com/remy/nodemon#running-non-node-scripts
686
+ */
687
+ const PRELOADING_PROGRAMS = new Set([
688
+ "mocha",
689
+ "node",
690
+ "nodemon",
691
+ "ts-node",
692
+ "tsx",
693
+ ]);
694
+ /** The programs that run the command after them, and an environment assignment before it. */
695
+ const LAUNCHERS = new Set(["cross-env", "env", "npx"]);
696
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/u;
697
+ /** The commands one script string runs, split at the shell's list and pipeline operators. */
698
+ const COMMAND_SEPARATORS = /&&|\|\||;|\||&/u;
699
+ /**
700
+ * The command strings one manifest runs: each of its `scripts`, and the `exec` command of
701
+ * nodemon's configuration, in the manifest's `nodemonConfig` or the `nodemon.json` beside
702
+ * it, which nodemon runs the watched script with:
703
+ * https://github.com/remy/nodemon#config-files
704
+ */
705
+ function manifestCommands(host, dir, manifest) {
706
+ const scripts = manifest["scripts"];
707
+ const nodemon = [manifest["nodemonConfig"], readJSON(host, joinPath(dir, "nodemon.json"))?.value];
708
+ return [
709
+ ...(isRecord(scripts) ? Object.values(scripts) : []),
710
+ ...nodemon.map((config) => (isRecord(config) ? config["exec"] : undefined)),
711
+ ].filter((command) => typeof command === "string");
712
+ }
713
+ /**
714
+ * The packages `scripts` preload by a runtime flag's bare module specifier, a flag
715
+ * counting only in a command whose program passes it to the runtime, and for Node only
716
+ * before the script it runs, whose own arguments come after it.
717
+ */
718
+ function preloadsOf(scripts) {
719
+ const found = new Set();
720
+ for (const script of scripts) {
721
+ for (const command of script.split(COMMAND_SEPARATORS)) {
722
+ const tokens = scriptTokens(command);
723
+ let at = 0;
724
+ while (at < tokens.length &&
725
+ (ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))) {
726
+ at += 1;
727
+ }
728
+ const path = tokens[at] ?? "";
729
+ const program = path.slice(path.lastIndexOf("/") + 1);
730
+ if (!PRELOADING_PROGRAMS.has(program)) {
731
+ continue;
732
+ }
733
+ for (at += 1; at < tokens.length; at += 1) {
734
+ const token = tokens[at] ?? "";
735
+ if (!token.startsWith("-")) {
736
+ if (program === "node") {
737
+ break;
738
+ }
739
+ continue;
740
+ }
741
+ const equals = token.indexOf("=");
742
+ const flag = equals < 0 ? token : token.slice(0, equals);
743
+ if (!PRELOAD_FLAGS.has(flag)) {
744
+ continue;
745
+ }
746
+ if (equals < 0) {
747
+ at += 1;
748
+ }
749
+ const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
750
+ const name = value === undefined ? undefined : packageOfSpecifier(value);
751
+ if (name !== undefined) {
752
+ found.add(name);
753
+ }
754
+ }
755
+ }
756
+ }
757
+ return found;
758
+ }
667
759
  /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
668
760
  function isInside(path, dir) {
669
761
  return dir === "" || path.startsWith(`${dir}/`);
@@ -735,6 +827,7 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
735
827
  }
736
828
  }
737
829
  const workflow = new Set(workflowTokens(host, targetRoot));
830
+ const workflowPreloads = preloadsOf(workflowCommands(host, targetRoot));
738
831
  const declared = [];
739
832
  const ran = new Set();
740
833
  const peers = new Map();
@@ -745,12 +838,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
745
838
  continue;
746
839
  }
747
840
  const tokens = scriptTokensOf(read.value);
841
+ const preloads = preloadsOf(manifestCommands(host, dir, read.value));
748
842
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
749
843
  const found = installedOf(host, dir, dependency.name);
750
844
  if (found === undefined && uninstalledHere.has(dir)) {
751
845
  continue;
752
846
  }
753
847
  declared.push(dependency);
848
+ if (preloads.has(dependency.name) || workflowPreloads.has(dependency.name)) {
849
+ ran.add(declarationKey(dependency));
850
+ }
754
851
  if (found === undefined) {
755
852
  continue;
756
853
  }
@@ -1,6 +1,6 @@
1
1
  import { DiagnosticCategory } from "@typescript/native/sync";
2
2
  import { baseUrlPaths } from "./base-url.js";
3
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.js";
3
+ import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
4
4
  import { SetupError } from "./setup-failure.js";
5
5
  import { writtenPath } from "./workspace-programs.js";
6
6
  /** Directory names discovery does not descend into. */
@@ -224,47 +224,6 @@ export function consumerProjects(engine, host, root) {
224
224
  function derivedId(targetRoot, configFile) {
225
225
  return relativePath(targetRoot, configFile) ?? configFile;
226
226
  }
227
- /**
228
- * The code of a configuration whose `extends` chain returns to a configuration it already
229
- * passed. The parse reports it and returns, and a snapshot opened from it never answers:
230
- * https://github.com/microsoft/TypeScript/issues/64754
231
- */
232
- const CIRCULAR_EXTENDS = 18000;
233
- /** The configuration files one parsed configuration's own `extends` names by path. */
234
- function extendedPaths(parsed, configFile) {
235
- const { raw } = parsed;
236
- const named = typeof raw === "object" && raw !== null ? Reflect.get(raw, "extends") : [];
237
- return (Array.isArray(named) ? named : [named])
238
- .filter((one) => typeof one === "string")
239
- .filter((one) => one.startsWith("./") || one.startsWith("../") || isAbsolutePath(one))
240
- .map((one) => resolvePath(dirnamePath(configFile), one));
241
- }
242
- /**
243
- * The `extends` chain from `configFile` to the first configuration it reaches twice, each
244
- * link by path, or undefined where no chain written by path returns.
245
- */
246
- function extendsCycle(engine, configFile, parsed) {
247
- const walk = (chain, at) => {
248
- for (const next of extendedPaths(at, chain.at(-1) ?? configFile)) {
249
- if (chain.includes(next)) {
250
- return [...chain, next];
251
- }
252
- let read;
253
- try {
254
- read = engine.parseConfigFile(next);
255
- }
256
- catch {
257
- continue;
258
- }
259
- const cycle = walk([...chain, next], read);
260
- if (cycle !== undefined) {
261
- return cycle;
262
- }
263
- }
264
- return undefined;
265
- };
266
- return walk([configFile], parsed);
267
- }
268
227
  /** The configuration files one parsed configuration's `references` name. */
269
228
  function referencesOf(parsed) {
270
229
  // The parse answers a directory reference as the directory; the compiler reads a
@@ -272,53 +231,26 @@ function referencesOf(parsed) {
272
231
  // https://github.com/microsoft/typescript-go/blob/89d5d5b2849a0db0957065889ca58536fa6d2e4a/internal/core/projectreference.go#L18
273
232
  return (parsed.projectReferences ?? []).map(({ path }) => path.endsWith(".json") ? path : joinPath(path, "tsconfig.json"));
274
233
  }
275
- /** Why one parsed configuration's own `extends` chain cannot be opened, where it is circular. */
276
- function circularity(engine, configFile, parsed) {
277
- const chain = extendsCycle(engine, configFile, parsed);
278
- if (chain === undefined && !parsed.errors.some(({ code }) => code === CIRCULAR_EXTENDS)) {
279
- return undefined;
280
- }
281
- return `TS${String(CIRCULAR_EXTENDS)}: Circularity detected while resolving configuration: ${(chain ?? [configFile]).join(" -> ")}`;
282
- }
283
234
  /**
284
- * Why a configuration the `references` reach, transitively, cannot be opened, where one's
285
- * `extends` chain is circular: the compiler opens every referenced project with the one
286
- * that references it.
235
+ * The code of a configuration whose `extends` chain returns to a configuration it already
236
+ * passed. A snapshot that opens two configurations whose `extends` chains reach one such
237
+ * cycle panics (`OwnerCache.AddOwner: entry not found`, typescript 7.1.0-dev.20261014.1),
238
+ * so the analysis refuses one before any snapshot is asked to open it.
287
239
  */
288
- function referencedCircularity(engine, references, visited) {
289
- for (const reference of references) {
290
- if (visited.has(reference)) {
291
- continue;
292
- }
293
- visited.add(reference);
294
- let parsed;
295
- try {
296
- parsed = engine.parseConfigFile(reference);
297
- }
298
- catch {
299
- continue;
300
- }
301
- const reason = circularity(engine, reference, parsed) ??
302
- referencedCircularity(engine, referencesOf(parsed), visited);
303
- if (reason !== undefined) {
304
- return reason;
305
- }
306
- }
307
- return undefined;
308
- }
240
+ const CIRCULAR_EXTENDS = 18000;
309
241
  /**
310
- * The configuration files one configuration's `references` name, or why the compiler
311
- * cannot read the configuration: it throws, or its `extends` chain or that of a
312
- * configuration it references is circular, which the analysis refuses before any
313
- * snapshot is asked to open it.
242
+ * The configurations one configuration file's `references` name, or why it cannot be read:
243
+ * the compiler throws, or its parse reports the `extends` chain circular.
314
244
  */
315
245
  function readConfiguration(engine, configFile) {
316
246
  try {
317
247
  const parsed = engine.parseConfigFile(configFile);
318
- const references = referencesOf(parsed);
319
- const reason = circularity(engine, configFile, parsed) ??
320
- referencedCircularity(engine, references, new Set([configFile]));
321
- return reason === undefined ? { references } : { reason };
248
+ const circular = parsed.errors.find(({ code }) => code === CIRCULAR_EXTENDS);
249
+ // The compiler leaves the message's `{0}` unfilled:
250
+ // https://github.com/microsoft/TypeScript/pull/64755
251
+ return circular === undefined
252
+ ? { references: referencesOf(parsed) }
253
+ : { reason: `TS${String(circular.code)}: ${circular.text.replace("{0}", () => configFile)}` };
322
254
  }
323
255
  catch (error) {
324
256
  return { reason: error instanceof Error ? error.message : String(error) };
@@ -10,14 +10,16 @@
10
10
  * value that is not a literal is not evaluated and not guessed, so a file it would
11
11
  * have named is entered by nothing here.
12
12
  */
13
- import { isArrayLiteralExpression, isAsExpression, isBinaryExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, isTemplateExpression, SyntaxKind, } from "@typescript/native/ast";
13
+ import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
14
14
  import { SymbolFlags } from "@typescript/native/sync";
15
15
  import { configurationFiles, filesNamedBy, moduleStrings, } from "./configuration-files.js";
16
+ import { buildScriptEntries } from "./build-scripts.js";
16
17
  import { globExpression } from "./glob.js";
17
18
  import { htmlEntries } from "./html-entries.js";
18
19
  import { documentEntries } from "./mdx-entries.js";
19
20
  import { dirnamePath, relativePath, resolvePath } from "./paths.js";
20
21
  import { UNANSWERED } from "./query.js";
22
+ import { computedLoadDirectory, leadingLiteral } from "./unseen-loads.js";
21
23
  import { workflowEntries } from "./workflow-steps.js";
22
24
  /** The extensions the test runner and the browser-test runner load a configuration from. */
23
25
  const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
@@ -210,7 +212,7 @@ function configurationEntries(host, targetRoot, files, parse, selected) {
210
212
  strings(dirnamePath(document.path), unselectedIn(document.path, document.strings));
211
213
  }
212
214
  for (const { file } of outside) {
213
- strings(dirnamePath(file.fileName), moduleStrings(file).map((one) => one.text));
215
+ strings(dirnamePath(file.fileName), unselected(file));
214
216
  }
215
217
  return found;
216
218
  }
@@ -398,17 +400,6 @@ function workersOf(project, file, files) {
398
400
  }
399
401
  return found;
400
402
  }
401
- /** The literal text a computed path starts with: a template's head, or a concatenation's first literal. */
402
- function leadingLiteral(node) {
403
- const at = unwrapped(node);
404
- if (isTemplateExpression(at)) {
405
- return at.head.text;
406
- }
407
- if (isBinaryExpression(at) && at.operatorToken.kind === SyntaxKind.PlusToken) {
408
- return literalOf(unwrapped(at.left)) ?? leadingLiteral(at.left);
409
- }
410
- return undefined;
411
- }
412
403
  /** Whether one call is `import.meta.glob(...)`. */
413
404
  function isMetaGlob(node) {
414
405
  if (!isCallExpression(node) || !isPropertyAccessExpression(node.expression)) {
@@ -432,11 +423,8 @@ function computedImportsOf(file, files) {
432
423
  const dir = dirnamePath(file.fileName);
433
424
  const found = [];
434
425
  const below = (prefix) => {
435
- const target = resolvePath(dir, prefix);
436
- if (!isModuleRelative(prefix) ||
437
- !prefix.endsWith("/") ||
438
- target === dir ||
439
- relativePath(target, dir) !== undefined) {
426
+ const target = computedLoadDirectory(dir, prefix);
427
+ if (target === undefined) {
440
428
  return;
441
429
  }
442
430
  for (const candidate of files.byName.values()) {
@@ -522,6 +510,9 @@ export function entryPoints(project, files, testFiles, host, targetRoot, parse,
522
510
  ...(names === undefined ? {} : { names }),
523
511
  });
524
512
  }
513
+ for (const { file, source } of buildScriptEntries(host, targetRoot, files, parse)) {
514
+ found.push({ file, rule: "bundler-entry", source, exports: true });
515
+ }
525
516
  for (const { file, source } of workflowEntries(host, targetRoot, files)) {
526
517
  found.push({ file, rule: "workflow-step", source, exports: true });
527
518
  }
@@ -106,11 +106,19 @@ export function evidenceRecords(input, detectors, disabled) {
106
106
  site: evidence.site,
107
107
  ...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
108
108
  ...(evidence.implementing === undefined ? {} : { implementing: evidence.implementing }),
109
+ ...(evidence.whileWritten === undefined ? {} : { whileWritten: evidence.whileWritten }),
109
110
  });
110
111
  }
111
112
  }
112
113
  return found;
113
114
  }
115
+ /**
116
+ * The records that hold given the declarations a write the mode counts stores into: one
117
+ * that holds only while written holds where its declaration is among them.
118
+ */
119
+ export function writtenRecords(records, written) {
120
+ return records.flatMap(({ whileWritten, ...record }) => whileWritten === undefined || written.has(record.id) ? [record] : []);
121
+ }
114
122
  /** The records that hold under one mode, beside the evidence a production mode found in a test file. */
115
123
  export function heldRecords(records, holding) {
116
124
  const found = [];
@@ -87,10 +87,12 @@ function testedByIn(input) {
87
87
  * published API, a public member of a published type included, is `certain` where the
88
88
  * run loaded every consumer the scope declared and declared one, `probable` where one did
89
89
  * not load, and `possible` where it declared none, whatever the configuration says of the
90
- * consumer set. A member an `in` expression tests by name is `possible`.
90
+ * consumer set. A member an `in` expression tests by name, and one a function outside the
91
+ * analysis fills, is `possible`.
91
92
  */
92
93
  function reachabilityClasses(input) {
93
94
  const tested = testedByIn(input);
95
+ const filled = input.boundary.filledOutside ?? new Set();
94
96
  const published = publishedIds(input.swept.matrix.union);
95
97
  const { declared } = input.boundary.consumers;
96
98
  const outside = declared.length === 0
@@ -99,12 +101,25 @@ function reachabilityClasses(input) {
99
101
  ? "certain"
100
102
  : "probable";
101
103
  return (id) => {
102
- if (tested.has(id)) {
104
+ if (tested.has(id) || filled.has(id)) {
103
105
  return "possible";
104
106
  }
105
107
  return published.has(id) ? outside : "certain";
106
108
  };
107
109
  }
110
+ /**
111
+ * Whether one test of dead code references a target symbol that is published API of a run
112
+ * that declares no consumer, which outside programs the run cannot see may use, so the
113
+ * target counts as live for the test and the test is no test of dead code.
114
+ */
115
+ export function testsUnseenAPI(input) {
116
+ if (input.boundary.consumers.declared.length > 0) {
117
+ return () => false;
118
+ }
119
+ const union = input.swept.matrix.union;
120
+ const published = publishedIds(union);
121
+ return (id) => testTargets(union, id).some((target) => published.has(target));
122
+ }
108
123
  /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
109
124
  function testTargets(union, id) {
110
125
  return (union.out[union.at(id)] ?? []).flatMap((edge) => {
@@ -151,13 +166,16 @@ function testUsedSubjects(input) {
151
166
  }
152
167
  /**
153
168
  * The ceiling the roots the run found put on one dead subject: a subject other declarations
154
- * reference, found dead only because no root reaches them, is `probable` unless
155
- * the configuration declares the roots complete, because a root the analysis cannot see
156
- * would make it live; every other declaration has none below `certain`.
169
+ * reference, found dead only because no root reaches them, is `probable` unless the
170
+ * configuration declares the roots complete and no configuration holding the subject loads
171
+ * a module the analysis cannot resolve, because a root the analysis cannot see would make
172
+ * it live; every other declaration has none below `certain`.
157
173
  */
158
- function rootsCeiling(input, finding, candidate) {
174
+ function rootsCeiling(input, finding, candidate, configurations) {
159
175
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
160
- return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
176
+ const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
177
+ const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
178
+ return unreachable && !complete ? "probable" : "certain";
161
179
  }
162
180
  /**
163
181
  * The component each dead declaration of the run falls in, and none for a live one, with
@@ -170,8 +188,24 @@ function findingComponents(swept, components, cascade) {
170
188
  held.set(member, component);
171
189
  }
172
190
  }
191
+ const listed = membersListed(swept);
192
+ return (id) => {
193
+ const component = held.get(id);
194
+ return component === undefined
195
+ ? undefined
196
+ : {
197
+ id: component.id,
198
+ root: component.roots.includes(id),
199
+ symbolCount: component.members.length,
200
+ deletableLines: component.deletableLines,
201
+ ...(cascade === "full" ? { members: listed(component.members) } : {}),
202
+ };
203
+ };
204
+ }
205
+ /** The members of a component as the run lists them, positioned and in the canonical order. */
206
+ function membersListed(swept) {
173
207
  const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
174
- const listed = (component) => component.members
208
+ return (members) => members
175
209
  .flatMap((member) => {
176
210
  const symbol = symbols.get(member);
177
211
  return symbol === undefined
@@ -188,18 +222,6 @@ function findingComponents(swept, components, cascade) {
188
222
  ];
189
223
  })
190
224
  .sort((a, b) => byPosition(a.position, b.position));
191
- return (id) => {
192
- const component = held.get(id);
193
- return component === undefined
194
- ? undefined
195
- : {
196
- id: component.id,
197
- root: component.roots.includes(id),
198
- symbolCount: component.members.length,
199
- deletableLines: component.deletableLines,
200
- ...(cascade === "full" ? { members: listed(component) } : {}),
201
- };
202
- };
203
225
  }
204
226
  /**
205
227
  * The run's dead components with each file a `DS1502` finding reports dead among them, so
@@ -277,11 +299,12 @@ export function completed(input, findings, kinds = KINDS) {
277
299
  let minted = computed;
278
300
  const dead = new Set(components.map((component) => component.id));
279
301
  const fallingWith = new Map();
280
- for (const { heldByTests } of swept.sweep.candidates) {
302
+ for (const { id, heldByTests } of swept.sweep.candidates) {
281
303
  if (heldByTests !== undefined) {
282
- fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
304
+ fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
283
305
  }
284
306
  }
307
+ const listed = membersListed(swept);
285
308
  const done = findings.map((finding) => {
286
309
  const row = kinds.get(finding.code);
287
310
  if (row === undefined) {
@@ -326,11 +349,25 @@ export function completed(input, findings, kinds = KINDS) {
326
349
  let component = found;
327
350
  if (component === undefined) {
328
351
  minted += 1;
352
+ const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
329
353
  component = {
330
354
  id: componentId(minted),
331
355
  root: true,
332
- symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
356
+ symbolCount: falling?.length ?? 1,
333
357
  deletableLines: 0,
358
+ ...(input.config.reporters.cascade !== "full"
359
+ ? {}
360
+ : falling === undefined
361
+ ? {
362
+ members: [
363
+ {
364
+ ref: finding.symbol.ref,
365
+ name: finding.symbol.name,
366
+ position: finding.position,
367
+ },
368
+ ],
369
+ }
370
+ : { members: listed(falling) }),
334
371
  };
335
372
  }
336
373
  const counts = candidate === undefined
@@ -341,7 +378,11 @@ export function completed(input, findings, kinds = KINDS) {
341
378
  ? candidate !== undefined && testUsed.declarations.has(judged.id)
342
379
  : finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
343
380
  const reachabilityClass = used && candidate?.testOfDeadCode !== true ? "possible" : ownClass;
344
- const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate)), judged === undefined ? "certain" : ceilingOf(judged.id));
381
+ const configurations = candidate?.configurations ??
382
+ (judged === undefined && !part
383
+ ? swept.matrix.configurations
384
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
385
+ const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate, configurations)), judged === undefined ? "certain" : ceilingOf(judged.id));
345
386
  const generated = input.files.generated?.has(finding.position.path) === true;
346
387
  const fixability = generated
347
388
  ? "none"
@@ -372,10 +413,7 @@ export function completed(input, findings, kinds = KINDS) {
372
413
  generated,
373
414
  component,
374
415
  retainedBy: [],
375
- configurations: candidate?.configurations ??
376
- (judged === undefined && !part
377
- ? swept.matrix.configurations
378
- : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
416
+ configurations,
379
417
  consumersLoaded: input.boundary.consumers.loaded,
380
418
  fixability,
381
419
  severity: severityOf(input, finding.code, row.defaultSeverity),
@@ -28,7 +28,8 @@ function overlapClause(overlap, part) {
28
28
  * caller may pass a function used as a value more arguments than it declares, so there one
29
29
  * a read parameter follows is renamed to an underscore name, which the compilers read as
30
30
  * declared unread, and one no read parameter follows is deleted: kept where the call's
31
- * inference may read its annotation, at `probable` where nothing reads the annotation.
31
+ * inference may read its annotation, at `probable` where nothing reads the annotation or
32
+ * where it is a name a contextually typed object pattern binds.
32
33
  */
33
34
  function parameterVerdict(part, free, freeButValued) {
34
35
  const place = part.parameter;
@@ -45,7 +46,8 @@ function parameterVerdict(part, free, freeButValued) {
45
46
  if (place?.annotated === "steers") {
46
47
  return undefined;
47
48
  }
48
- return { fix: "delete", confidence: place?.annotated === "free" ? "probable" : "certain" };
49
+ const unseen = place?.annotated === "free" || place?.contextualPattern === true;
50
+ return { fix: "delete", confidence: unseen ? "probable" : "certain" };
49
51
  }
50
52
  /**
51
53
  * Whether one part is reported. A parameter needs its function's signature free to
@@ -1,7 +1,7 @@
1
1
  import { positionKey } from "../position.js";
2
2
  import { declarationKey } from "../ref.js";
3
- /** The containers whose type parameters `DS1303` reports: a function and a class method. */
4
- const SIGNATURES = new Set(["function", "method"]);
3
+ /** The containers whose type parameters `DS1303` reports: a function, a class method and a type alias. */
4
+ const SIGNATURES = new Set(["function", "method", "type"]);
5
5
  function indexed(input) {
6
6
  const { matrix, sweep } = input.swept;
7
7
  return {
@@ -165,10 +165,11 @@ function unusedEnumMembers(run) {
165
165
  return unnamed(run, "enum-member").map((symbol) => findingAbout(symbol, "DS1302", `enum member ${symbol.name} is named nowhere`));
166
166
  }
167
167
  /**
168
- * `DS1303`: a type parameter of a function or a class method that neither the
169
- * declaration's signature nor its body names. A type parameter of a class, an interface,
170
- * a type alias or a method signature belongs to a type declaration and is never
171
- * reported, because a phantom parameter makes two instantiations distinct types.
168
+ * `DS1303`: a type parameter of a function, a class method or a type alias that neither
169
+ * the declaration's signature nor its body names. An alias names no type of its own, so
170
+ * a parameter its type never names makes no two instantiations distinct. One of a class,
171
+ * an interface or a method signature is never reported: a class or an interface merges
172
+ * only with declarations of the same parameters.
172
173
  */
173
174
  function unusedTypeParameters(run) {
174
175
  return unnamed(run, "type-parameter")
@@ -1,3 +1,4 @@
1
+ import { testsUnseenAPI } from "./completion.js";
1
2
  import { unreachableExports } from "./visibility-narrowing.js";
2
3
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
3
4
  const MEMBERS = new Set([
@@ -48,7 +49,7 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
48
49
  function codeOf(judged) {
49
50
  const { candidate, symbol } = judged;
50
51
  if (candidate.testOfDeadCode) {
51
- return "DS1005";
52
+ return judged.testsUnseenAPI ? undefined : "DS1005";
52
53
  }
53
54
  if ((judged.test && !candidate.unreferencedTest) || NOT_THIS_FAMILY.has(symbol.kind)) {
54
55
  return undefined;
@@ -199,6 +200,7 @@ function familyFindings(input, fallen) {
199
200
  const union = input.swept.matrix.union;
200
201
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
201
202
  const unreachable = unreachableExports(input);
203
+ const unseenAPI = testsUnseenAPI(input);
202
204
  let brands;
203
205
  const found = [];
204
206
  for (const candidate of input.swept.sweep.candidates) {
@@ -217,6 +219,7 @@ function familyFindings(input, fallen) {
217
219
  test: union.test[at] === true,
218
220
  deprecated: input.deprecated.has(symbol.id),
219
221
  unreachable: unreachable.has(symbol.id),
222
+ testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
220
223
  };
221
224
  const code = codeOf(judged);
222
225
  if (code === undefined ||
package/dist/src/graph.js CHANGED
@@ -34,9 +34,9 @@ const SWEPT = {
34
34
  * Whether a root of each kind names a caller, which holds it live under both relations: a
35
35
  * tool reads what it loads and what its strings name, a runtime runs a test, setup or
36
36
  * worker file, a page's module script, a command, a script or a workflow step, the manifest
37
- * names what enters the package, an alias global stands for its export, and an entry file,
38
- * a configured root and a pattern assert a caller. A library's published API only supposes
39
- * an unseen consumer, so it is live under reachability alone.
37
+ * names what enters the package, an alias global stands for its export, an outside
38
+ * declaration names a global interface merging into it, and an entry file, a configured root
39
+ * and a pattern assert a caller. A published API supposes a consumer: reachability alone.
40
40
  */
41
41
  const NAMES_A_CALLER = {
42
42
  "entry-file": true,
@@ -49,8 +49,10 @@ const NAMES_A_CALLER = {
49
49
  pattern: true,
50
50
  "type-error": true,
51
51
  "type-query-alias": true,
52
+ "global-augmentation": true,
52
53
  "configuration-file": true,
53
54
  "configuration-string": true,
55
+ "bundler-entry": true,
54
56
  "test-runner": true,
55
57
  "browser-tests": true,
56
58
  worker: true,