@cplieger/deadset-ts 7.0.0-dev.2 → 7.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 (75) hide show
  1. package/dist/src/analysis.js +89 -44
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/components.js +62 -20
  4. package/dist/src/configuration-files.js +5 -5
  5. package/dist/src/conformance-record.js +1 -1
  6. package/dist/src/contextual-uses.js +71 -14
  7. package/dist/src/convention-rows.js +6 -2
  8. package/dist/src/conventions.js +3 -1
  9. package/dist/src/dependencies.js +80 -26
  10. package/dist/src/element-access.js +341 -0
  11. package/dist/src/erased-stores.js +317 -0
  12. package/dist/src/findings/completion.js +46 -15
  13. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  14. package/dist/src/findings/emitters.js +1 -1
  15. package/dist/src/findings/self-check.js +17 -4
  16. package/dist/src/findings-pass.js +6 -5
  17. package/dist/src/in-operands.js +30 -0
  18. package/dist/src/indexed-access.js +264 -125
  19. package/dist/src/intra-function-parts.js +25 -11
  20. package/dist/src/inventory.js +25 -0
  21. package/dist/src/keyof-comparisons.js +170 -0
  22. package/dist/src/kinds.js +17 -7
  23. package/dist/src/manifest.js +6 -16
  24. package/dist/src/matrix.js +20 -2
  25. package/dist/src/query.js +3 -0
  26. package/dist/src/references.js +12 -1
  27. package/dist/src/report.js +1 -1
  28. package/dist/src/reporters/sarif.js +4 -0
  29. package/dist/src/serialization-contract.js +43 -14
  30. package/dist/src/session.js +12 -7
  31. package/dist/src/suppress-file.js +58 -6
  32. package/dist/src/suppress-inline.js +13 -4
  33. package/dist/src/suppress.js +54 -24
  34. package/dist/src/unimplemented-callees.js +312 -0
  35. package/dist/src/value-reach.js +7 -7
  36. package/dist/src/verbs/analyze.js +11 -9
  37. package/jsr.json +1 -1
  38. package/package.json +2 -2
  39. package/src/analysis.ts +135 -52
  40. package/src/augmentations.ts +14 -11
  41. package/src/components.ts +103 -21
  42. package/src/configuration-files.ts +5 -4
  43. package/src/conformance-record.ts +1 -1
  44. package/src/contextual-uses.ts +81 -17
  45. package/src/convention-rows.ts +6 -2
  46. package/src/conventions.ts +10 -1
  47. package/src/dependencies.ts +107 -30
  48. package/src/element-access.ts +446 -0
  49. package/src/erased-stores.ts +413 -0
  50. package/src/file-facts.ts +2 -0
  51. package/src/findings/completion.ts +54 -20
  52. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  53. package/src/findings/emitters.ts +1 -1
  54. package/src/findings/self-check.ts +18 -4
  55. package/src/findings-pass.ts +12 -5
  56. package/src/in-operands.ts +45 -0
  57. package/src/indexed-access.ts +303 -145
  58. package/src/intra-function-parts.ts +41 -16
  59. package/src/inventory.ts +32 -0
  60. package/src/keyof-comparisons.ts +211 -0
  61. package/src/kinds.ts +20 -7
  62. package/src/manifest.ts +6 -21
  63. package/src/matrix.ts +34 -2
  64. package/src/query.ts +10 -0
  65. package/src/references.ts +23 -1
  66. package/src/report.ts +3 -1
  67. package/src/reporters/sarif.ts +4 -0
  68. package/src/serialization-contract.ts +53 -14
  69. package/src/session.ts +34 -29
  70. package/src/suppress-file.ts +68 -5
  71. package/src/suppress-inline.ts +14 -4
  72. package/src/suppress.ts +63 -28
  73. package/src/unimplemented-callees.ts +411 -0
  74. package/src/value-reach.ts +11 -7
  75. package/src/verbs/analyze.ts +15 -9
@@ -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") {
@@ -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
@@ -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,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
  };
@@ -6,8 +6,8 @@
6
6
  * the parameters and results of functions and constructors too; a class instance there is
7
7
  * the interface-satisfaction class's evidence instead.
8
8
  */
9
- import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
10
- import { SignatureKind, SymbolFlags } from "@typescript/native/unstable/sync";
9
+ import { isClassDeclaration, isClassExpression, isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isMethodSignatureDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
10
+ import { SignatureKind } from "@typescript/native/unstable/sync";
11
11
  import { isAnswered } from "./query.js";
12
12
  /** The member kinds a contextual type writes and reads: data members and the members of object types. */
13
13
  const MEMBER_KINDS = new Set(["type-member", "class-member"]);
@@ -36,6 +36,20 @@ function propertyName(property) {
36
36
  }
37
37
  return undefined;
38
38
  }
39
+ /**
40
+ * Whether an object spread copies a member a declaration declares: a method, a method
41
+ * signature and a class's accessor are no own data property of the value, while an
42
+ * accessor of an interface or a type literal describes one. It is decided on the
43
+ * declaration, because a mapped type over a class keeps the class's declarations on
44
+ * plain property symbols.
45
+ */
46
+ function copiedBySpread(node) {
47
+ if (isMethodDeclaration(node) || isMethodSignatureDeclaration(node)) {
48
+ return false;
49
+ }
50
+ return !((isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) &&
51
+ (isClassDeclaration(node.parent) || isClassExpression(node.parent)));
52
+ }
39
53
  /**
40
54
  * The contextual uses of one project's literals and values, asked in batches: one
41
55
  * contextual type per literal and per value whose type carries a member of the target,
@@ -85,9 +99,12 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
85
99
  ids: chains
86
100
  .declarationsOf(property)
87
101
  .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
102
+ methods: chains
103
+ .declarationsOf(property)
104
+ .filter((id) => kinds.get(id) === "interface-method"),
88
105
  handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
89
106
  own: property.declarations.some((handle) => ownPaths.has(handle.path)),
90
- accessor: (property.flags & SymbolFlags.Accessor) !== 0,
107
+ declarations: property.declarations.filter((handle) => ownPaths.has(handle.path)),
91
108
  });
92
109
  }
93
110
  tables.set(type.id, table);
@@ -137,7 +154,10 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
137
154
  batches += batchesOf(carried.length);
138
155
  const types = queries.typesAt(carried.map((flow) => flow.value), cap);
139
156
  /** The members a position may read structurally: those of object types, not of classes. */
140
- const readable = (member) => member.ids.filter((id) => kinds.get(id) === "type-member");
157
+ const readable = (member) => [
158
+ ...member.ids.filter((id) => kinds.get(id) === "type-member"),
159
+ ...member.methods,
160
+ ];
141
161
  const carries = (part) => [...membersOf(part).values()].some((member) => readable(member).length > 0);
142
162
  /** Whether an object type has members and declares none of them in the target. */
143
163
  const outside = (part) => {
@@ -161,6 +181,19 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
161
181
  elements.set(type.id, element);
162
182
  return element;
163
183
  };
184
+ const argumentsOf = new Map();
185
+ /** The type arguments of an instantiation of a generic type, read once per type. */
186
+ const typeArgumentsOf = (type) => {
187
+ const known = argumentsOf.get(type.id);
188
+ if (known !== undefined) {
189
+ return known;
190
+ }
191
+ lookups += type.isTypeReference() ? 1 : 0;
192
+ const held = queries.typeArguments(type);
193
+ const found = isAnswered(held) ? held : [];
194
+ argumentsOf.set(type.id, found);
195
+ return found;
196
+ };
164
197
  const calls = new Map();
165
198
  /**
166
199
  * The parameter and return types of a type's one call signature, or of its one construct
@@ -195,10 +228,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
195
228
  };
196
229
  /**
197
230
  * Each object type a value of `source` is checked as against the object types of
198
- * `target`: the value's own, and through array elements and through a function's
199
- * parameters, checked the other way, and its result. A source that is one of the
200
- * target's own object types matches it as it stands. Each pair is compared once, which
201
- * ends the walk over a recursive type.
231
+ * `target`: the value's own, and through array elements, a function's parameters,
232
+ * checked the other way, and result, and the type arguments two instantiations share
233
+ * by place, an array among them, as an array reaches `Iterable<T>`.
234
+ * A source that is one of the target's own object types matches it as it stands. Each
235
+ * pair is compared once, which ends the walk over a recursive type.
202
236
  */
203
237
  const compared = (source, target, into, seen) => {
204
238
  const key = `${String(source?.id)}>${String(target?.id)}`;
@@ -220,9 +254,20 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
220
254
  }
221
255
  const element = elementOf(one);
222
256
  const called = element === undefined ? callOf(one) : undefined;
257
+ const sourceArguments = typeArgumentsOf(one);
223
258
  for (const part of targets) {
259
+ const partElement = elementOf(part);
260
+ if (element !== undefined && partElement !== undefined) {
261
+ compared(element, partElement, into, seen);
262
+ continue;
263
+ }
264
+ const targetArguments = typeArgumentsOf(part);
265
+ // A defaulted trailing parameter, as `Iterable<T, TReturn, TNext>` has, leaves
266
+ // the places two instantiations share comparable.
267
+ sourceArguments.slice(0, targetArguments.length).forEach((argument, index) => {
268
+ compared(argument, targetArguments[index], into, seen);
269
+ });
224
270
  if (element !== undefined) {
225
- compared(element, elementOf(part), into, seen);
226
271
  continue;
227
272
  }
228
273
  const other = called === undefined ? undefined : callOf(part);
@@ -253,7 +298,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
253
298
  if (element !== undefined) {
254
299
  return mayCarry(element, seen);
255
300
  }
256
- return callOf(part) !== undefined;
301
+ return callOf(part) !== undefined || typeArgumentsOf(part).some((one) => mayCarry(one, seen));
257
302
  });
258
303
  };
259
304
  const sourced = carried.flatMap((flow, index) => {
@@ -302,6 +347,19 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
302
347
  reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
303
348
  }
304
349
  });
350
+ const copies = new Map();
351
+ /** Whether a spread copies the member: where any declaration of it declares data. */
352
+ const copied = (member) => {
353
+ let known = copies.get(member);
354
+ if (known === undefined) {
355
+ known = member.declarations.some((handle) => {
356
+ const node = project.declarationAt(handle)?.node;
357
+ return node === undefined || copiedBySpread(node);
358
+ });
359
+ copies.set(member, known);
360
+ }
361
+ return known;
362
+ };
305
363
  const spreads = literals.flatMap(({ literal, from }) => literal.properties.flatMap((property) => isSpreadAssignment(property) ? [{ value: property.expression, from }] : []));
306
364
  batches += batchesOf(spreads.length);
307
365
  const spreadTypes = queries.typesAt(spreads.map((one) => one.value), cap);
@@ -310,11 +368,10 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
310
368
  if (type === undefined || !isAnswered(type)) {
311
369
  return;
312
370
  }
313
- // A spread gets every own enumerable property, an overwritten key's getter included.
314
- // So it reads each data member but a class's accessor, which sits on the prototype with
315
- // the class's methods (never data members), and a private name, which is no property
371
+ // A spread gets every own enumerable property, an overwritten key's getter included,
372
+ // and no private name, which is no property
316
373
  // (https://tc39.es/ecma262/#sec-copydataproperties).
317
- const own = (member, id) => !privateNames.has(id) && !(member.accessor && kinds.get(id) === "class-member");
374
+ const own = (member, id) => !privateNames.has(id) && copied(member);
318
375
  const ids = [
319
376
  ...new Set(partsOf(type).flatMap((part) => [...membersOf(part).values()].flatMap((member) => member.ids.filter((id) => own(member, id))))),
320
377
  ];