@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/src/analysis.ts CHANGED
@@ -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
 
11
9
  import { lineOf, readComponent } from "./component-files.ts";
@@ -40,10 +38,12 @@ import {
40
38
  readConventions,
41
39
  type AppliedConvention,
42
40
  type ConventionManifest,
41
+ type Conventions,
43
42
  } from "./conventions.ts";
44
43
  import { readEdgeSides } from "./edges.ts";
45
44
  import { enumGroup } from "./enum-group.ts";
46
45
  import { generatedFile, generatedFilesOf } from "./generated-file.ts";
46
+ import { inOperandNames } from "./in-operands.ts";
47
47
  import { sourceFilesOf } from "./source-files.ts";
48
48
  import {
49
49
  evidenceRecords,
@@ -94,13 +94,26 @@ import {
94
94
  } from "./type-errors.ts";
95
95
  import { reflectiveLookup } from "./reflective-lookup.ts";
96
96
  import { references, testFileRulesOf, type Reference, type TestFileRule } from "./references.ts";
97
- import { roots, unmatchedEverywhere, type RootKind, type Roots } from "./roots.ts";
97
+ import {
98
+ roots,
99
+ unmatchedEverywhere,
100
+ type RootKind,
101
+ type RootOptions,
102
+ type Roots,
103
+ } from "./roots.ts";
98
104
  import type { Scope } from "./scope.ts";
99
105
  import { serializationContract } from "./serialization-contract.ts";
100
106
  import { outsideTestReferences, testFilesAt, type OutsideTest } from "./outside-tests.ts";
101
107
  import { joinedTestFiles, withJoinedTests } from "./test-joins.ts";
102
108
  import { supportReferences, testSupportFiles } from "./test-support.ts";
103
- import { diagnosticsOf, ownedAs, runSession, type Engine, type ProjectView } from "./session.ts";
109
+ import {
110
+ diagnosticsOf,
111
+ ownedAs,
112
+ runSession,
113
+ type Engine,
114
+ type OpenedFiles,
115
+ type ProjectView,
116
+ } from "./session.ts";
104
117
  import { accessorsOf, storesOf } from "./stores.ts";
105
118
  import { isPartKind, type SuppressionRecord, type Suppressions, type Verdict } from "./suppress.ts";
106
119
  import { bindRows, readBaseline, readIgnoreFile, type Recorded } from "./suppress-file.ts";
@@ -225,32 +238,73 @@ interface ReadProjects<Answer> {
225
238
  readonly conventionUses: readonly string[];
226
239
  /** The directory of every workspace member, absolute. */
227
240
  readonly memberDirs: readonly string[];
241
+ /** The directory of every nested package, absolute. */
242
+ readonly nestedDirs: readonly string[];
228
243
  /** The test files below the target root no project the run reads holds, parsed. */
229
244
  readonly outsideTests: readonly OutsideTest[];
230
245
  }
231
246
 
232
247
  /**
233
248
  * The manifests the analysis reads, each once: the target's own, then every workspace
234
- * member's below the target root.
249
+ * member's and every nested package's below the target root.
235
250
  */
236
251
  function conventionManifests(
237
252
  host: Host,
238
253
  targetRoot: string,
239
254
  memberDirs: readonly string[],
255
+ nestedDirs: readonly string[],
240
256
  ): readonly ConventionManifest[] {
241
257
  const found = new Map<string, ConventionManifest>();
242
258
  if (host.kindOf(joinPath(targetRoot, "package.json")) === "file") {
243
259
  found.set(targetRoot, { dir: targetRoot, path: "package.json" });
244
260
  }
245
- for (const dir of memberDirs) {
246
- const below = relativePath(targetRoot, dir);
247
- if (below !== undefined && !found.has(dir)) {
248
- found.set(dir, { dir, path: `${below}/package.json` });
261
+ for (const [dirs, nested] of [
262
+ [memberDirs, false],
263
+ [nestedDirs, true],
264
+ ] as const) {
265
+ for (const dir of dirs) {
266
+ const below = relativePath(targetRoot, dir);
267
+ if (below !== undefined && !found.has(dir)) {
268
+ found.set(dir, { dir, path: `${below}/package.json`, nested });
269
+ }
249
270
  }
250
271
  }
251
272
  return [...found.values()];
252
273
  }
253
274
 
275
+ /**
276
+ * The nested packages of a run: each directory below the target root, in no `node_modules`
277
+ * directory and none of `known`, that holds a `package.json` and lies at or above one of
278
+ * `files`, the program's source files and the compiler configuration files, in path order.
279
+ */
280
+ function nestedPackageDirs(
281
+ host: Host,
282
+ targetRoot: string,
283
+ files: Iterable<string>,
284
+ known: ReadonlySet<string>,
285
+ ): readonly string[] {
286
+ const seen = new Set<string>();
287
+ const found: string[] = [];
288
+ for (const file of files) {
289
+ for (
290
+ let dir = dirnamePath(file);
291
+ dir !== targetRoot && !seen.has(dir) && relativePath(targetRoot, dir) !== undefined;
292
+ dir = dirnamePath(dir)
293
+ ) {
294
+ seen.add(dir);
295
+ const below = relativePath(targetRoot, dir) ?? "";
296
+ if (
297
+ !below.split("/").includes("node_modules") &&
298
+ !known.has(dir) &&
299
+ host.kindOf(joinPath(dir, "package.json")) === "file"
300
+ ) {
301
+ found.push(dir);
302
+ }
303
+ }
304
+ }
305
+ return found.sort(compare);
306
+ }
307
+
254
308
  /**
255
309
  * Reads every project the scope discovers, and each declared consumer's, in one snapshot,
256
310
  * each checked for errors first. `stage` is handed a target project and enumerates and roots
@@ -268,6 +322,12 @@ function readProjects<Answer>(
268
322
  ): ReadProjects<Answer> {
269
323
  const targetRoot = scope.target.path;
270
324
  const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
325
+ if (discovered.configFiles.length === 0 && discovered.notBuilt.length === 0) {
326
+ throw new DiscoveryError(
327
+ `${targetRoot} holds no compiler configuration, so the run holds no program`,
328
+ [],
329
+ );
330
+ }
271
331
  const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
272
332
  const loads = consumerLoads(engine, host, scope, discovered.configFiles);
273
333
  const consumerOf = new Map(
@@ -283,33 +343,45 @@ function readProjects<Answer>(
283
343
  discovered.configFiles,
284
344
  readManifest(host, targetRoot),
285
345
  );
286
- const conventions = readConventions(
287
- host,
288
- (fileName, text) => engine.parseSourceFile(fileName, text),
289
- conventionManifests(host, absoluteRoot, workspace?.memberDirs ?? []),
290
- config.ts.disabledConventions,
291
- );
292
- const options = {
293
- host,
294
- manifest: workspace?.manifest ?? readManifest(host, targetRoot),
295
- otherPackages: [
296
- ...(workspace?.entries ?? []),
297
- ...packageEntries(
298
- host,
299
- normalizePath(absoluteRoot),
300
- discovered.configFiles,
301
- new Set(workspace?.memberDirs ?? []),
346
+ const parse = (fileName: string, text: string) => engine.parseSourceFile(fileName, text);
347
+ const memberDirs = workspace?.memberDirs ?? [];
348
+ // The nested packages are known only once every program is open, and every visit reads
349
+ // the conventions and options they decide, so both are set in the session's first step.
350
+ let nestedDirs: readonly string[] = [];
351
+ let conventions = readConventions(host, parse, [], config.ts.disabledConventions);
352
+ let options: RootOptions & { readonly parse: typeof parse; readonly conventions: Conventions };
353
+ const readManifests = (opened: readonly OpenedFiles[]): void => {
354
+ nestedDirs = nestedPackageDirs(
355
+ host,
356
+ normalizePath(absoluteRoot),
357
+ opened
358
+ .filter((one) => !consumerOf.has(one.configFile))
359
+ .flatMap((one) => [one.configFile, ...one.fileNames()]),
360
+ new Set(memberDirs),
361
+ );
362
+ conventions = readConventions(
363
+ host,
364
+ parse,
365
+ conventionManifests(host, absoluteRoot, memberDirs, nestedDirs),
366
+ config.ts.disabledConventions,
367
+ );
368
+ options = {
369
+ host,
370
+ manifest: workspace?.manifest ?? readManifest(host, targetRoot),
371
+ otherPackages: [
372
+ ...(workspace?.entries ?? []),
373
+ ...packageEntries(host, normalizePath(absoluteRoot), nestedDirs),
374
+ ],
375
+ patterns: config.rootPatterns,
376
+ entryFiles: config.ts.entryFiles,
377
+ conventions,
378
+ testFiles: config.ts.testFiles,
379
+ parse,
380
+ publishedAPI: config.targetKind === "library",
381
+ publishedMembers: new Set(
382
+ (workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir),
302
383
  ),
303
- ],
304
- patterns: config.rootPatterns,
305
- entryFiles: config.ts.entryFiles,
306
- conventions,
307
- testFiles: config.ts.testFiles,
308
- parse: (fileName: string, text: string) => engine.parseSourceFile(fileName, text),
309
- publishedAPI: config.targetKind === "library",
310
- publishedMembers: new Set(
311
- (workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir),
312
- ),
384
+ };
313
385
  };
314
386
  const notBuilt: NotBuilt[] = [...discovered.notBuilt];
315
387
  const dropped = (configFile: string, error: string): void => {
@@ -477,9 +549,10 @@ function readProjects<Answer>(
477
549
  sourcePaths(host, targetRoot).filter(
478
550
  (path) => !heldFiles.has(path) && testPatterns.some((pattern) => pattern.test(path)),
479
551
  ),
480
- options.parse,
552
+ parse,
481
553
  );
482
554
  },
555
+ readManifests,
483
556
  );
484
557
  for (const configFile of session.unopened) {
485
558
  if (consumerOf.has(configFile) || !isGuess(discovered, configFile)) {
@@ -518,7 +591,8 @@ function readProjects<Answer>(
518
591
  componentsUnread: workspace?.componentsUnread ?? [],
519
592
  conventionsApplied: conventions.applied,
520
593
  conventionUses: conventions.uses,
521
- memberDirs: workspace?.memberDirs ?? [],
594
+ memberDirs,
595
+ nestedDirs,
522
596
  outsideTests,
523
597
  };
524
598
  }
@@ -707,6 +781,8 @@ interface ReadRun<Extra> {
707
781
  readonly conventionUses: readonly string[];
708
782
  /** The directory of every workspace member, absolute. */
709
783
  readonly memberDirs: readonly string[];
784
+ /** The directory of every nested package, absolute. */
785
+ readonly nestedDirs: readonly string[];
710
786
  }
711
787
 
712
788
  /**
@@ -726,6 +802,7 @@ function readRun<Extra>(
726
802
  project: ProjectView<Brand>,
727
803
  read: ProjectRead,
728
804
  references: readonly Reference[],
805
+ testFiles: readonly string[],
729
806
  ) => Extra,
730
807
  ): ReadRun<Extra> {
731
808
  const targetRoot = scope.target.path;
@@ -771,7 +848,7 @@ function readRun<Extra>(
771
848
  reason: warning.reason,
772
849
  })),
773
850
  ),
774
- extra: extra(project, projectRead, resolved.references),
851
+ extra: extra(project, projectRead, resolved.references, resolved.testFilePaths),
775
852
  };
776
853
  });
777
854
  const outsidePaths = read.outsideTests.map((one) => one.path);
@@ -816,6 +893,9 @@ function readRun<Extra>(
816
893
  ...(projectRead.referenceOnly.length === 0
817
894
  ? {}
818
895
  : { referenceOnly: projectRead.referenceOnly }),
896
+ ...(projectRead.held.dependents.size === 0
897
+ ? {}
898
+ : { dependents: projectRead.held.dependents }),
819
899
  };
820
900
  return {
821
901
  configFile: projectRead.configFile,
@@ -865,6 +945,7 @@ function readRun<Extra>(
865
945
  conventionsApplied: read.conventionsApplied,
866
946
  conventionUses: read.conventionUses,
867
947
  memberDirs: read.memberDirs,
948
+ nestedDirs: read.nestedDirs,
868
949
  };
869
950
  }
870
951
 
@@ -1010,14 +1091,12 @@ function sweptOf<Extra>(
1010
1091
  }
1011
1092
 
1012
1093
  /**
1013
- * Sweeps the run the scope and the configuration describe. Each project's references
1014
- * and exemptions are read against its own inventory while its view is open; the
1015
- * projects then merge into one matrix in the run's order, which is swept under the
1016
- * caller's marks and mode with the exemptions every project found. A record found in
1017
- * one configuration holds in every configuration that holds its declaration, because
1018
- * an exemption is evidence of a use the analysis cannot see and a use in one
1019
- * configuration is a use. A project that carries an error ends the run before any
1020
- * project is swept.
1094
+ * Sweeps the run the scope and the configuration describe. Each project's references and
1095
+ * exemptions are read against its own inventory while its view is open; the projects then
1096
+ * merge into one matrix in run order, swept under the caller's marks and mode with every
1097
+ * project's exemptions. A record found in one configuration holds in every configuration
1098
+ * that holds its declaration, because an exemption evidences a use the analysis cannot see.
1099
+ * A project that carries an error ends the run before any project is swept.
1021
1100
  */
1022
1101
  export function runSweep(
1023
1102
  engine: Engine,
@@ -1041,6 +1120,7 @@ interface EmitterExtra {
1041
1120
  readonly implementations: ReturnType<typeof implementations>;
1042
1121
  readonly heldByInclusion: readonly string[];
1043
1122
  readonly generated: readonly string[];
1123
+ readonly inOperands: readonly string[];
1044
1124
  readonly rooted: Roots;
1045
1125
  readonly directives: readonly InlineDirective[];
1046
1126
  readonly declarationsNamed: readonly boolean[];
@@ -1064,7 +1144,7 @@ export function readEmitterRun(
1064
1144
  config,
1065
1145
  mode,
1066
1146
  detectors,
1067
- (project, read, references) => ({
1147
+ (project, read, references, testFiles) => ({
1068
1148
  deprecation: deprecatedDeclarations(project, read.held),
1069
1149
  accessors: accessorsOf(project, read.held),
1070
1150
  needs: projectNeeds(project, read.held, host, targetRoot, (fileName, text) =>
@@ -1073,6 +1153,7 @@ export function readEmitterRun(
1073
1153
  implementations: implementations(project, read.held),
1074
1154
  heldByInclusion: heldByInclusion(project, targetRoot),
1075
1155
  generated: [...read.generated.keys()],
1156
+ inOperands: inOperandNames(project, targetRoot, testFiles),
1076
1157
  rooted: read.rooted,
1077
1158
  directives: inlineDirectives(project, read.held, targetRoot).filter(
1078
1159
  (directive) => !read.referenceOnly.includes(directive.site.path),
@@ -1165,12 +1246,14 @@ export function emitterInputOver(
1165
1246
  swept.sweep.candidates.map((candidate) => candidate.id),
1166
1247
  members.read,
1167
1248
  members.unbuilt,
1249
+ read.nestedDirs,
1168
1250
  ),
1169
1251
  implementations: mergeImplementations(projects.map((one) => one.extra.implementations)),
1170
1252
  files: {
1171
1253
  tree: sourceTree(host, targetRoot),
1172
1254
  heldByInclusion: new Set(projects.flatMap((one) => one.extra.heldByInclusion)),
1173
1255
  generated: new Set(projects.flatMap((one) => one.extra.generated)),
1256
+ inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
1174
1257
  },
1175
1258
  boundary: {
1176
1259
  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
 
9
10
  import type { Inventory, InventorySymbol } from "./inventory.ts";
@@ -24,19 +25,21 @@ function isGlobalBlock(
24
25
  }
25
26
 
26
27
  /**
27
- * For each reference to a declaration an augmentation block holds, a read at the same place
28
- * of every interface and namespace between it and the block, and of a `declare global` block
29
- * itself.
28
+ * For each reference to a declaration an augmentation block, or the global scope of a
29
+ * file in `scripts`, holds, a read at the same place of every interface and namespace
30
+ * between it and the block or the file, and of the block itself. `scripts` holds the
31
+ * files that are no module, by identifier.
30
32
  */
31
33
  export function augmentationReferences(
32
34
  held: Inventory,
33
35
  found: readonly Reference[],
36
+ scripts: ReadonlySet<string> = new Set(),
34
37
  ): readonly Reference[] {
35
38
  const files = new Set(held.symbols.filter((one) => one.kind === "file").map((one) => one.id));
36
39
  const globals = new Set(
37
40
  held.symbols.filter((one) => isGlobalBlock(one, held, files)).map((one) => one.id),
38
41
  );
39
- if (globals.size === 0 && held.modules.size === 0) {
42
+ if (globals.size === 0 && held.modules.size === 0 && scripts.size === 0) {
40
43
  return [];
41
44
  }
42
45
  const byId = new Map(held.symbols.map((one) => [one.id, one]));
@@ -47,10 +50,10 @@ export function augmentationReferences(
47
50
  at !== undefined;
48
51
  at = byId.get(at.parent)
49
52
  ) {
50
- if (globals.has(at.id)) {
53
+ if (globals.has(at.id) || held.modules.has(at.id)) {
51
54
  return [...ids, at.id];
52
55
  }
53
- if (held.modules.has(at.id)) {
56
+ if (scripts.has(at.id)) {
54
57
  return ids;
55
58
  }
56
59
  if (at.kind === "interface" || at.kind === "namespace") {
package/src/calls.ts CHANGED
@@ -22,8 +22,8 @@ import {
22
22
  type Decorator,
23
23
  type Expression,
24
24
  type Node,
25
- } from "@typescript/native/unstable/ast";
26
- import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
25
+ } from "@typescript/native/ast";
26
+ import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/sync";
27
27
  import type { AliasChains } from "./alias-chain.ts";
28
28
  import { nodeKey, type Inventory } from "./inventory.ts";
29
29
  import { must } from "./query.ts";
@@ -4,8 +4,8 @@
4
4
  * that extends it and a member a subclass overrides is the subclass's own.
5
5
  */
6
6
 
7
- import { isClassDeclaration, isClassExpression, type Node } from "@typescript/native/unstable/ast";
8
- import type { Symbol as TSSymbol, SymbolFlags, Type } from "@typescript/native/unstable/sync";
7
+ import { isClassDeclaration, isClassExpression, type Node } from "@typescript/native/ast";
8
+ import type { Symbol as TSSymbol, SymbolFlags, Type } from "@typescript/native/sync";
9
9
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
10
10
  import { must } from "./query.ts";
11
11
  import type { ProjectView } from "./session.ts";
@@ -6,7 +6,7 @@
6
6
  * `node_modules` beside each configuration, to a directory holding only its manifest.
7
7
  */
8
8
 
9
- import type { CreateSnapshotParams } from "@typescript/native/unstable/sync";
9
+ import type { CreateSnapshotParams } from "@typescript/native/sync";
10
10
  import type { Host } from "./host.ts";
11
11
  import { dirnamePath, joinPath } from "./paths.ts";
12
12
 
package/src/components.ts CHANGED
@@ -37,8 +37,27 @@ export interface Component {
37
37
  * cycle with a live container is one, so a component has at least one.
38
38
  */
39
39
  readonly roots: readonly string[];
40
- /** The source lines one deletion of the component removes: the distinct lines its members span. */
40
+ /**
41
+ * The source lines one deletion of the component removes: the distinct lines its
42
+ * members span, and none for a component of held declarations.
43
+ */
41
44
  readonly deletableLines: number;
45
+ /**
46
+ * Whether its members are held declarations, which no mechanical edit deletes: a dead
47
+ * declaration whose deletion changes a live declaration's value, and every dead
48
+ * declaration such a one references or whose deletion would change its value.
49
+ */
50
+ readonly held: boolean;
51
+ }
52
+
53
+ /**
54
+ * What the held-declaration rule reads of a graph, by place: the places of the later
55
+ * declarations whose value each one's deletion changes, and whether reachability holds
56
+ * each one live in some configuration.
57
+ */
58
+ export interface Positional {
59
+ readonly dependents: readonly (readonly number[])[];
60
+ readonly live: readonly boolean[];
42
61
  }
43
62
 
44
63
  /** What a report carries for one dead component under one cascade mode. */
@@ -63,41 +82,97 @@ export function listing(component: Component, cascade: Cascade): Listing {
63
82
  /**
64
83
  * Groups the declarations `dead` marks into components, ordered by their first member's
65
84
  * site; `dead`, `testOfDeadCode` and `unreferencedTest` hold one flag per declaration of
66
- * the graph. Root members sit on the cycles no other cycle references. A cycle two roots
67
- * both reach is dead only through both, so a component is a weakly connected part of the
68
- * dead subgraph: no dead declaration of one references a declaration of another.
85
+ * the graph. Root members sit on the cycles no other cycle references. A component is a
86
+ * weakly connected part of the dead subgraph, because a cycle two roots reach is dead only
87
+ * through both. The held declarations `positional` decides form components of their own,
88
+ * and a dead declaration that is not held shares one with each of its dead dependents.
69
89
  */
70
90
  export function componentsOf(
71
91
  graph: Graph,
72
92
  dead: readonly boolean[],
73
93
  testOfDeadCode: readonly boolean[],
74
94
  unreferencedTest: readonly boolean[] = [],
95
+ positional: Positional = { dependents: [], live: [] },
75
96
  ): readonly Component[] {
76
- const { at, adjacent } = deadSubgraph(graph, dead, testOfDeadCode, unreferencedTest);
97
+ const { at, position, adjacent, references } = deadSubgraph(
98
+ graph,
99
+ dead,
100
+ testOfDeadCode,
101
+ unreferencedTest,
102
+ );
77
103
  if (at.length === 0) {
78
104
  return [];
79
105
  }
80
- const { componentOf, members } = connected(adjacent);
81
- const referenced = referencedCycles(adjacent, componentOf, members.length);
82
- const groups = joined(adjacent);
83
- const idOf = (position: number): string => graph.symbols[at[position] ?? 0]?.id ?? "";
106
+ const dependents = at.map((index) =>
107
+ (positional.dependents[index] ?? []).map((to) => ({
108
+ to: position[to] ?? UNVISITED,
109
+ live: positional.live[to] === true,
110
+ })),
111
+ );
112
+ const held = heldSet(dependents, references);
113
+ const kept = adjacent.map((targets, from) => targets.filter((to) => held[to] === held[from]));
114
+ const { componentOf, members } = connected(kept);
115
+ const referenced = referencedCycles(kept, componentOf, members.length);
116
+ const groups = joined(
117
+ kept.map((targets, from) => [
118
+ ...targets,
119
+ ...(held[from] === true
120
+ ? []
121
+ : (dependents[from] ?? []).flatMap(({ to }) =>
122
+ to === UNVISITED || held[to] === true ? [] : [to],
123
+ )),
124
+ ]),
125
+ );
126
+ const idOf = (place: number): string => graph.symbols[at[place] ?? 0]?.id ?? "";
84
127
 
85
128
  return groups.map((group, place) => {
86
- const roots = group.filter((position) => {
129
+ const roots = group.filter((one) => {
87
130
  // A dead member's dead container is on the member's own cycle, and the container
88
131
  // is where the deletion starts.
89
- const container = graph.parent[at[position] ?? 0] ?? OUTSIDE;
90
- return referenced[componentOf[position] ?? 0] !== true && dead[container] !== true;
132
+ const container = graph.parent[at[one] ?? 0] ?? OUTSIDE;
133
+ return referenced[componentOf[one] ?? 0] !== true && dead[container] !== true;
91
134
  });
135
+ const isHeld = group.every((one) => held[one] === true);
92
136
  return {
93
137
  id: componentId(place + 1),
94
138
  members: group.map(idOf),
95
139
  roots: roots.map(idOf),
96
- deletableLines: linesSpanned(group.map((position) => graph.symbols[at[position] ?? 0])),
140
+ deletableLines: isHeld ? 0 : linesSpanned(group.map((one) => graph.symbols[at[one] ?? 0])),
141
+ held: isHeld,
97
142
  };
98
143
  });
99
144
  }
100
145
 
146
+ /**
147
+ * Per place of the dead subgraph, whether the declaration is held: one with a live
148
+ * dependent, and each dead declaration a held one references or has as a dependent,
149
+ * until no further one joins.
150
+ */
151
+ function heldSet(
152
+ dependents: readonly (readonly { readonly to: number; readonly live: boolean }[])[],
153
+ references: readonly (readonly number[])[],
154
+ ): readonly boolean[] {
155
+ const held = dependents.map((after) => after.some((one) => one.live));
156
+ const before: number[][] = dependents.map(() => []);
157
+ dependents.forEach((after, from) => {
158
+ for (const { to } of after) {
159
+ if (to !== UNVISITED) {
160
+ before[to]?.push(from);
161
+ }
162
+ }
163
+ });
164
+ const pending = held.flatMap((one, place) => (one ? [place] : []));
165
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
166
+ for (const reached of [...(references[next] ?? []), ...(before[next] ?? [])]) {
167
+ if (held[reached] !== true) {
168
+ held[reached] = true;
169
+ pending.push(reached);
170
+ }
171
+ }
172
+ }
173
+ return held;
174
+ }
175
+
101
176
  /**
102
177
  * The distinct source lines a set of declarations spans, each declaration at least the
103
178
  * line it starts on. A member written inside its container's span adds no line, because
@@ -133,19 +208,24 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
133
208
  }
134
209
 
135
210
  /**
136
- * The dead declarations by their place in the subgraph, a test file's declaration only
137
- * where it is a test of dead code or nothing the tests reach, and the edges between them:
138
- * each reference one makes to another, except an unreferenced test-file declaration's
139
- * reference to production code, which decides nothing of its target, an edge each way
140
- * between a dead member and its dead container, and an edge back from each production
141
- * target of an admitted test, which puts the test in its subject's component.
211
+ * The dead declarations by subgraph place (`at`, and `position` per graph declaration),
212
+ * a test file's only where it tests dead code or nothing the tests reach. `adjacent` holds
213
+ * each reference one makes to another, except an unreferenced test-file declaration's to
214
+ * production code, which decides nothing of its target; an edge each way between a dead
215
+ * member and its dead container; and one back from each production target of an admitted
216
+ * test, which puts the test in its subject's component. `references` holds the first alone.
142
217
  */
143
218
  function deadSubgraph(
144
219
  graph: Graph,
145
220
  dead: readonly boolean[],
146
221
  testOfDeadCode: readonly boolean[],
147
222
  unreferencedTest: readonly boolean[],
148
- ): { readonly at: readonly number[]; readonly adjacent: readonly (readonly number[])[] } {
223
+ ): {
224
+ readonly at: readonly number[];
225
+ readonly position: readonly number[];
226
+ readonly adjacent: readonly (readonly number[])[];
227
+ readonly references: readonly (readonly number[])[];
228
+ } {
149
229
  const at: number[] = [];
150
230
  const position = graph.symbols.map((_symbol, index) => {
151
231
  const judged = testOfDeadCode[index] === true || unreferencedTest[index] === true;
@@ -156,6 +236,7 @@ function deadSubgraph(
156
236
  return at.length - 1;
157
237
  });
158
238
  const adjacent: number[][] = at.map(() => []);
239
+ const references: number[][] = at.map(() => []);
159
240
  at.forEach((index, from) => {
160
241
  for (const edge of graph.out[index] ?? []) {
161
242
  const to = position[edge.to] ?? UNVISITED;
@@ -163,6 +244,7 @@ function deadSubgraph(
163
244
  continue;
164
245
  }
165
246
  adjacent[from]?.push(to);
247
+ references[from]?.push(to);
166
248
  if (
167
249
  testOfDeadCode[index] === true &&
168
250
  graph.test[edge.to] !== true &&
@@ -177,7 +259,7 @@ function deadSubgraph(
177
259
  adjacent[container]?.push(from);
178
260
  }
179
261
  });
180
- return { at, adjacent };
262
+ return { at, position, adjacent, references };
181
263
  }
182
264
 
183
265
  /** One frame of the component walk: a declaration, and the next of its edges to follow. */