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

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 (57) hide show
  1. package/dist/src/analysis.js +27 -5
  2. package/dist/src/augmentations.js +62 -0
  3. package/dist/src/build-scripts.js +437 -0
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/convention-rows.js +39 -2
  6. package/dist/src/conventions.js +15 -0
  7. package/dist/src/dependencies.js +89 -0
  8. package/dist/src/entry-points.js +9 -18
  9. package/dist/src/findings/completion.js +62 -26
  10. package/dist/src/findings/intra-function.js +4 -2
  11. package/dist/src/findings/reads-and-writes.js +7 -6
  12. package/dist/src/findings/unused-declarations.js +4 -1
  13. package/dist/src/graph.js +5 -3
  14. package/dist/src/intra-function-parts.js +45 -17
  15. package/dist/src/kinds.js +7 -7
  16. package/dist/src/manifest.js +18 -0
  17. package/dist/src/matrix.js +38 -4
  18. package/dist/src/report.js +23 -4
  19. package/dist/src/reporters/sarif.js +12 -8
  20. package/dist/src/roots.js +138 -11
  21. package/dist/src/serialization-contract.js +258 -8
  22. package/dist/src/sweep.js +7 -9
  23. package/dist/src/test-globals.js +234 -0
  24. package/dist/src/type-errors.js +11 -3
  25. package/dist/src/unseen-loads.js +107 -0
  26. package/dist/src/verbs/analyze.js +24 -2
  27. package/dist/src/yaml-document.js +355 -0
  28. package/jsr.json +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis.ts +45 -7
  31. package/src/augmentations.ts +76 -1
  32. package/src/build-scripts.ts +564 -0
  33. package/src/conformance-record.ts +1 -1
  34. package/src/convention-rows.ts +47 -2
  35. package/src/conventions.ts +16 -0
  36. package/src/dependencies.ts +96 -0
  37. package/src/entry-points.ts +10 -25
  38. package/src/file-facts.ts +3 -0
  39. package/src/findings/completion.ts +72 -28
  40. package/src/findings/intra-function.ts +4 -2
  41. package/src/findings/reads-and-writes.ts +7 -6
  42. package/src/findings/unused-declarations.ts +6 -1
  43. package/src/graph.ts +5 -3
  44. package/src/intra-function-parts.ts +55 -16
  45. package/src/kinds.ts +7 -7
  46. package/src/manifest.ts +21 -3
  47. package/src/matrix.ts +51 -4
  48. package/src/report.ts +56 -16
  49. package/src/reporters/sarif.ts +16 -8
  50. package/src/roots.ts +162 -12
  51. package/src/serialization-contract.ts +309 -7
  52. package/src/sweep.ts +21 -9
  53. package/src/test-globals.ts +275 -0
  54. package/src/type-errors.ts +14 -3
  55. package/src/unseen-loads.ts +145 -0
  56. package/src/verbs/analyze.ts +28 -2
  57. package/src/yaml-document.ts +377 -0
@@ -12,7 +12,14 @@ import { documentStrings } from "./configuration-files.js";
12
12
  import { globExpression } from "./glob.js";
13
13
  import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.js";
14
14
  import { satisfies } from "./version-range.js";
15
+ import { yamlDocument } from "./yaml-document.js";
15
16
  const JSON_EXTENSION = ".json";
17
+ /**
18
+ * A configuration file its tool reads as a YAML document, read as the JSON document it
19
+ * denotes. Its nodes' offsets are that JSON text's, so no move may read one: a failure's
20
+ * line would name the wrong line.
21
+ */
22
+ const YAML_NAME = /\.ya?ml$/u;
16
23
  /** A configuration file with no extension that its tool reads as a JSON document. */
17
24
  const JSON_RC = /^\.[\w-]+rc$/u;
18
25
  /** The sections an installed package is required by, so its absence fails the run. */
@@ -614,6 +621,14 @@ function readingFiles(host, parse, dir, files) {
614
621
  catch {
615
622
  continue;
616
623
  }
624
+ if (YAML_NAME.test(name)) {
625
+ const value = yamlDocument(text);
626
+ if (value !== undefined) {
627
+ const json = JSON.stringify(value);
628
+ found.push({ name, text: json, file: parse(`${path}${JSON_EXTENSION}`, json) });
629
+ }
630
+ continue;
631
+ }
617
632
  found.push({
618
633
  name,
619
634
  text,
@@ -664,6 +664,91 @@ 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 packages one manifest's scripts preload by a runtime flag's bare module specifier,
701
+ * a flag counting only in a command whose program passes it to the runtime, and for Node
702
+ * only before the script it runs, whose own arguments come after it.
703
+ */
704
+ function scriptPreloadsOf(manifest) {
705
+ const scripts = manifest["scripts"];
706
+ const found = new Set();
707
+ if (!isRecord(scripts)) {
708
+ return found;
709
+ }
710
+ for (const script of Object.values(scripts)) {
711
+ if (typeof script !== "string") {
712
+ continue;
713
+ }
714
+ for (const command of script.split(COMMAND_SEPARATORS)) {
715
+ const tokens = scriptTokens(command);
716
+ let at = 0;
717
+ while (at < tokens.length &&
718
+ (ASSIGNMENT.test(tokens[at] ?? "") || LAUNCHERS.has(tokens[at] ?? ""))) {
719
+ at += 1;
720
+ }
721
+ const path = tokens[at] ?? "";
722
+ const program = path.slice(path.lastIndexOf("/") + 1);
723
+ if (!PRELOADING_PROGRAMS.has(program)) {
724
+ continue;
725
+ }
726
+ for (at += 1; at < tokens.length; at += 1) {
727
+ const token = tokens[at] ?? "";
728
+ if (!token.startsWith("-")) {
729
+ if (program === "node") {
730
+ break;
731
+ }
732
+ continue;
733
+ }
734
+ const equals = token.indexOf("=");
735
+ const flag = equals < 0 ? token : token.slice(0, equals);
736
+ if (!PRELOAD_FLAGS.has(flag)) {
737
+ continue;
738
+ }
739
+ if (equals < 0) {
740
+ at += 1;
741
+ }
742
+ const value = equals < 0 ? tokens[at] : token.slice(equals + 1);
743
+ const name = value === undefined ? undefined : packageOfSpecifier(value);
744
+ if (name !== undefined) {
745
+ found.add(name);
746
+ }
747
+ }
748
+ }
749
+ }
750
+ return found;
751
+ }
667
752
  /** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
668
753
  function isInside(path, dir) {
669
754
  return dir === "" || path.startsWith(`${dir}/`);
@@ -745,12 +830,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
745
830
  continue;
746
831
  }
747
832
  const tokens = scriptTokensOf(read.value);
833
+ const preloads = scriptPreloadsOf(read.value);
748
834
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
749
835
  const found = installedOf(host, dir, dependency.name);
750
836
  if (found === undefined && uninstalledHere.has(dir)) {
751
837
  continue;
752
838
  }
753
839
  declared.push(dependency);
840
+ if (preloads.has(dependency.name)) {
841
+ ran.add(declarationKey(dependency));
842
+ }
754
843
  if (found === undefined) {
755
844
  continue;
756
845
  }
@@ -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
  }
@@ -105,6 +105,19 @@ function reachabilityClasses(input) {
105
105
  return published.has(id) ? outside : "certain";
106
106
  };
107
107
  }
108
+ /**
109
+ * Whether one test of dead code references a target symbol that is published API of a run
110
+ * that declares no consumer, which outside programs the run cannot see may use, so the
111
+ * target counts as live for the test and the test is no test of dead code.
112
+ */
113
+ export function testsUnseenAPI(input) {
114
+ if (input.boundary.consumers.declared.length > 0) {
115
+ return () => false;
116
+ }
117
+ const union = input.swept.matrix.union;
118
+ const published = publishedIds(union);
119
+ return (id) => testTargets(union, id).some((target) => published.has(target));
120
+ }
108
121
  /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
109
122
  function testTargets(union, id) {
110
123
  return (union.out[union.at(id)] ?? []).flatMap((edge) => {
@@ -151,13 +164,16 @@ function testUsedSubjects(input) {
151
164
  }
152
165
  /**
153
166
  * 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`.
167
+ * reference, found dead only because no root reaches them, is `probable` unless the
168
+ * configuration declares the roots complete and no configuration holding the subject loads
169
+ * a module the analysis cannot resolve, because a root the analysis cannot see would make
170
+ * it live; every other declaration has none below `certain`.
157
171
  */
158
- function rootsCeiling(input, finding, candidate) {
172
+ function rootsCeiling(input, finding, candidate, configurations) {
159
173
  const unreachable = candidate?.relation === "reachability" || finding.unreachableOnly === true;
160
- return unreachable && !input.config.ts.rootsComplete ? "probable" : "certain";
174
+ const unseen = new Set((input.files.unseenLoads ?? []).map((load) => load.configuration));
175
+ const complete = input.config.ts.rootsComplete && !configurations.some((one) => unseen.has(one));
176
+ return unreachable && !complete ? "probable" : "certain";
161
177
  }
162
178
  /**
163
179
  * The component each dead declaration of the run falls in, and none for a live one, with
@@ -170,8 +186,24 @@ function findingComponents(swept, components, cascade) {
170
186
  held.set(member, component);
171
187
  }
172
188
  }
189
+ const listed = membersListed(swept);
190
+ return (id) => {
191
+ const component = held.get(id);
192
+ return component === undefined
193
+ ? undefined
194
+ : {
195
+ id: component.id,
196
+ root: component.roots.includes(id),
197
+ symbolCount: component.members.length,
198
+ deletableLines: component.deletableLines,
199
+ ...(cascade === "full" ? { members: listed(component.members) } : {}),
200
+ };
201
+ };
202
+ }
203
+ /** The members of a component as the run lists them, positioned and in the canonical order. */
204
+ function membersListed(swept) {
173
205
  const symbols = new Map(swept.matrix.union.symbols.map((symbol) => [symbol.id, symbol]));
174
- const listed = (component) => component.members
206
+ return (members) => members
175
207
  .flatMap((member) => {
176
208
  const symbol = symbols.get(member);
177
209
  return symbol === undefined
@@ -188,18 +220,6 @@ function findingComponents(swept, components, cascade) {
188
220
  ];
189
221
  })
190
222
  .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
223
  }
204
224
  /**
205
225
  * The run's dead components with each file a `DS1502` finding reports dead among them, so
@@ -277,11 +297,12 @@ export function completed(input, findings, kinds = KINDS) {
277
297
  let minted = computed;
278
298
  const dead = new Set(components.map((component) => component.id));
279
299
  const fallingWith = new Map();
280
- for (const { heldByTests } of swept.sweep.candidates) {
300
+ for (const { id, heldByTests } of swept.sweep.candidates) {
281
301
  if (heldByTests !== undefined) {
282
- fallingWith.set(heldByTests, (fallingWith.get(heldByTests) ?? 0) + 1);
302
+ fallingWith.set(heldByTests, [...(fallingWith.get(heldByTests) ?? []), id]);
283
303
  }
284
304
  }
305
+ const listed = membersListed(swept);
285
306
  const done = findings.map((finding) => {
286
307
  const row = kinds.get(finding.code);
287
308
  if (row === undefined) {
@@ -326,11 +347,25 @@ export function completed(input, findings, kinds = KINDS) {
326
347
  let component = found;
327
348
  if (component === undefined) {
328
349
  minted += 1;
350
+ const falling = symbol?.kind === "interface" ? fallingWith.get(symbol.id) : undefined;
329
351
  component = {
330
352
  id: componentId(minted),
331
353
  root: true,
332
- symbolCount: symbol?.kind === "interface" ? (fallingWith.get(symbol.id) ?? 1) : 1,
354
+ symbolCount: falling?.length ?? 1,
333
355
  deletableLines: 0,
356
+ ...(input.config.reporters.cascade !== "full"
357
+ ? {}
358
+ : falling === undefined
359
+ ? {
360
+ members: [
361
+ {
362
+ ref: finding.symbol.ref,
363
+ name: finding.symbol.name,
364
+ position: finding.position,
365
+ },
366
+ ],
367
+ }
368
+ : { members: listed(falling) }),
334
369
  };
335
370
  }
336
371
  const counts = candidate === undefined
@@ -341,7 +376,11 @@ export function completed(input, findings, kinds = KINDS) {
341
376
  ? candidate !== undefined && testUsed.declarations.has(judged.id)
342
377
  : finding.code === FILE_NEVER_IMPORTED && testUsed.files.has(finding.position.path);
343
378
  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));
379
+ const configurations = candidate?.configurations ??
380
+ (judged === undefined && !part
381
+ ? swept.matrix.configurations
382
+ : (swept.matrix.heldIn[at] ?? swept.matrix.configurations));
383
+ const untested = capped(capped(capped(ownClass, row.maxClass), rootsCeiling(input, finding, candidate, configurations)), judged === undefined ? "certain" : ceilingOf(judged.id));
345
384
  const generated = input.files.generated?.has(finding.position.path) === true;
346
385
  const fixability = generated
347
386
  ? "none"
@@ -372,10 +411,7 @@ export function completed(input, findings, kinds = KINDS) {
372
411
  generated,
373
412
  component,
374
413
  retainedBy: [],
375
- configurations: candidate?.configurations ??
376
- (judged === undefined && !part
377
- ? swept.matrix.configurations
378
- : (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
414
+ configurations,
379
415
  consumersLoaded: input.boundary.consumers.loaded,
380
416
  fixability,
381
417
  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,
@@ -772,10 +772,9 @@ function calleeSteers(project, signature, at) {
772
772
  }
773
773
  /**
774
774
  * The parts with each name of `contextual` judged where the compiler gives its function a
775
- * contextual type: the code handing the argument may act on what an object pattern reads,
776
- * so each such name is read, and an annotated parameter is marked with whether the call's
777
- * inference may read its annotation. A question the checker leaves unanswered keeps the
778
- * name read.
775
+ * contextual type: a name an object pattern binds is marked as one, and an annotated
776
+ * parameter with whether the call's inference may read its annotation. A question the
777
+ * checker leaves unanswered keeps the name read.
779
778
  */
780
779
  function withContextualRead(project, parts, contextual) {
781
780
  if (contextual.length === 0) {
@@ -791,15 +790,17 @@ function withContextualRead(project, parts, contextual) {
791
790
  continue;
792
791
  }
793
792
  const place = one.part.parameter;
794
- judged.set(one.part, one.pattern || answer === UNANSWERED || place === undefined
793
+ judged.set(one.part, answer === UNANSWERED || place === undefined
795
794
  ? { ...one.part, used: true }
796
- : {
797
- ...one.part,
798
- parameter: {
799
- ...place,
800
- annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
801
- },
802
- });
795
+ : one.pattern
796
+ ? { ...one.part, parameter: { ...place, contextualPattern: true } }
797
+ : {
798
+ ...one.part,
799
+ parameter: {
800
+ ...place,
801
+ annotated: calleeSteers(project, one.signature, one.at) ? "steers" : "free",
802
+ },
803
+ });
803
804
  }
804
805
  return parts.map((part) => judged.get(part) ?? part);
805
806
  }
@@ -884,15 +885,28 @@ function functionOf(block) {
884
885
  }
885
886
  }
886
887
  /**
887
- * The locals one block declares with `let`, `const` or `var`, each with the names of the
888
- * block that spell it. A `var` of a nested block is one where no nested function of its
889
- * function names it, since the function scope holds it past the block's end.
888
+ * The locals one block declares with `let`, `const`, `var` or a function declaration
889
+ * inside a function, each with the names of the block that spell it. A `var` of a nested
890
+ * block is one where no nested function of its function names it, since the function scope
891
+ * holds it past the block's end.
890
892
  */
891
893
  function localsOf(block) {
892
894
  const declared = [];
893
895
  const scope = functionOf(block);
894
896
  const outlives = scope?.body !== block;
895
897
  block.statements.forEach((statement, at) => {
898
+ if (isFunctionDeclaration(statement)) {
899
+ if (scope !== undefined && statement.name !== undefined && statement.body !== undefined) {
900
+ declared.push({
901
+ name: statement.name,
902
+ statement: at,
903
+ initialized: true,
904
+ outlives: false,
905
+ hoisted: true,
906
+ });
907
+ }
908
+ return;
909
+ }
896
910
  if (!isVariableStatement(statement)) {
897
911
  return;
898
912
  }
@@ -902,6 +916,11 @@ function localsOf(block) {
902
916
  return;
903
917
  }
904
918
  for (const declaration of list.declarations) {
919
+ // A write to a local typed `never` is the compiler's exhaustiveness check, which
920
+ // fails to compile once a case goes unhandled.
921
+ if (declaration.type?.kind === SyntaxKind.NeverKeyword) {
922
+ continue;
923
+ }
905
924
  for (const name of boundNames(declaration.name)) {
906
925
  if (!blockScoped &&
907
926
  outlives &&
@@ -913,6 +932,7 @@ function localsOf(block) {
913
932
  statement: at,
914
933
  initialized: declaration.initializer !== undefined,
915
934
  outlives: !blockScoped && outlives,
935
+ hoisted: false,
916
936
  });
917
937
  }
918
938
  }
@@ -953,12 +973,20 @@ function deadStores(file, root, block, holder, locals, ids) {
953
973
  used: false,
954
974
  });
955
975
  for (const local of locals) {
956
- const uses = local.uses.filter((use) => readsBinding(use, local.name, ids));
976
+ const own = local.hoisted ? block.statements[local.statement] : undefined;
977
+ // A function's reads of itself keep it no more than its absence would.
978
+ const uses = local.uses.filter((use) => readsBinding(use, local.name, ids) && (own === undefined || !within(use, own)));
957
979
  if (!local.outlives && uses.every(isPlainWrite)) {
958
- const targets = [...(local.initialized ? [local.name] : []), ...uses];
980
+ // A declaration with no initializer writes `undefined`, the one store left to name.
981
+ const writes = [...(local.initialized ? [local.name] : []), ...uses];
982
+ const targets = writes.length === 0 ? [local.name] : writes;
959
983
  found.push(...targets.map((target) => deadAt(local, target)));
960
984
  continue;
961
985
  }
986
+ // A read anywhere in the block reads a function declaration, which is written first.
987
+ if (local.hoisted) {
988
+ continue;
989
+ }
962
990
  if (uses.some((use) => inNestedFunction(use, block))) {
963
991
  continue;
964
992
  }