@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
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  deadset-ts finds dead code in TypeScript projects, down to class members, type members and dead stores inside functions, and writes a report your CI can fail on.
6
6
 
7
- It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261010.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
7
+ It reads your `tsconfig.json` projects, `allowJs` JavaScript and Vue, Svelte and Astro scripts included, with the type information of TypeScript 7.1.0-dev.20261014.1, its one dependency. Your project keeps its own TypeScript version. A type error under that version skips the function or statement that holds it. deadset-ts reports and never edits your code. It is pre-release, tested only on Linux, needs Node.js 24 or later and is licensed under GPL-3.0-or-later.
8
8
 
9
9
  ## Why use it
10
10
 
@@ -20,7 +20,7 @@ import { enumGroup } from "./enum-group.js";
20
20
  import { generatedFile, generatedFilesOf } from "./generated-file.js";
21
21
  import { inOperandNames } from "./in-operands.js";
22
22
  import { sourceFilesOf } from "./source-files.js";
23
- import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
23
+ import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, writtenRecords, } from "./exempt.js";
24
24
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
25
25
  import { globExpression } from "./glob.js";
26
26
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
@@ -41,9 +41,12 @@ import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.
41
41
  import { reflectiveLookup } from "./reflective-lookup.js";
42
42
  import { references, testFileRulesOf } from "./references.js";
43
43
  import { roots, unmatchedEverywhere, } from "./roots.js";
44
- import { serializationContract } from "./serialization-contract.js";
44
+ import { decodedMembers, serializationContract } from "./serialization-contract.js";
45
45
  import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
46
+ import { mergedGlobalInterfaces } from "./augmentations.js";
47
+ import { unseenLoadsOf } from "./unseen-loads.js";
46
48
  import { joinedTestFiles, withJoinedTests } from "./test-joins.js";
49
+ import { GLOBALS_RUNNER, namesTestGlobals, withTestGlobals } from "./test-globals.js";
47
50
  import { supportReferences, testSupportFiles } from "./test-support.js";
48
51
  import { diagnosticsOf, ownedAs, runSession, } from "./session.js";
49
52
  import { accessorsOf, storesOf } from "./stores.js";
@@ -195,7 +198,7 @@ function readProjects(engine, host, scope, config, stage) {
195
198
  const joins = discovered.derived
196
199
  ? joinedTestFiles(engine, host, absoluteRoot, discovered.configFiles, testPatterns)
197
200
  : new Map();
198
- const programs = withBaseUrlPaths(engine, host, [...discovered.configFiles, ...consumerOf.keys()], workspace?.programs ?? new Map());
201
+ const programs = withTestGlobals(engine, host, parse, discovered.configFiles, withBaseUrlPaths(engine, host, [...discovered.configFiles, ...consumerOf.keys()], workspace?.programs ?? new Map()));
199
202
  const session = runSession(engine, [...discovered.configFiles, ...consumerOf.keys()], (opened) => {
200
203
  const consumer = consumerOf.get(opened.configFile);
201
204
  const setup = consumer === undefined ? workspace?.unbuildable(opened.configFile) : undefined;
@@ -499,6 +502,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
499
502
  : { generated: projectRead.generated }),
500
503
  }, detectors, disabled);
501
504
  const components = project.ownSourceFiles().filter(isComponentFile);
505
+ const injected = project.program
506
+ .getSourceFileNames()
507
+ .some((name) => name.endsWith(`/node_modules/${GLOBALS_RUNNER}/globals.d.ts`));
502
508
  const pathOf = (file) => relativePath(targetRoot, file.fileName) ?? file.fileName;
503
509
  return {
504
510
  projectRead,
@@ -506,17 +512,29 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
506
512
  testFiles: resolved.testFilePaths,
507
513
  evidence,
508
514
  componentFiles: components.map(pathOf),
515
+ augmented: mergedGlobalInterfaces(project, projectRead.held, sourceFilesOf(project, targetRoot)).map((id) => ({ id, kind: "global-augmentation", source: "" })),
509
516
  imports: new Map(project.ownSourceFiles().flatMap((file) => {
510
517
  const path = relativePath(targetRoot, file.fileName);
511
- return path === undefined
512
- ? []
513
- : [[path, file.imports.filter(isStringLiteralLikeNode).map((one) => one.text)]];
518
+ if (path === undefined) {
519
+ return [];
520
+ }
521
+ const specifiers = file.imports.filter(isStringLiteralLikeNode).map((one) => one.text);
522
+ // A file calling the globals a program declares is written for their runner.
523
+ return [
524
+ [
525
+ path,
526
+ injected && namesTestGlobals(project, file)
527
+ ? [...specifiers, GLOBALS_RUNNER]
528
+ : specifiers,
529
+ ],
530
+ ];
514
531
  })),
515
532
  componentWarnings: components.flatMap((file) => readComponent(file.originalText, extensionOf(file.fileName)).warnings.map((warning) => ({
516
533
  path: pathOf(file),
517
534
  line: lineOf(file.originalText, warning.offset),
518
535
  reason: warning.reason,
519
536
  }))),
537
+ decoded: decodedMembers(project, projectRead.held, productionFiles(project, targetRoot, resolved.testFilePaths), consumers),
520
538
  extra: extra(project, projectRead, resolved.references, resolved.testFilePaths),
521
539
  };
522
540
  });
@@ -532,11 +550,24 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
532
550
  // Test-support code is code only test code imports in any configuration of the run: a
533
551
  // configuration that holds no test file says nothing about what the tests import.
534
552
  const { support, testUsed } = testSupportFiles(withOutside.flatMap((one) => one.projectRead.held.symbols), withOutside.flatMap((one) => one.references), withOutside.flatMap((one) => one.projectRead.rooted.liveUnderReachability), withOutside.flatMap((one) => one.testFiles), testEvidence(host, targetRoot, withOutside));
535
- const settled = settledSatisfaction(withOutside.map((one) => ({ evidence: one.evidence, held: one.projectRead.held })), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
553
+ // A member a decode fills or a copy carries is written: a value decoded and sent again carries it on.
554
+ const written = new Set([
555
+ ...countedWrites([...withOutside.flatMap((one) => one.references), ...read.consumers.references], support, mode),
556
+ ...withOutside.flatMap((one) => [
557
+ ...one.decoded.filled,
558
+ ...one.decoded.parsed,
559
+ ...one.decoded.copied,
560
+ ]),
561
+ ]);
562
+ const evidence = withOutside.map((one) => writtenRecords(one.evidence, written));
563
+ const settled = settledSatisfaction(withOutside.map((one, index) => ({
564
+ evidence: evidence[index] ?? [],
565
+ held: one.projectRead.held,
566
+ })), [...withOutside.flatMap((one) => one.references), ...read.consumers.references]);
536
567
  const answers = withOutside.map((one, index) => {
537
568
  const { projectRead } = one;
538
569
  const satisfied = settled[index];
539
- const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
570
+ const detected = heldRecords(satisfied?.evidence ?? evidence[index] ?? [], {
540
571
  mode,
541
572
  testFiles: new Set([...one.testFiles, ...support]),
542
573
  });
@@ -549,7 +580,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
549
580
  configuration: projectRead.configuration,
550
581
  symbols: projectRead.held.symbols,
551
582
  references: [...made, ...evidenceReferences(projectRead.held.symbols, detected.inTestFiles)],
552
- roots: projectRead.rooted.liveUnderReachability,
583
+ // The augmentations are rooted after test support is read: an outside declaration
584
+ // naming one calls nothing of its file.
585
+ roots: [...projectRead.rooted.liveUnderReachability, ...one.augmented],
553
586
  testFiles: [...one.testFiles, ...supportFiles],
554
587
  ...(supportFiles.length === 0 ? {} : { supportFiles }),
555
588
  ...(testUsedFiles.length === 0 ? {} : { testUsedFiles }),
@@ -572,7 +605,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
572
605
  .filter((reference) => reference.resolution === "by-name")
573
606
  .map((reference) => reference.to),
574
607
  ...exempt.filter(isUnansweredRecord).map((record) => record.id),
608
+ ...one.decoded.unanswered,
575
609
  ],
610
+ filledOutside: one.decoded.filled,
576
611
  typeErrorSkips: projectRead.typeErrorSkips,
577
612
  unmappedEntries: projectRead.rooted.unmappedEntries,
578
613
  skipped: projectRead.skipped,
@@ -662,6 +697,13 @@ function evidenceReferences(symbols, inTestFiles) {
662
697
  ];
663
698
  });
664
699
  }
700
+ /**
701
+ * The declarations a write the mode counts stores into, a write a test-support file
702
+ * makes being a test's.
703
+ */
704
+ function countedWrites(references, support, mode) {
705
+ return new Set(supportReferences(references, support).flatMap((reference) => reference.use === "write" && !(mode.production && reference.test) ? [reference.to] : []));
706
+ }
665
707
  /**
666
708
  * The declarations every reference the mode counts stores into, a loaded consumer's
667
709
  * reference counting as a read: what the write-only kind would report but for a record.
@@ -746,6 +788,11 @@ function sweptOf(read, input) {
746
788
  export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
747
789
  return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
748
790
  }
791
+ /** The project's own files below the target root that are no test file. */
792
+ function productionFiles(project, targetRoot, testFiles) {
793
+ const tests = new Set(testFiles);
794
+ return [...sourceFilesOf(project, targetRoot).byPath].flatMap(([path, file]) => tests.has(path) ? [] : [file]);
795
+ }
749
796
  /** Reads a run's projects with every per-project fact an emitter reads. */
750
797
  export function readEmitterRun(engine, host, scope, config, mode, detectors) {
751
798
  const targetRoot = scope.target.path;
@@ -761,8 +808,9 @@ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
761
808
  directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
762
809
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
763
810
  parts: projectParts(project, read.held, references, targetRoot),
811
+ unseenLoads: unseenLoadsOf(project, read.configuration, targetRoot, testFiles),
764
812
  }));
765
- // Whether a declaration carries the marker decides its code, so one whose marker went
813
+ // Whether a declaration carries the marker decides its code, so one whose answer went
766
814
  // unread is reported by nothing.
767
815
  const unread = run.projects.flatMap((one) => one.extra.deprecation.unanswered);
768
816
  return unread.length === 0
@@ -836,6 +884,7 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
836
884
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
837
885
  generated: new Set(projects.flatMap((one) => one.extra.generated)),
838
886
  inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
887
+ unseenLoads: projects.flatMap((one) => one.extra.unseenLoads),
839
888
  },
840
889
  boundary: {
841
890
  consumers: {
@@ -844,6 +893,7 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
844
893
  },
845
894
  encapsulated: readManifest(host, targetRoot).declaresExports,
846
895
  edges: readEdgeSides(host, targetRoot),
896
+ filledOutside: new Set(projects.flatMap((one) => one.filledOutside)),
847
897
  },
848
898
  selfCheck,
849
899
  intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
@@ -886,6 +936,7 @@ function readAnalysis(engine, host, scope, config, provenance, mode, detectors)
886
936
  librariesUnfound: read.librariesUnfound,
887
937
  conventionsApplied: read.conventionsApplied,
888
938
  unmappedEntries: unmappedEverywhere(read.projects),
939
+ unseenLoads: read.projects.flatMap((one) => one.extra.unseenLoads),
889
940
  },
890
941
  configured: read.projects.map((one) => one.configured),
891
942
  symbols,
@@ -6,9 +6,11 @@
6
6
  * name, which the language or a library types values by where the target never names it,
7
7
  * as `import.meta` or `process.env`, so a use of its member is a use of it.
8
8
  */
9
+ import { isInterfaceDeclaration, isModuleBlock, isModuleDeclaration, } from "@typescript/native/ast";
10
+ import { nodeKey } from "./inventory.js";
9
11
  import { referenceKey } from "./matrix.js";
10
12
  /** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
11
- function isGlobalBlock(symbol, held, files) {
13
+ export function isGlobalBlock(symbol, held, files) {
12
14
  return (symbol.kind === "namespace" &&
13
15
  files.has(symbol.parent) &&
14
16
  symbol.ref.endsWith("#global") &&
@@ -54,6 +56,66 @@ export function ambientGlobalInterfaces(held, interfaces, scripts) {
54
56
  ? []
55
57
  : interfaces.filter((id) => held.ambient.has(id) && holders(id).global);
56
58
  }
59
+ /**
60
+ * Each ambient global interface of the project's own files that merges into an interface
61
+ * declared outside them, with every interface, namespace and block between it and the
62
+ * global scope: the outside declaration names it wherever a value is typed by it. Where
63
+ * every outside declaration is a package's, not the compiler's library, and declares no
64
+ * member, each member it declares as well: such an interface is a slot the package reads
65
+ * by type, as `keyof` does, in declarations the analysis does not walk.
66
+ */
67
+ export function mergedGlobalInterfaces(project, held, files) {
68
+ const scripts = new Set();
69
+ const names = new Map();
70
+ for (const file of files.byName.values()) {
71
+ const fileId = held.declarations.get(nodeKey(file, file));
72
+ if (fileId !== undefined && file.externalModuleIndicator === undefined) {
73
+ scripts.add(fileId);
74
+ }
75
+ const visit = (node) => {
76
+ if (isInterfaceDeclaration(node)) {
77
+ const id = held.declarations.get(nodeKey(file, node));
78
+ if (id !== undefined) {
79
+ names.set(id, node.name);
80
+ }
81
+ }
82
+ else if (isModuleDeclaration(node) || isModuleBlock(node)) {
83
+ node.forEachChild(visit);
84
+ }
85
+ };
86
+ file.forEachChild(visit);
87
+ }
88
+ const global = new Set(ambientGlobalInterfaces(held, [...names.keys()], scripts));
89
+ const asked = [...names].filter(([id]) => global.has(id));
90
+ if (asked.length === 0) {
91
+ return [];
92
+ }
93
+ const answers = project.symbolsAt(asked.map(([, name]) => project.handle(name)));
94
+ const holders = holdersOf(held, scripts);
95
+ const empty = (handle) => {
96
+ if (project.program.getSourceFileMetadata(handle.path)?.isDefaultLibrary === true) {
97
+ return false;
98
+ }
99
+ const node = project.declarationAt(handle)?.node;
100
+ return node !== undefined && isInterfaceDeclaration(node) && node.members.length === 0;
101
+ };
102
+ return [
103
+ ...new Set(asked.flatMap(([id], at) => {
104
+ const symbol = answers[at];
105
+ if (typeof symbol !== "object") {
106
+ return [];
107
+ }
108
+ const outside = symbol.declarations.filter((handle) => !files.byName.has(handle.path));
109
+ if (outside.length === 0) {
110
+ return [];
111
+ }
112
+ const members = outside.every(empty)
113
+ ? held.symbols.filter((one) => one.parent === id).map((one) => one.id)
114
+ : [];
115
+ return [id, ...(holders?.(id).ids ?? []), ...members];
116
+ })),
117
+ ];
118
+ }
57
119
  /**
58
120
  * For each reference to a declaration an augmentation block, or the global scope of a
59
121
  * file in `scripts` (the files that are no module), holds, a read at the same place of