@cplieger/deadset-ts 7.0.0-dev.2 → 7.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 (137) hide show
  1. package/README.md +1 -1
  2. package/dist/src/alias-chain.js +2 -2
  3. package/dist/src/analysis.js +89 -44
  4. package/dist/src/augmentations.js +14 -12
  5. package/dist/src/calls.js +2 -2
  6. package/dist/src/class-members.js +1 -1
  7. package/dist/src/components.js +62 -20
  8. package/dist/src/configuration-files.js +6 -6
  9. package/dist/src/configured-declarations.js +2 -2
  10. package/dist/src/conformance-record.js +1 -1
  11. package/dist/src/contextual-uses.js +71 -14
  12. package/dist/src/convention-rows.js +6 -2
  13. package/dist/src/conventions.js +4 -2
  14. package/dist/src/dependencies.js +81 -27
  15. package/dist/src/deprecation.js +1 -1
  16. package/dist/src/destructuring.js +2 -2
  17. package/dist/src/discover.js +1 -1
  18. package/dist/src/element-access.js +341 -0
  19. package/dist/src/entry-points.js +2 -2
  20. package/dist/src/enum-group.js +2 -2
  21. package/dist/src/erased-stores.js +317 -0
  22. package/dist/src/findings/completion.js +46 -15
  23. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/self-check.js +17 -4
  26. package/dist/src/findings-pass.js +6 -5
  27. package/dist/src/framework-lifecycle.js +1 -1
  28. package/dist/src/html-entries.js +1 -1
  29. package/dist/src/implementations.js +2 -2
  30. package/dist/src/in-operands.js +30 -0
  31. package/dist/src/indexed-access.js +264 -125
  32. package/dist/src/injection-container.js +1 -1
  33. package/dist/src/interface-satisfaction.js +2 -2
  34. package/dist/src/intra-function-parts.js +26 -12
  35. package/dist/src/inventory.js +26 -1
  36. package/dist/src/keyof-comparisons.js +170 -0
  37. package/dist/src/kinds.js +17 -7
  38. package/dist/src/manifest.js +6 -16
  39. package/dist/src/matrix.js +20 -2
  40. package/dist/src/member-names.js +1 -1
  41. package/dist/src/module-declarations.js +1 -1
  42. package/dist/src/namespace-objects.js +1 -1
  43. package/dist/src/outside-tests.js +1 -1
  44. package/dist/src/overrides.js +2 -2
  45. package/dist/src/query.js +4 -1
  46. package/dist/src/references.js +13 -2
  47. package/dist/src/reflective-lookup.js +2 -2
  48. package/dist/src/report.js +1 -1
  49. package/dist/src/reporters/sarif.js +4 -0
  50. package/dist/src/roots.js +1 -1
  51. package/dist/src/serialization-contract.js +45 -16
  52. package/dist/src/session.js +15 -8
  53. package/dist/src/signatures.js +1 -1
  54. package/dist/src/stores.js +1 -1
  55. package/dist/src/suppress-file.js +58 -6
  56. package/dist/src/suppress-inline.js +14 -5
  57. package/dist/src/suppress.js +54 -24
  58. package/dist/src/symbol-keys.js +1 -1
  59. package/dist/src/type-errors.js +1 -1
  60. package/dist/src/type-query-alias.js +1 -1
  61. package/dist/src/unimplemented-callees.js +312 -0
  62. package/dist/src/value-flow.js +1 -1
  63. package/dist/src/value-reach.js +8 -8
  64. package/dist/src/verbs/analyze.js +11 -9
  65. package/dist/src/workspace-programs.js +2 -2
  66. package/jsr.json +1 -1
  67. package/package.json +3 -3
  68. package/src/alias-chain.ts +2 -2
  69. package/src/analysis.ts +135 -52
  70. package/src/augmentations.ts +14 -11
  71. package/src/calls.ts +2 -2
  72. package/src/class-members.ts +2 -2
  73. package/src/component-layer.ts +1 -1
  74. package/src/components.ts +103 -21
  75. package/src/configuration-files.ts +6 -5
  76. package/src/configured-declarations.ts +2 -6
  77. package/src/conformance-record.ts +1 -1
  78. package/src/contextual-uses.ts +82 -18
  79. package/src/convention-rows.ts +6 -2
  80. package/src/conventions.ts +11 -2
  81. package/src/decorator.ts +1 -1
  82. package/src/dependencies.ts +108 -31
  83. package/src/deprecation.ts +1 -1
  84. package/src/destructuring.ts +2 -2
  85. package/src/discover.ts +2 -2
  86. package/src/element-access.ts +446 -0
  87. package/src/entry-points.ts +2 -2
  88. package/src/enum-group.ts +2 -2
  89. package/src/erased-stores.ts +413 -0
  90. package/src/file-facts.ts +3 -1
  91. package/src/findings/completion.ts +54 -20
  92. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  93. package/src/findings/emitters.ts +1 -1
  94. package/src/findings/self-check.ts +18 -4
  95. package/src/findings-pass.ts +12 -5
  96. package/src/framework-lifecycle.ts +2 -2
  97. package/src/html-entries.ts +1 -1
  98. package/src/implementations.ts +2 -2
  99. package/src/in-operands.ts +45 -0
  100. package/src/indexed-access.ts +304 -146
  101. package/src/injection-container.ts +1 -1
  102. package/src/interface-satisfaction.ts +2 -2
  103. package/src/intra-function-parts.ts +42 -17
  104. package/src/inventory.ts +34 -2
  105. package/src/keyof-comparisons.ts +211 -0
  106. package/src/kinds.ts +20 -7
  107. package/src/manifest.ts +6 -21
  108. package/src/matrix.ts +34 -2
  109. package/src/member-names.ts +1 -1
  110. package/src/module-declarations.ts +1 -1
  111. package/src/namespace-objects.ts +1 -1
  112. package/src/outside-tests.ts +1 -1
  113. package/src/overrides.ts +2 -2
  114. package/src/position.ts +1 -1
  115. package/src/query.ts +12 -2
  116. package/src/references.ts +25 -3
  117. package/src/reflective-lookup.ts +2 -2
  118. package/src/report.ts +3 -1
  119. package/src/reporters/sarif.ts +4 -0
  120. package/src/roots.ts +2 -2
  121. package/src/serialization-contract.ts +55 -16
  122. package/src/session.ts +39 -32
  123. package/src/signatures.ts +1 -1
  124. package/src/source-files.ts +1 -1
  125. package/src/stores.ts +1 -1
  126. package/src/suppress-file.ts +68 -5
  127. package/src/suppress-inline.ts +15 -5
  128. package/src/suppress.ts +63 -28
  129. package/src/symbol-keys.ts +2 -2
  130. package/src/type-errors.ts +2 -2
  131. package/src/type-query-alias.ts +1 -1
  132. package/src/unimplemented-callees.ts +411 -0
  133. package/src/value-flow.ts +1 -1
  134. package/src/value-reach.ts +12 -8
  135. package/src/verbs/analyze.ts +15 -9
  136. package/src/workflow-steps.ts +1 -1
  137. package/src/workspace-programs.ts +4 -4
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.20261007.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.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.
8
8
 
9
9
  ## Why use it
10
10
 
@@ -3,8 +3,8 @@
3
3
  * declaration of the target it is, and along the chain of aliases it heads to each
4
4
  * declaration every link of that chain is.
5
5
  */
6
- import { isExportAssignment, isExportSpecifier, isIdentifier, isImportClause, isImportSpecifier, isNamespaceExport, isNamespaceImport, } from "@typescript/native/unstable/ast";
7
- import { SymbolFlags } from "@typescript/native/unstable/sync";
6
+ import { isExportAssignment, isExportSpecifier, isIdentifier, isImportClause, isImportSpecifier, isNamespaceExport, isNamespaceImport, } from "@typescript/native/ast";
7
+ import { SymbolFlags } from "@typescript/native/sync";
8
8
  import { declarationsByName, nodeKey } from "./inventory.js";
9
9
  import { UNANSWERED } from "./query.js";
10
10
  /**
@@ -1,11 +1,9 @@
1
1
  /**
2
- * One run's passes, composed: the projects discovery names, opened in one snapshot
3
- * of one client, each read through the stages in the order they depend on each
4
- * other, and the per-project answers merged into the answer for the run.
5
- *
6
- * The stages take a project's view, which only this composition makes: a view
7
- * carries a project's identity in its type so a handle cannot cross to another
8
- * project's checker, and that holds only for views the session itself hands out.
2
+ * One run's passes, composed: the projects discovery names, opened in one snapshot of one
3
+ * client, each read through the stages in dependency order, and the per-project answers
4
+ * merged into the run's. The stages take a project's view, which only this composition
5
+ * makes: a view carries a project's identity in its type so a handle cannot cross to
6
+ * another project's checker, which holds only for views the session hands out.
9
7
  */
10
8
  import { lineOf, readComponent } from "./component-files.js";
11
9
  import { consumerLoads, consumerReferences, moduleIdentity, } from "./consumers.js";
@@ -18,6 +16,7 @@ import { conventionFailuresFor, generatedFiles, readConventions, } from "./conve
18
16
  import { readEdgeSides } from "./edges.js";
19
17
  import { enumGroup } from "./enum-group.js";
20
18
  import { generatedFile, generatedFilesOf } from "./generated-file.js";
19
+ import { inOperandNames } from "./in-operands.js";
21
20
  import { sourceFilesOf } from "./source-files.js";
22
21
  import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhile, isUnansweredRecord, retainedIn, } from "./exempt.js";
23
22
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
@@ -38,12 +37,12 @@ import { SetupError, setupLine } from "./setup-failure.js";
38
37
  import { pathsOf, readErrors, skippedRoots, skippedUnits, } from "./type-errors.js";
39
38
  import { reflectiveLookup } from "./reflective-lookup.js";
40
39
  import { references, testFileRulesOf } from "./references.js";
41
- import { roots, unmatchedEverywhere } from "./roots.js";
40
+ import { roots, unmatchedEverywhere, } from "./roots.js";
42
41
  import { serializationContract } from "./serialization-contract.js";
43
42
  import { outsideTestReferences, testFilesAt } from "./outside-tests.js";
44
43
  import { joinedTestFiles, withJoinedTests } from "./test-joins.js";
45
44
  import { supportReferences, testSupportFiles } from "./test-support.js";
46
- import { diagnosticsOf, ownedAs, runSession } from "./session.js";
45
+ import { diagnosticsOf, ownedAs, runSession, } from "./session.js";
47
46
  import { accessorsOf, storesOf } from "./stores.js";
48
47
  import { isPartKind } from "./suppress.js";
49
48
  import { bindRows, readBaseline, readIgnoreFile } from "./suppress-file.js";
@@ -75,21 +74,47 @@ function compare(a, b) {
75
74
  }
76
75
  /**
77
76
  * The manifests the analysis reads, each once: the target's own, then every workspace
78
- * member's below the target root.
77
+ * member's and every nested package's below the target root.
79
78
  */
80
- function conventionManifests(host, targetRoot, memberDirs) {
79
+ function conventionManifests(host, targetRoot, memberDirs, nestedDirs) {
81
80
  const found = new Map();
82
81
  if (host.kindOf(joinPath(targetRoot, "package.json")) === "file") {
83
82
  found.set(targetRoot, { dir: targetRoot, path: "package.json" });
84
83
  }
85
- for (const dir of memberDirs) {
86
- const below = relativePath(targetRoot, dir);
87
- if (below !== undefined && !found.has(dir)) {
88
- found.set(dir, { dir, path: `${below}/package.json` });
84
+ for (const [dirs, nested] of [
85
+ [memberDirs, false],
86
+ [nestedDirs, true],
87
+ ]) {
88
+ for (const dir of dirs) {
89
+ const below = relativePath(targetRoot, dir);
90
+ if (below !== undefined && !found.has(dir)) {
91
+ found.set(dir, { dir, path: `${below}/package.json`, nested });
92
+ }
89
93
  }
90
94
  }
91
95
  return [...found.values()];
92
96
  }
97
+ /**
98
+ * The nested packages of a run: each directory below the target root, in no `node_modules`
99
+ * directory and none of `known`, that holds a `package.json` and lies at or above one of
100
+ * `files`, the program's source files and the compiler configuration files, in path order.
101
+ */
102
+ function nestedPackageDirs(host, targetRoot, files, known) {
103
+ const seen = new Set();
104
+ const found = [];
105
+ for (const file of files) {
106
+ for (let dir = dirnamePath(file); dir !== targetRoot && !seen.has(dir) && relativePath(targetRoot, dir) !== undefined; dir = dirnamePath(dir)) {
107
+ seen.add(dir);
108
+ const below = relativePath(targetRoot, dir) ?? "";
109
+ if (!below.split("/").includes("node_modules") &&
110
+ !known.has(dir) &&
111
+ host.kindOf(joinPath(dir, "package.json")) === "file") {
112
+ found.push(dir);
113
+ }
114
+ }
115
+ }
116
+ return found.sort(compare);
117
+ }
93
118
  /**
94
119
  * Reads every project the scope discovers, and each declared consumer's, in one snapshot,
95
120
  * each checked for errors first. `stage` is handed a target project and enumerates and roots
@@ -101,26 +126,41 @@ function conventionManifests(host, targetRoot, memberDirs) {
101
126
  function readProjects(engine, host, scope, config, stage) {
102
127
  const targetRoot = scope.target.path;
103
128
  const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
129
+ if (discovered.configFiles.length === 0 && discovered.notBuilt.length === 0) {
130
+ throw new DiscoveryError(`${targetRoot} holds no compiler configuration, so the run holds no program`, []);
131
+ }
104
132
  const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
105
133
  const loads = consumerLoads(engine, host, scope, discovered.configFiles);
106
134
  const consumerOf = new Map(loads.flatMap((load) => load.configFiles.map((configFile) => [configFile, load.consumer])));
107
135
  const absoluteRoot = resolvePath(host.workingDirectory(), targetRoot);
108
136
  const workspace = workspaceRun(engine, host, absoluteRoot, discovered.configFiles, readManifest(host, targetRoot));
109
- const conventions = readConventions(host, (fileName, text) => engine.parseSourceFile(fileName, text), conventionManifests(host, absoluteRoot, workspace?.memberDirs ?? []), config.ts.disabledConventions);
110
- const options = {
111
- host,
112
- manifest: workspace?.manifest ?? readManifest(host, targetRoot),
113
- otherPackages: [
114
- ...(workspace?.entries ?? []),
115
- ...packageEntries(host, normalizePath(absoluteRoot), discovered.configFiles, new Set(workspace?.memberDirs ?? [])),
116
- ],
117
- patterns: config.rootPatterns,
118
- entryFiles: config.ts.entryFiles,
119
- conventions,
120
- testFiles: config.ts.testFiles,
121
- parse: (fileName, text) => engine.parseSourceFile(fileName, text),
122
- publishedAPI: config.targetKind === "library",
123
- publishedMembers: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
137
+ const parse = (fileName, text) => engine.parseSourceFile(fileName, text);
138
+ const memberDirs = workspace?.memberDirs ?? [];
139
+ // The nested packages are known only once every program is open, and every visit reads
140
+ // the conventions and options they decide, so both are set in the session's first step.
141
+ let nestedDirs = [];
142
+ let conventions = readConventions(host, parse, [], config.ts.disabledConventions);
143
+ let options;
144
+ const readManifests = (opened) => {
145
+ nestedDirs = nestedPackageDirs(host, normalizePath(absoluteRoot), opened
146
+ .filter((one) => !consumerOf.has(one.configFile))
147
+ .flatMap((one) => [one.configFile, ...one.fileNames()]), new Set(memberDirs));
148
+ conventions = readConventions(host, parse, conventionManifests(host, absoluteRoot, memberDirs, nestedDirs), config.ts.disabledConventions);
149
+ options = {
150
+ host,
151
+ manifest: workspace?.manifest ?? readManifest(host, targetRoot),
152
+ otherPackages: [
153
+ ...(workspace?.entries ?? []),
154
+ ...packageEntries(host, normalizePath(absoluteRoot), nestedDirs),
155
+ ],
156
+ patterns: config.rootPatterns,
157
+ entryFiles: config.ts.entryFiles,
158
+ conventions,
159
+ testFiles: config.ts.testFiles,
160
+ parse,
161
+ publishedAPI: config.targetKind === "library",
162
+ publishedMembers: new Set((workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir)),
163
+ };
124
164
  };
125
165
  const notBuilt = [...discovered.notBuilt];
126
166
  const dropped = (configFile, error) => {
@@ -253,8 +293,8 @@ function readProjects(engine, host, scope, config, stage) {
253
293
  }, discovered.derived ? "record" : "refuse", joins.size === 0
254
294
  ? workspace?.programs
255
295
  : withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins), () => {
256
- outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path))), options.parse);
257
- });
296
+ outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path))), parse);
297
+ }, readManifests);
258
298
  for (const configFile of session.unopened) {
259
299
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
260
300
  throw new DiscoveryError(`${configFile} opened no project`, []);
@@ -286,7 +326,8 @@ function readProjects(engine, host, scope, config, stage) {
286
326
  componentsUnread: workspace?.componentsUnread ?? [],
287
327
  conventionsApplied: conventions.applied,
288
328
  conventionUses: conventions.uses,
289
- memberDirs: workspace?.memberDirs ?? [],
329
+ memberDirs,
330
+ nestedDirs,
290
331
  outsideTests,
291
332
  };
292
333
  }
@@ -401,7 +442,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
401
442
  line: lineOf(file.originalText, warning.offset),
402
443
  reason: warning.reason,
403
444
  }))),
404
- extra: extra(project, projectRead, resolved.references),
445
+ extra: extra(project, projectRead, resolved.references, resolved.testFilePaths),
405
446
  };
406
447
  });
407
448
  const outsidePaths = read.outsideTests.map((one) => one.path);
@@ -438,6 +479,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
438
479
  ...(projectRead.referenceOnly.length === 0
439
480
  ? {}
440
481
  : { referenceOnly: projectRead.referenceOnly }),
482
+ ...(projectRead.held.dependents.size === 0
483
+ ? {}
484
+ : { dependents: projectRead.held.dependents }),
441
485
  };
442
486
  return {
443
487
  configFile: projectRead.configFile,
@@ -485,6 +529,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
485
529
  conventionsApplied: read.conventionsApplied,
486
530
  conventionUses: read.conventionUses,
487
531
  memberDirs: read.memberDirs,
532
+ nestedDirs: read.nestedDirs,
488
533
  };
489
534
  }
490
535
  /**
@@ -611,14 +656,12 @@ function sweptOf(read, input) {
611
656
  };
612
657
  }
613
658
  /**
614
- * Sweeps the run the scope and the configuration describe. Each project's references
615
- * and exemptions are read against its own inventory while its view is open; the
616
- * projects then merge into one matrix in the run's order, which is swept under the
617
- * caller's marks and mode with the exemptions every project found. A record found in
618
- * one configuration holds in every configuration that holds its declaration, because
619
- * an exemption is evidence of a use the analysis cannot see and a use in one
620
- * configuration is a use. A project that carries an error ends the run before any
621
- * project is swept.
659
+ * Sweeps the run the scope and the configuration describe. Each project's references and
660
+ * exemptions are read against its own inventory while its view is open; the projects then
661
+ * merge into one matrix in run order, swept under the caller's marks and mode with every
662
+ * project's exemptions. A record found in one configuration holds in every configuration
663
+ * that holds its declaration, because an exemption evidences a use the analysis cannot see.
664
+ * A project that carries an error ends the run before any project is swept.
622
665
  */
623
666
  export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
624
667
  return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
@@ -626,13 +669,14 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
626
669
  /** Reads a run's projects with every per-project fact an emitter reads. */
627
670
  export function readEmitterRun(engine, host, scope, config, mode, detectors) {
628
671
  const targetRoot = scope.target.path;
629
- const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
672
+ const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references, testFiles) => ({
630
673
  deprecation: deprecatedDeclarations(project, read.held),
631
674
  accessors: accessorsOf(project, read.held),
632
675
  needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) => engine.parseSourceFile(fileName, text)),
633
676
  implementations: implementations(project, read.held),
634
677
  heldByInclusion: heldByInclusion(project, targetRoot),
635
678
  generated: [...read.generated.keys()],
679
+ inOperands: inOperandNames(project, targetRoot, testFiles),
636
680
  rooted: read.rooted,
637
681
  directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
638
682
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
@@ -696,12 +740,13 @@ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
696
740
  references: one.configured.references,
697
741
  accessors: one.extra.accessors,
698
742
  })), swept.exempt, input.mode),
699
- dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), members.read, members.unbuilt),
743
+ dependencies: dependenciesOf(host, targetRoot, [...projects.map((one) => one.extra.needs), configurationNeeds(read.conventionUses)], swept.sweep.candidates.map((candidate) => candidate.id), members.read, members.unbuilt, read.nestedDirs),
700
744
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
701
745
  files: {
702
746
  tree: sourceTree(host, targetRoot),
703
747
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
704
748
  generated: new Set(projects.flatMap((one) => one.extra.generated)),
749
+ inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
705
750
  },
706
751
  boundary: {
707
752
  consumers: {
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The references a use of an augmentation's declaration makes to what holds it. No name
3
- * spells a `declare global` block, so a use of a declaration it holds is a use of the
4
- * block. An interface or namespace an augmentation block holds merges with the one of its
5
- * name, which the language or a library may type a value by where the target never names
6
- * it, as it types `import.meta` or `process.env`, so a use of its member is a use of it.
2
+ * The references a use of an augmentation's declaration makes to what holds it. A use of
3
+ * a declaration a `declare global` block or a module declaration holds is a use of the
4
+ * block, which no name spells or no import may name. An interface or namespace such a
5
+ * block, or the top level of a file that is no module, holds merges with the one of its
6
+ * name, which the language or a library types values by where the target never names it,
7
+ * as `import.meta` or `process.env`, so a use of its member is a use of it.
7
8
  */
8
9
  /** Whether one declaration is a `declare global` block: a module-level namespace named `global`. */
9
10
  function isGlobalBlock(symbol, held, files) {
@@ -13,24 +14,25 @@ function isGlobalBlock(symbol, held, files) {
13
14
  !held.modules.has(symbol.id));
14
15
  }
15
16
  /**
16
- * For each reference to a declaration an augmentation block holds, a read at the same place
17
- * of every interface and namespace between it and the block, and of a `declare global` block
18
- * itself.
17
+ * For each reference to a declaration an augmentation block, or the global scope of a
18
+ * file in `scripts`, holds, a read at the same place of every interface and namespace
19
+ * between it and the block or the file, and of the block itself. `scripts` holds the
20
+ * files that are no module, by identifier.
19
21
  */
20
- export function augmentationReferences(held, found) {
22
+ export function augmentationReferences(held, found, scripts = new Set()) {
21
23
  const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
22
24
  const globals = new Set(held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id));
23
- if (globals.size === 0 && held.modules.size === 0) {
25
+ if (globals.size === 0 && held.modules.size === 0 && scripts.size === 0) {
24
26
  return [];
25
27
  }
26
28
  const byId = new Map(held.symbols.map((one) => [one.id, one]));
27
29
  const holders = (id) => {
28
30
  const ids = [];
29
31
  for (let at = byId.get(byId.get(id)?.parent ?? ""); at !== undefined; at = byId.get(at.parent)) {
30
- if (globals.has(at.id)) {
32
+ if (globals.has(at.id) || held.modules.has(at.id)) {
31
33
  return [...ids, at.id];
32
34
  }
33
- if (held.modules.has(at.id)) {
35
+ if (scripts.has(at.id)) {
34
36
  return ids;
35
37
  }
36
38
  if (at.kind === "interface" || at.kind === "namespace") {
package/dist/src/calls.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * literal or a property value of an object literal written as one, at any depth, and a
6
6
  * decorator written as a call is a call.
7
7
  */
8
- import { isArrayLiteralExpression, isCallExpression, isClassDeclaration, isDecorator, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, } from "@typescript/native/unstable/ast";
9
- import { SymbolFlags } from "@typescript/native/unstable/sync";
8
+ import { isArrayLiteralExpression, isCallExpression, isClassDeclaration, isDecorator, isIdentifier, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadElement, } from "@typescript/native/ast";
9
+ import { SymbolFlags } from "@typescript/native/sync";
10
10
  import { nodeKey } from "./inventory.js";
11
11
  import { must } from "./query.js";
12
12
  /** The name node a callee or a decorator expression resolves through, where it has one. */
@@ -3,7 +3,7 @@
3
3
  * for the class's type, so a member a base class declares is a member of every class
4
4
  * that extends it and a member a subclass overrides is the subclass's own.
5
5
  */
6
- import { isClassDeclaration, isClassExpression } from "@typescript/native/unstable/ast";
6
+ import { isClassDeclaration, isClassExpression } from "@typescript/native/ast";
7
7
  import { nodeKey } from "./inventory.js";
8
8
  import { must } from "./query.js";
9
9
  /** The name node of each class declaration of the target, by the class's identifier. */
@@ -26,34 +26,74 @@ export function listing(component, cascade) {
26
26
  /**
27
27
  * Groups the declarations `dead` marks into components, ordered by their first member's
28
28
  * site; `dead`, `testOfDeadCode` and `unreferencedTest` hold one flag per declaration of
29
- * the graph. Root members sit on the cycles no other cycle references. A cycle two roots
30
- * both reach is dead only through both, so a component is a weakly connected part of the
31
- * dead subgraph: no dead declaration of one references a declaration of another.
29
+ * the graph. Root members sit on the cycles no other cycle references. A component is a
30
+ * weakly connected part of the dead subgraph, because a cycle two roots reach is dead only
31
+ * through both. The held declarations `positional` decides form components of their own,
32
+ * and a dead declaration that is not held shares one with each of its dead dependents.
32
33
  */
33
- export function componentsOf(graph, dead, testOfDeadCode, unreferencedTest = []) {
34
- const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest);
34
+ export function componentsOf(graph, dead, testOfDeadCode, unreferencedTest = [], positional = { dependents: [], live: [] }) {
35
+ const { at, position, adjacent, references } = deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest);
35
36
  if (at.length === 0) {
36
37
  return [];
37
38
  }
38
- const { componentOf, members } = connected(adjacent);
39
- const referenced = referencedCycles(adjacent, componentOf, members.length);
40
- const groups = joined(adjacent);
41
- const idOf = (position) => graph.symbols[at[position] ?? 0]?.id ?? "";
39
+ const dependents = at.map((index) => (positional.dependents[index] ?? []).map((to) => ({
40
+ to: position[to] ?? UNVISITED,
41
+ live: positional.live[to] === true,
42
+ })));
43
+ const held = heldSet(dependents, references);
44
+ const kept = adjacent.map((targets, from) => targets.filter((to) => held[to] === held[from]));
45
+ const { componentOf, members } = connected(kept);
46
+ const referenced = referencedCycles(kept, componentOf, members.length);
47
+ const groups = joined(kept.map((targets, from) => [
48
+ ...targets,
49
+ ...(held[from] === true
50
+ ? []
51
+ : (dependents[from] ?? []).flatMap(({ to }) => to === UNVISITED || held[to] === true ? [] : [to])),
52
+ ]));
53
+ const idOf = (place) => graph.symbols[at[place] ?? 0]?.id ?? "";
42
54
  return groups.map((group, place) => {
43
- const roots = group.filter((position) => {
55
+ const roots = group.filter((one) => {
44
56
  // A dead member's dead container is on the member's own cycle, and the container
45
57
  // is where the deletion starts.
46
- const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
47
- return referenced[componentOf[position] ?? 0] !== true && dead[container] !== true;
58
+ const container = graph.parent[at[one] ?? 0] ?? OUTSIDE;
59
+ return referenced[componentOf[one] ?? 0] !== true && dead[container] !== true;
48
60
  });
61
+ const isHeld = group.every((one) => held[one] === true);
49
62
  return {
50
63
  id: componentId(place + 1),
51
64
  members: group.map(idOf),
52
65
  roots: roots.map(idOf),
53
- deletableLines: linesSpanned(group.map((position) => graph.symbols[at[position] ?? 0])),
66
+ deletableLines: isHeld ? 0 : linesSpanned(group.map((one) => graph.symbols[at[one] ?? 0])),
67
+ held: isHeld,
54
68
  };
55
69
  });
56
70
  }
71
+ /**
72
+ * Per place of the dead subgraph, whether the declaration is held: one with a live
73
+ * dependent, and each dead declaration a held one references or has as a dependent,
74
+ * until no further one joins.
75
+ */
76
+ function heldSet(dependents, references) {
77
+ const held = dependents.map((after) => after.some((one) => one.live));
78
+ const before = dependents.map(() => []);
79
+ dependents.forEach((after, from) => {
80
+ for (const { to } of after) {
81
+ if (to !== UNVISITED) {
82
+ before[to]?.push(from);
83
+ }
84
+ }
85
+ });
86
+ const pending = held.flatMap((one, place) => (one ? [place] : []));
87
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
88
+ for (const reached of [...(references[next] ?? []), ...(before[next] ?? [])]) {
89
+ if (held[reached] !== true) {
90
+ held[reached] = true;
91
+ pending.push(reached);
92
+ }
93
+ }
94
+ }
95
+ return held;
96
+ }
57
97
  /**
58
98
  * The distinct source lines a set of declarations spans, each declaration at least the
59
99
  * line it starts on. A member written inside its container's span adds no line, because
@@ -89,12 +129,12 @@ function linesSpanned(symbols) {
89
129
  return total;
90
130
  }
91
131
  /**
92
- * The dead declarations by their place in the subgraph, a test file's declaration only
93
- * where it is a test of dead code or nothing the tests reach, and the edges between them:
94
- * each reference one makes to another, except an unreferenced test-file declaration's
95
- * reference to production code, which decides nothing of its target, an edge each way
96
- * between a dead member and its dead container, and an edge back from each production
97
- * target of an admitted test, which puts the test in its subject's component.
132
+ * The dead declarations by subgraph place (`at`, and `position` per graph declaration),
133
+ * a test file's only where it tests dead code or nothing the tests reach. `adjacent` holds
134
+ * each reference one makes to another, except an unreferenced test-file declaration's to
135
+ * production code, which decides nothing of its target; an edge each way between a dead
136
+ * member and its dead container; and one back from each production target of an admitted
137
+ * test, which puts the test in its subject's component. `references` holds the first alone.
98
138
  */
99
139
  function deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest) {
100
140
  const at = [];
@@ -107,6 +147,7 @@ function deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest) {
107
147
  return at.length - 1;
108
148
  });
109
149
  const adjacent = at.map(() => []);
150
+ const references = at.map(() => []);
110
151
  at.forEach((index, from) => {
111
152
  for (const edge of graph.out[index] ?? []) {
112
153
  const to = position[edge.to] ?? UNVISITED;
@@ -114,6 +155,7 @@ function deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest) {
114
155
  continue;
115
156
  }
116
157
  adjacent[from]?.push(to);
158
+ references[from]?.push(to);
117
159
  if (testOfDeadCode[index] === true &&
118
160
  graph.test[edge.to] !== true &&
119
161
  graph.subject[edge.to] === true) {
@@ -126,7 +168,7 @@ function deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest) {
126
168
  adjacent[container]?.push(from);
127
169
  }
128
170
  });
129
- return { at, adjacent };
171
+ return { at, position, adjacent, references };
130
172
  }
131
173
  /**
132
174
  * Each declaration's cycle and each cycle's members by site, by Tarjan's
@@ -6,7 +6,7 @@
6
6
  * every string either holds as it likes, so each one that spells a declared dependency
7
7
  * uses it and each relative one that names a source file of the program roots the file.
8
8
  */
9
- import { isExportDeclaration, isImportDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/unstable/ast";
9
+ import { isExportDeclaration, isImportDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, } from "@typescript/native/ast";
10
10
  import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.js";
11
11
  const MANIFEST = "package.json";
12
12
  /** A configuration module's name, each form with the spelling a root reports it by. */
@@ -341,7 +341,7 @@ export function configurationFiles(host, targetRoot, files, parse) {
341
341
  documents: [...documents.values()],
342
342
  outside: parse === undefined
343
343
  ? []
344
- : outsideModules(host, [...dirs], files, holdsManifest, parse, named),
344
+ : outsideModules(host, root, [...dirs], files, holdsManifest, parse, named),
345
345
  };
346
346
  }
347
347
  /** The relative specifiers one file's imports and re-exports name. */
@@ -386,10 +386,10 @@ export function resolveRelativeSpecifier(host, dir, specifier) {
386
386
  }
387
387
  /**
388
388
  * The configuration modules of the directories that hold a manifest which the program
389
- * does not hold, and the files they import by a relative specifier, until no further
390
- * file joins. A file that cannot be read is skipped.
389
+ * does not hold, and the project files below `root` they import by a relative specifier,
390
+ * until no further file joins. A file that cannot be read is skipped.
391
391
  */
392
- function outsideModules(host, dirs, files, holdsManifest, parse, named) {
392
+ function outsideModules(host, root, dirs, files, holdsManifest, parse, named) {
393
393
  const pending = [...named];
394
394
  for (const dir of [...dirs].sort()) {
395
395
  if (!holdsManifest(dir)) {
@@ -432,7 +432,7 @@ function outsideModules(host, dirs, files, holdsManifest, parse, named) {
432
432
  outside.manifestDirs.push(next.manifestDir);
433
433
  for (const specifier of relativeSpecifiers(outside.file)) {
434
434
  const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
435
- if (reached !== undefined) {
435
+ if (reached !== undefined && isProjectPath(root, reached)) {
436
436
  pending.push({ path: reached, manifestDir: next.manifestDir });
437
437
  }
438
438
  }
@@ -4,8 +4,8 @@
4
4
  * member read through an instantiation of a generic type is a symbol of its own that
5
5
  * keeps the declarations of the member it instantiates.
6
6
  */
7
- import { isCallExpression, isExportDeclaration, isExternalModuleReference, isImportDeclaration, isImportEqualsDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/unstable/ast";
8
- import { SymbolFlags, } from "@typescript/native/unstable/sync";
7
+ import { isCallExpression, isExportDeclaration, isExternalModuleReference, isImportDeclaration, isImportEqualsDeclaration, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
8
+ import { SymbolFlags } from "@typescript/native/sync";
9
9
  import { resolvedTargets } from "./calls.js";
10
10
  import { must, Unanswerable } from "./query.js";
11
11
  import { nameComponent } from "./ref.js";
@@ -4,7 +4,7 @@ export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "5.0.0",
6
6
  result: "pass",
7
- digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
7
+ digest: "sha256:c46c7d06b6f0c58504c5f311870efefc0b85f18c2c46ba7d7b722081cb370c0c",
8
8
  },
9
9
  gaps: [],
10
10
  };