@cplieger/deadset-ts 7.0.0-dev.1 → 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 (113) hide show
  1. package/dist/src/analysis.js +95 -60
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/component-layer.js +1 -1
  4. package/dist/src/components.js +62 -20
  5. package/dist/src/configuration-files.js +230 -30
  6. package/dist/src/conformance-record.js +1 -1
  7. package/dist/src/contextual-uses.js +104 -13
  8. package/dist/src/convention-rows.js +10 -0
  9. package/dist/src/conventions.js +35 -1
  10. package/dist/src/dependencies.js +90 -32
  11. package/dist/src/discover.js +1 -1
  12. package/dist/src/edges.js +1 -1
  13. package/dist/src/element-access.js +341 -0
  14. package/dist/src/erased-stores.js +317 -0
  15. package/dist/src/exempt-classes.js +9 -22
  16. package/dist/src/exempt.js +3 -15
  17. package/dist/src/findings/completion.js +78 -16
  18. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  19. package/dist/src/findings/emitters.js +1 -5
  20. package/dist/src/findings/self-check.js +27 -10
  21. package/dist/src/findings-pass.js +6 -5
  22. package/dist/src/in-operands.js +30 -0
  23. package/dist/src/indexed-access.js +264 -119
  24. package/dist/src/intra-function-parts.js +25 -11
  25. package/dist/src/inventory.js +28 -12
  26. package/dist/src/keyof-comparisons.js +170 -0
  27. package/dist/src/kinds.js +17 -7
  28. package/dist/src/manifest.js +6 -16
  29. package/dist/src/matrix.js +20 -2
  30. package/dist/src/query.js +3 -0
  31. package/dist/src/references.js +14 -3
  32. package/dist/src/report.js +4 -1
  33. package/dist/src/reporters/sarif.js +4 -0
  34. package/dist/src/roots.js +0 -1
  35. package/dist/src/run.js +0 -2
  36. package/dist/src/schema.js +1 -1
  37. package/dist/src/scope.js +2 -2
  38. package/dist/src/serialization-contract.js +43 -14
  39. package/dist/src/session.js +12 -8
  40. package/dist/src/suppress-file.js +58 -6
  41. package/dist/src/suppress-inline.js +14 -5
  42. package/dist/src/suppress.js +64 -35
  43. package/dist/src/unimplemented-callees.js +312 -0
  44. package/dist/src/value-reach.js +7 -7
  45. package/dist/src/verbs/analyze.js +11 -9
  46. package/dist/src/workspace-programs.js +0 -3
  47. package/dist/src/workspace-resolution.js +0 -2
  48. package/dist/src/workspace.js +0 -3
  49. package/jsr.json +1 -1
  50. package/package.json +3 -2
  51. package/src/analysis.ts +145 -82
  52. package/src/augmentations.ts +14 -11
  53. package/src/calls.ts +3 -3
  54. package/src/component-files.ts +7 -7
  55. package/src/component-layer.ts +2 -2
  56. package/src/components.ts +104 -22
  57. package/src/config.ts +4 -4
  58. package/src/configuration-files.ts +250 -31
  59. package/src/conformance-record.ts +1 -1
  60. package/src/contextual-uses.ts +135 -17
  61. package/src/convention-rows.ts +23 -0
  62. package/src/conventions.ts +53 -1
  63. package/src/dependencies.ts +119 -38
  64. package/src/discover.ts +3 -3
  65. package/src/edges.ts +3 -3
  66. package/src/element-access.ts +446 -0
  67. package/src/entry-points.ts +1 -1
  68. package/src/erased-stores.ts +413 -0
  69. package/src/exempt-classes.ts +10 -24
  70. package/src/exempt.ts +5 -29
  71. package/src/file-facts.ts +3 -1
  72. package/src/finding.ts +3 -1
  73. package/src/findings/completion.ts +97 -21
  74. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  75. package/src/findings/emitters.ts +4 -9
  76. package/src/findings/self-check.ts +31 -14
  77. package/src/findings-pass.ts +14 -7
  78. package/src/graph.ts +2 -2
  79. package/src/html-entries.ts +1 -1
  80. package/src/in-operands.ts +45 -0
  81. package/src/indexed-access.ts +303 -137
  82. package/src/intra-function-parts.ts +41 -16
  83. package/src/inventory.ts +37 -33
  84. package/src/json-document.ts +1 -1
  85. package/src/keyof-comparisons.ts +211 -0
  86. package/src/kinds.ts +20 -7
  87. package/src/manifest.ts +7 -22
  88. package/src/matrix.ts +34 -2
  89. package/src/namespace-objects.ts +1 -1
  90. package/src/pnpm-workspace.ts +1 -1
  91. package/src/query.ts +10 -0
  92. package/src/references.ts +30 -7
  93. package/src/report.ts +11 -5
  94. package/src/reporters/sarif.ts +4 -0
  95. package/src/roots.ts +0 -3
  96. package/src/run.ts +0 -2
  97. package/src/schema.ts +2 -2
  98. package/src/scope.ts +2 -2
  99. package/src/serialization-contract.ts +53 -14
  100. package/src/session.ts +34 -31
  101. package/src/stores.ts +1 -1
  102. package/src/suppress-file.ts +68 -5
  103. package/src/suppress-inline.ts +17 -7
  104. package/src/suppress.ts +79 -47
  105. package/src/template-field.ts +1 -1
  106. package/src/unimplemented-callees.ts +411 -0
  107. package/src/value-reach.ts +12 -8
  108. package/src/verbs/analyze.ts +15 -9
  109. package/src/verbs/verb.ts +1 -4
  110. package/src/workspace-programs.ts +2 -9
  111. package/src/workspace-resolution.ts +0 -3
  112. package/src/workspace-run.ts +1 -1
  113. package/src/workspace.ts +1 -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,12 +16,13 @@ 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";
24
23
  import { globExpression } from "./glob.js";
25
24
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
26
- import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
25
+ import { unmatchedRootFindings } from "./findings/self-check.js";
27
26
  import { frameworkLifecycle } from "./framework-lifecycle.js";
28
27
  import { implementations, mergeImplementations } from "./implementations.js";
29
28
  import { injectionContainer } from "./injection-container.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) => {
@@ -243,6 +283,7 @@ function readProjects(engine, host, scope, config, stage) {
243
283
  .filter((file) => referenceOnly(file.fileName))
244
284
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
245
285
  generated: generatedFilesOf(own, generatedFiles(conventions, own)),
286
+ testFileNames: conventions.testFiles,
246
287
  };
247
288
  };
248
289
  for (const file of project.ownSourceFiles()) {
@@ -252,8 +293,8 @@ function readProjects(engine, host, scope, config, stage) {
252
293
  }, discovered.derived ? "record" : "refuse", joins.size === 0
253
294
  ? workspace?.programs
254
295
  : withJoinedTests(engine, host, workspace?.programs ?? new Map(), joins), () => {
255
- outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path))), options.parse);
256
- });
296
+ outsideTests = testFilesAt(host, targetRoot, sourcePaths(host, targetRoot).filter((path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path))), parse);
297
+ }, readManifests);
257
298
  for (const configFile of session.unopened) {
258
299
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
259
300
  throw new DiscoveryError(`${configFile} opened no project`, []);
@@ -285,7 +326,8 @@ function readProjects(engine, host, scope, config, stage) {
285
326
  componentsUnread: workspace?.componentsUnread ?? [],
286
327
  conventionsApplied: conventions.applied,
287
328
  conventionUses: conventions.uses,
288
- memberDirs: workspace?.memberDirs ?? [],
329
+ memberDirs,
330
+ nestedDirs,
289
331
  outsideTests,
290
332
  };
291
333
  }
@@ -346,7 +388,7 @@ export function runRoots(engine, host, scope, config, provenance) {
346
388
  };
347
389
  }
348
390
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
349
- const DETECTORS = new Map([
391
+ export const DETECTORS = new Map([
350
392
  ["interface-satisfaction", interfaceSatisfaction],
351
393
  ["enum-group", enumGroup],
352
394
  ["generated-file", generatedFile],
@@ -374,6 +416,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
374
416
  const projectRead = opened.read();
375
417
  const resolved = references(project, projectRead.held, targetRoot, {
376
418
  testFiles: config.ts.testFiles,
419
+ testFileNames: projectRead.testFileNames,
377
420
  });
378
421
  const evidence = evidenceRecords({
379
422
  project,
@@ -399,7 +442,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
399
442
  line: lineOf(file.originalText, warning.offset),
400
443
  reason: warning.reason,
401
444
  }))),
402
- extra: extra(project, projectRead, resolved.references),
445
+ extra: extra(project, projectRead, resolved.references, resolved.testFilePaths),
403
446
  };
404
447
  });
405
448
  const outsidePaths = read.outsideTests.map((one) => one.path);
@@ -419,7 +462,6 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
419
462
  const { projectRead } = one;
420
463
  const satisfied = settled[index];
421
464
  const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
422
- disabled,
423
465
  mode,
424
466
  testFiles: new Set([...one.testFiles, ...support]),
425
467
  });
@@ -437,6 +479,9 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
437
479
  ...(projectRead.referenceOnly.length === 0
438
480
  ? {}
439
481
  : { referenceOnly: projectRead.referenceOnly }),
482
+ ...(projectRead.held.dependents.size === 0
483
+ ? {}
484
+ : { dependents: projectRead.held.dependents }),
440
485
  };
441
486
  return {
442
487
  configFile: projectRead.configFile,
@@ -484,6 +529,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
484
529
  conventionsApplied: read.conventionsApplied,
485
530
  conventionUses: read.conventionUses,
486
531
  memberDirs: read.memberDirs,
532
+ nestedDirs: read.nestedDirs,
487
533
  };
488
534
  }
489
535
  /**
@@ -610,28 +656,27 @@ function sweptOf(read, input) {
610
656
  };
611
657
  }
612
658
  /**
613
- * Sweeps the run the scope and the configuration describe. Each project's references
614
- * and exemptions are read against its own inventory while its view is open; the
615
- * projects then merge into one matrix in the run's order, which is swept under the
616
- * caller's marks and mode with the exemptions every project found. A record found in
617
- * one configuration holds in every configuration that holds its declaration, because
618
- * an exemption is evidence of a use the analysis cannot see and a use in one
619
- * configuration is a use. A project that carries an error ends the run before any
620
- * 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.
621
665
  */
622
666
  export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
623
667
  return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
624
668
  }
625
669
  /** Reads a run's projects with every per-project fact an emitter reads. */
626
- function readEmitterRun(engine, host, scope, config, mode, detectors) {
670
+ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
627
671
  const targetRoot = scope.target.path;
628
- 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) => ({
629
673
  deprecation: deprecatedDeclarations(project, read.held),
630
674
  accessors: accessorsOf(project, read.held),
631
675
  needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) => engine.parseSourceFile(fileName, text)),
632
676
  implementations: implementations(project, read.held),
633
677
  heldByInclusion: heldByInclusion(project, targetRoot),
634
678
  generated: [...read.generated.keys()],
679
+ inOperands: inOperandNames(project, targetRoot, testFiles),
635
680
  rooted: read.rooted,
636
681
  directives: inlineDirectives(project, read.held, targetRoot).filter((directive) => !read.referenceOnly.includes(directive.site.path)),
637
682
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
@@ -682,7 +727,7 @@ function nearestManifestDir(host, root, file) {
682
727
  * The input every emitter reads over one sweep of a read run: the facts beside the sweep
683
728
  * each kind family reads, every per-project one read in the same session as the sweep.
684
729
  */
685
- function emitterInputOver(host, scope, config, read, input, selfCheck) {
730
+ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
686
731
  const targetRoot = scope.target.path;
687
732
  const swept = sweptOf(read, input);
688
733
  const { projects } = read;
@@ -695,12 +740,13 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
695
740
  references: one.configured.references,
696
741
  accessors: one.extra.accessors,
697
742
  })), swept.exempt, input.mode),
698
- 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),
699
744
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
700
745
  files: {
701
746
  tree: sourceTree(host, targetRoot),
702
747
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
703
748
  generated: new Set(projects.flatMap((one) => one.extra.generated)),
749
+ inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
704
750
  },
705
751
  boundary: {
706
752
  consumers: {
@@ -714,17 +760,6 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
714
760
  intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
715
761
  };
716
762
  }
717
- /**
718
- * What every emitter reads of the run the scope and the configuration describe, swept
719
- * under the caller's marks: the run's sweep, the facts beside it each kind family reads,
720
- * and the sides of the target's declared cross-language edges that name a symbol of this
721
- * language. The run reads no suppression document, so the self-check family has nothing
722
- * to report.
723
- */
724
- export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
725
- const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
726
- return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
727
- }
728
763
  /**
729
764
  * Reads the run the scope and the configuration describe for its findings. The inline
730
765
  * directives, the ignore file's entries and the baseline's rows are bound before any
@@ -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") {
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { dirnamePath, joinPath } from "./paths.js";
9
9
  /** The name the mapper's manifest declares and every configuration's entry names. */
10
- export const MAPPER_PACKAGE = "deadset-ts-component-files";
10
+ const MAPPER_PACKAGE = "deadset-ts-component-files";
11
11
  /** The manifest of the mapper's package, which runs `command`. */
12
12
  export function mapperManifest(command) {
13
13
  return `${JSON.stringify({
@@ -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