@cplieger/deadset-ts 8.0.0-dev.16 → 8.0.0-dev.18

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 (161) hide show
  1. package/dist/src/alias-chain.js +0 -5
  2. package/dist/src/amd.js +81 -0
  3. package/dist/src/analysis.js +82 -91
  4. package/dist/src/augmentations.js +24 -25
  5. package/dist/src/bindings.js +382 -0
  6. package/dist/src/calls.js +279 -43
  7. package/dist/src/commands/tools.js +5 -9
  8. package/dist/src/component-files.js +3 -2
  9. package/dist/src/conformance-record.js +1 -1
  10. package/dist/src/consumers.js +6 -9
  11. package/dist/src/contextual-uses.js +1 -17
  12. package/dist/src/contract/config.generated.js +4 -1
  13. package/dist/src/contract/contract.generated.js +2 -2
  14. package/dist/src/contract/report.generated.js +6 -2
  15. package/dist/src/dependencies.js +25 -29
  16. package/dist/src/destructuring.js +1 -19
  17. package/dist/src/element-access.js +179 -164
  18. package/dist/src/entries/build-scripts.js +2 -2
  19. package/dist/src/entries/configuration-files.js +3 -7
  20. package/dist/src/entries/conventions.js +2 -2
  21. package/dist/src/entries/html-entries.js +7 -10
  22. package/dist/src/entries/mdx-entries.js +2 -2
  23. package/dist/src/entries/static-module.js +2 -1
  24. package/dist/src/exempt.js +0 -26
  25. package/dist/src/exposure.js +400 -0
  26. package/dist/src/facts.js +4 -4
  27. package/dist/src/findings/completion.js +44 -21
  28. package/dist/src/findings/emitters.js +22 -33
  29. package/dist/src/{implementations.js → findings/implementations.js} +3 -3
  30. package/dist/src/{in-operands.js → findings/in-operands.js} +2 -2
  31. package/dist/src/findings/intra-function.js +3 -3
  32. package/dist/src/findings/reads-and-writes.js +71 -15
  33. package/dist/src/findings/unused-declarations.js +2 -22
  34. package/dist/src/findings/visibility-narrowing.js +14 -20
  35. package/dist/src/framework-lifecycle.js +41 -14
  36. package/dist/src/import-bindings.js +73 -28
  37. package/dist/src/index/sites.js +557 -0
  38. package/dist/src/indexed-access.js +523 -478
  39. package/dist/src/interface-satisfaction.js +1 -1
  40. package/dist/src/inventory.js +18 -0
  41. package/dist/src/keyof-comparisons.js +124 -103
  42. package/dist/src/{components.js → liveness/components.js} +1 -1
  43. package/dist/src/{graph.js → liveness/graph.js} +45 -20
  44. package/dist/src/liveness/matrix.js +99 -0
  45. package/dist/src/liveness/sweep.js +558 -0
  46. package/dist/src/load/expected-module.js +2 -2
  47. package/dist/src/load/specifiers.js +2 -4
  48. package/dist/src/load/synthesize.js +5 -0
  49. package/dist/src/load/workspace-resolution.js +2 -2
  50. package/dist/src/module-declarations.js +70 -85
  51. package/dist/src/overrides.js +134 -112
  52. package/dist/src/paths.js +46 -7
  53. package/dist/src/platform-calls.js +2 -2
  54. package/dist/src/platform-symbols.js +13 -19
  55. package/dist/src/query.js +46 -0
  56. package/dist/src/references/read-rule.js +1 -0
  57. package/dist/src/references/read-rules.js +48 -0
  58. package/dist/src/references/sink.js +237 -0
  59. package/dist/src/references/sources.js +450 -0
  60. package/dist/src/references.js +110 -1285
  61. package/dist/src/roots/configured-declarations.js +3 -0
  62. package/dist/src/roots/published-reach.js +4 -4
  63. package/dist/src/roots/root-set.js +1 -1
  64. package/dist/src/roots/unseen-loads.js +38 -10
  65. package/dist/src/serialization-contract.js +8 -3
  66. package/dist/src/stores.js +4 -2
  67. package/dist/src/symbol-keys.js +89 -83
  68. package/dist/src/tests/file-class.js +76 -0
  69. package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
  70. package/dist/src/{test-support.js → tests/support.js} +2 -11
  71. package/dist/src/type-parameter-reads.js +178 -139
  72. package/dist/src/unimplemented-callees.js +262 -242
  73. package/dist/src/verbs/analyze.js +3 -3
  74. package/dist/src/verbs/explain.js +75 -132
  75. package/dist/src/verbs/print-retained.js +3 -3
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/alias-chain.ts +0 -7
  79. package/src/amd.ts +130 -0
  80. package/src/analysis.ts +125 -128
  81. package/src/augmentations.ts +42 -28
  82. package/src/bindings.ts +460 -0
  83. package/src/calls.ts +338 -46
  84. package/src/commands/tools.ts +5 -11
  85. package/src/component-files.ts +4 -2
  86. package/src/conformance-record.ts +1 -1
  87. package/src/consumers.ts +8 -11
  88. package/src/contextual-uses.ts +2 -22
  89. package/src/contract/config.generated.ts +4 -1
  90. package/src/contract/contract.generated.ts +2 -2
  91. package/src/contract/report.generated.ts +6 -2
  92. package/src/dependencies.ts +27 -28
  93. package/src/destructuring.ts +1 -31
  94. package/src/element-access.ts +188 -182
  95. package/src/entries/build-scripts.ts +2 -2
  96. package/src/entries/configuration-files.ts +11 -8
  97. package/src/entries/conventions.ts +2 -1
  98. package/src/entries/html-entries.ts +15 -14
  99. package/src/entries/mdx-entries.ts +2 -2
  100. package/src/entries/static-module.ts +2 -1
  101. package/src/exempt.ts +31 -46
  102. package/src/exposure.ts +492 -0
  103. package/src/facts.ts +5 -3
  104. package/src/finding.ts +1 -1
  105. package/src/findings/boundary.ts +9 -0
  106. package/src/findings/completion.ts +48 -23
  107. package/src/findings/emitter.ts +1 -1
  108. package/src/findings/emitters.ts +23 -41
  109. package/src/{implementations.ts → findings/implementations.ts} +4 -4
  110. package/src/{in-operands.ts → findings/in-operands.ts} +3 -3
  111. package/src/findings/interfaces.ts +3 -3
  112. package/src/findings/intra-function.ts +3 -3
  113. package/src/findings/reads-and-writes.ts +99 -26
  114. package/src/findings/unused-declarations.ts +4 -28
  115. package/src/findings/visibility-narrowing.ts +25 -26
  116. package/src/framework-lifecycle.ts +42 -14
  117. package/src/import-bindings.ts +98 -29
  118. package/src/index/sites.ts +781 -0
  119. package/src/indexed-access.ts +570 -527
  120. package/src/interface-satisfaction.ts +2 -2
  121. package/src/inventory.ts +19 -0
  122. package/src/keyof-comparisons.ts +131 -116
  123. package/src/{components.ts → liveness/components.ts} +3 -3
  124. package/src/{graph.ts → liveness/graph.ts} +93 -27
  125. package/src/liveness/matrix.ts +162 -0
  126. package/src/liveness/sweep.ts +855 -0
  127. package/src/load/expected-module.ts +8 -2
  128. package/src/load/specifiers.ts +8 -5
  129. package/src/load/synthesize.ts +6 -0
  130. package/src/load/workspace-resolution.ts +9 -2
  131. package/src/module-declarations.ts +74 -98
  132. package/src/overrides.ts +162 -134
  133. package/src/paths.ts +54 -8
  134. package/src/platform-calls.ts +3 -3
  135. package/src/platform-symbols.ts +11 -27
  136. package/src/query.ts +81 -0
  137. package/src/references/read-rule.ts +60 -0
  138. package/src/references/read-rules.ts +55 -0
  139. package/src/references/sink.ts +314 -0
  140. package/src/references/sources.ts +632 -0
  141. package/src/references.ts +175 -1733
  142. package/src/report.ts +1 -1
  143. package/src/roots/configured-declarations.ts +3 -0
  144. package/src/roots/published-reach.ts +9 -4
  145. package/src/roots/root-set.ts +1 -1
  146. package/src/roots/unseen-loads.ts +49 -11
  147. package/src/serialization-contract.ts +8 -3
  148. package/src/stores.ts +15 -3
  149. package/src/symbol-keys.ts +102 -102
  150. package/src/tests/file-class.ts +135 -0
  151. package/src/{outside-tests.ts → tests/outside.ts} +16 -10
  152. package/src/{test-support.ts → tests/support.ts} +9 -20
  153. package/src/type-parameter-reads.ts +187 -158
  154. package/src/unimplemented-callees.ts +297 -280
  155. package/src/verbs/analyze.ts +3 -2
  156. package/src/verbs/explain.ts +83 -158
  157. package/src/verbs/print-retained.ts +3 -3
  158. package/dist/src/matrix.js +0 -273
  159. package/dist/src/sweep.js +0 -222
  160. package/src/matrix.ts +0 -463
  161. package/src/sweep.ts +0 -361
@@ -4,9 +4,9 @@
4
4
  * together with every finding that falls with a root they withhold.
5
5
  */
6
6
  import { CODE, FAMILY_KEY_LENGTH, KIND_ROWS, } from "../contract/contract.generated.js";
7
- import { componentId } from "../components.js";
8
- import { deadComponents } from "../matrix.js";
9
- import { OUTSIDE } from "../graph.js";
7
+ import { componentId } from "../liveness/components.js";
8
+ import { deadComponents } from "../liveness/sweep.js";
9
+ import { OUTSIDE } from "../liveness/graph.js";
10
10
  import { KINDS } from "../kinds.js";
11
11
  import { byPosition, positionKey } from "../position.js";
12
12
  import { declarationKey } from "../ref.js";
@@ -79,12 +79,13 @@ function testedByIn(input) {
79
79
  * published API, a public member of a published type included, is `certain` where the
80
80
  * run loaded every consumer the scope declared and declared one, `probable` where one did
81
81
  * not load, and `possible` where it declared none, whatever the configuration says of the
82
- * consumer set. A member an `in` expression tests by name, and one a function outside the
83
- * analysis fills, is `possible`.
82
+ * consumer set. A member an `in` expression tests by name, one a function outside the
83
+ * analysis fills, and one {@link possiblyLoaded} names, is `possible`.
84
84
  */
85
- function reachabilityClasses(input) {
85
+ export function reachabilityClasses(input) {
86
86
  const tested = testedByIn(input);
87
87
  const filled = input.boundary.filledOutside ?? new Set();
88
+ const possible = possiblyLoaded(input);
88
89
  const published = publishedIds(input.swept.matrix.union);
89
90
  const { declared } = input.boundary.consumers;
90
91
  const outside = declared.length === 0
@@ -93,25 +94,12 @@ function reachabilityClasses(input) {
93
94
  ? "certain"
94
95
  : "probable";
95
96
  return (id) => {
96
- if (tested.has(id) || filled.has(id)) {
97
+ if (tested.has(id) || filled.has(id) || possible.has(id)) {
97
98
  return "possible";
98
99
  }
99
100
  return published.has(id) ? outside : "certain";
100
101
  };
101
102
  }
102
- /**
103
- * Whether one test of dead code references a target symbol that is published API of a run
104
- * that declares no consumer, which outside programs the run cannot see may use, so the
105
- * target counts as live for the test and the test is no test of dead code.
106
- */
107
- export function testsUnseenAPI(input) {
108
- if (input.boundary.consumers.declared.length > 0) {
109
- return () => false;
110
- }
111
- const union = input.swept.matrix.union;
112
- const published = publishedIds(union);
113
- return (id) => testTargets(union, id).some((target) => published.has(target));
114
- }
115
103
  /** The target symbols one test of dead code references, by id: production declarations the sweep judges. */
116
104
  function testTargets(union, id) {
117
105
  return (union.out[union.at(id)] ?? []).flatMap((edge) => {
@@ -137,6 +125,35 @@ function testCeilings(input, classOf) {
137
125
  ? testTargets(union, id).reduce((lowest, target) => capped(lowest, classOf(target)), "certain")
138
126
  : "certain";
139
127
  }
128
+ /**
129
+ * What only a bundler's retry of an import Node leaves unresolved names (program.38), and each
130
+ * dead declaration that reaches through dead declarations, as confidence.20 reads what only
131
+ * tests reach: each is dead only while no loader the analysis does not see loads the file.
132
+ */
133
+ function possiblyLoaded(input) {
134
+ const seeds = input.boundary.possiblyLoaded ?? new Set();
135
+ if (seeds.size === 0) {
136
+ return seeds;
137
+ }
138
+ const union = input.swept.matrix.union;
139
+ const dead = new Set(input.swept.sweep.candidates.map((one) => one.id));
140
+ const reached = new Set();
141
+ const pending = union.symbols.flatMap((symbol, at) => (seeds.has(symbol.id) ? [at] : []));
142
+ for (let at = pending.pop(); at !== undefined; at = pending.pop()) {
143
+ const symbol = union.symbols[at];
144
+ if (symbol === undefined || reached.has(symbol.id)) {
145
+ continue;
146
+ }
147
+ reached.add(symbol.id);
148
+ for (const edge of union.out[at] ?? []) {
149
+ const to = union.symbols[edge.to];
150
+ if (to !== undefined && dead.has(to.id)) {
151
+ pending.push(edge.to);
152
+ }
153
+ }
154
+ }
155
+ return reached;
156
+ }
140
157
  /**
141
158
  * The subjects dead only because production code only tests use is: each declaration the
142
159
  * sweep marks test-used, each `DS1602` file of such code, and each test of dead code every
@@ -281,6 +298,7 @@ export function completed(input, findings, kinds = KINDS) {
281
298
  const union = swept.matrix.union;
282
299
  const positions = new Map(union.symbols.map((symbol, at) => [declarationKey(symbol.ref, symbol.position.path), at]));
283
300
  const candidates = new Map(swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
301
+ const fileIds = new Map(union.symbols.flatMap((one) => (one.kind === "file" ? [[one.position.path, one.id]] : [])));
284
302
  const classOf = reachabilityClasses(input);
285
303
  const ceilingOf = testCeilings(input, classOf);
286
304
  const testUsed = testUsedSubjects(input);
@@ -365,7 +383,12 @@ export function completed(input, findings, kinds = KINDS) {
365
383
  const counts = candidate === undefined
366
384
  ? union.made[at]
367
385
  : { production: candidate.productionRefs, test: candidate.testRefs };
368
- const ownClass = judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
386
+ const ownClass = judged !== undefined
387
+ ? classOf(judged.id)
388
+ : (finding.reachabilityClass ??
389
+ (finding.code === CODE.fileNeverImported
390
+ ? classOf(fileIds.get(finding.position.path) ?? "")
391
+ : "certain"));
369
392
  const used = judged !== undefined
370
393
  ? candidate !== undefined && testUsed.declarations.has(judged.id)
371
394
  : finding.code === CODE.fileNeverImported && testUsed.files.has(finding.position.path);
@@ -1,5 +1,4 @@
1
1
  import { evaluateEdges } from "../edges.js";
2
- import { liveFilesAt } from "../matrix.js";
3
2
  import { declarationKey } from "../ref.js";
4
3
  import { ledgerOf } from "../suppress.js";
5
4
  import { insideSkipped } from "../type-errors.js";
@@ -8,7 +7,7 @@ import { dependenciesAndModuleMachinery } from "./dependencies-and-module-machin
8
7
  import { interfaces } from "./interfaces.js";
9
8
  import { intraFunction } from "./intra-function.js";
10
9
  import { nonCodeArtifacts } from "./non-code-artifacts.js";
11
- import { readsAndWrites } from "./reads-and-writes.js";
10
+ import { fallingWithWrites, readsAndWrites } from "./reads-and-writes.js";
12
11
  import { selfCheck } from "./self-check.js";
13
12
  import { fallenMembers, unusedDeclarations } from "./unused-declarations.js";
14
13
  import { visibilityNarrowing } from "./visibility-narrowing.js";
@@ -23,37 +22,25 @@ export const EMITTERS = new Map([
23
22
  ["self-check", selfCheck],
24
23
  ["intra-function", intraFunction],
25
24
  ]);
26
- /**
27
- * The keys of every declaration an unanswered question could have kept live, which the run
28
- * reports nothing about: a finding about one would be computed from the gap.
29
- */
30
- function heldKeys(input) {
31
- const held = new Set(input.swept.heldByUnanswered);
32
- if (held.size === 0) {
33
- return held;
34
- }
25
+ /** The keys of the declarations the identifiers name, as {@link findingKey} keys a finding. */
26
+ function keysOf(input, ids) {
27
+ const named = new Set(ids);
35
28
  return new Set(input.swept.matrix.union.symbols
36
- .filter((symbol) => held.has(symbol.id))
29
+ .filter((symbol) => named.has(symbol.id))
37
30
  .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
38
31
  }
32
+ /** The key of the declaration a finding is about, or of the one holding the part it is about. */
33
+ function findingKey(finding) {
34
+ return declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
35
+ }
39
36
  /**
40
- * The keys of every top-level declaration of a live component file. The file's
41
- * markup may use each of them, which no analysis of the markup can rule out, so the run
42
- * reports nothing about one while the file is live.
37
+ * Every emitter's findings over one sweep, and those of any further emitter, completed by
38
+ * one step, but none about a declaration that falls with a write-only subject: that
39
+ * subject's finding names it, so no finding about it exists for a record to match.
43
40
  */
44
- function markupKeys(input) {
45
- const { swept } = input;
46
- if (swept.componentFiles.length === 0) {
47
- return new Set();
48
- }
49
- const live = new Set(liveFilesAt(swept.matrix, swept.sweep, new Set(swept.componentFiles)).map((symbol) => symbol.id));
50
- return new Set(swept.matrix.union.symbols
51
- .filter((symbol) => live.has(symbol.parent))
52
- .map((symbol) => declarationKey(symbol.ref, symbol.position.path)));
53
- }
54
- /** Every emitter's findings over one sweep, and those of any further emitter, completed by one step. */
55
41
  function completedOver(input, ...more) {
56
- return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input)));
42
+ const falls = keysOf(input, fallingWithWrites(input));
43
+ return completed(input, [...EMITTERS.values(), ...more].flatMap((emit) => emit(input))).filter((finding) => !falls.has(findingKey(finding)));
57
44
  }
58
45
  /**
59
46
  * The dials over the findings a record could name, each read against the sweep that
@@ -96,13 +83,15 @@ function atPossible(input) {
96
83
  */
97
84
  function dialledOver(input, suppressed, found, unmarked) {
98
85
  const would = [...unmarked, ...found];
99
- const unanswered = heldKeys(input);
100
- const heldIds = new Set(input.swept.heldByUnanswered);
101
- const keyOf = (finding) => declarationKey(finding.symbol.holder ?? finding.symbol.ref, finding.position.path);
102
- const held = (finding) => unanswered.has(keyOf(finding)) || insideSkipped(input.swept.skipped, finding.position);
86
+ const { sweep } = input.swept;
87
+ // A hold is a gap of the analysis, which leaves a record it touches dormant; a live
88
+ // component file's markup is no gap, so a record on one of its bindings stays in effect.
89
+ const unanswered = keysOf(input, sweep.held);
90
+ const markup = keysOf(input, sweep.bindings);
91
+ const heldIds = new Set(sweep.held);
92
+ const held = (finding) => unanswered.has(findingKey(finding)) || insideSkipped(input.swept.skipped, finding.position);
103
93
  const ledger = ledgerOf(suppressed.records, would, dialsOver(input, found, suppressed, unmarked), { withholds: held, holdsRecord: (record) => heldIds.has(record.bound) });
104
- const markup = markupKeys(input);
105
- const candidates = found.filter((finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(keyOf(finding)));
94
+ const candidates = found.filter((finding) => !ledger.withheld(finding) && !held(finding) && !markup.has(findingKey(finding)));
106
95
  const kept = reportable(input, candidates);
107
96
  const evaluated = evaluateEdges(kept, input.boundary.edges, input.swept.matrix.union.symbols);
108
97
  return {
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import { isClassDeclaration, isHeritageClause, isInterfaceDeclaration, isMethodDeclaration, SyntaxKind, } from "@typescript/native/ast";
9
9
  import { SymbolFlags } from "@typescript/native/sync";
10
- import { aliasChains } from "./alias-chain.js";
11
- import { nodeKey } from "./inventory.js";
12
- import { must, Unanswerable } from "./query.js";
10
+ import { aliasChains } from "../alias-chain.js";
11
+ import { nodeKey } from "../inventory.js";
12
+ import { must, Unanswerable } from "../query.js";
13
13
  /** The interfaces, the classes and the `implements` entries of one project's own files. */
14
14
  function walk(project, held) {
15
15
  const interfaces = [];
@@ -4,8 +4,8 @@
4
4
  * links the read to a declaration, so only a text match can tell which member it reads.
5
5
  */
6
6
  import { isBinaryExpression, isNoSubstitutionTemplateLiteral, isStringLiteral, SyntaxKind, } from "@typescript/native/ast";
7
- import { sourceFilesOf } from "./source-files.js";
8
- import { bytewise } from "./text/compare.js";
7
+ import { sourceFilesOf } from "../source-files.js";
8
+ import { bytewise } from "../text/compare.js";
9
9
  /**
10
10
  * The text of each string literal, or template literal with no substitution, written as
11
11
  * the left operand of an `in` expression in a file of the project that is no test file.
@@ -2,7 +2,7 @@ import { CODE } from "../contract/contract.generated.js";
2
2
  import { KINDS } from "../kinds.js";
3
3
  import { parameterRef } from "../ref.js";
4
4
  import {} from "../intra-function-parts.js";
5
- import { reachedWithTheTests } from "../sweep.js";
5
+ import { reachedWithTheTests } from "../liveness/sweep.js";
6
6
  /** A list of rules as a sentence names them. */
7
7
  function spelled(rules) {
8
8
  if (rules.length < 2) {
@@ -84,8 +84,8 @@ export const intraFunction = (input) => {
84
84
  const union = input.swept.matrix.union;
85
85
  const byId = new Map(union.symbols.map((symbol) => [symbol.id, symbol]));
86
86
  const reached = reachedWithTheTests(union, [
87
- ...input.swept.exempt.map((record) => record.id),
88
- ...input.swept.heldByUnanswered,
87
+ ...input.swept.sweep.exempt.map((record) => record.id),
88
+ ...input.swept.sweep.held,
89
89
  ]);
90
90
  const testsOfDeadCode = new Set(input.swept.sweep.candidates.filter((one) => one.testOfDeadCode).map((one) => one.id));
91
91
  const judged = (id) => {
@@ -1,6 +1,8 @@
1
1
  import { CODE } from "../contract/contract.generated.js";
2
+ import { WORDS } from "../inventory.js";
2
3
  import { positionKey } from "../position.js";
3
4
  import { declarationKey } from "../ref.js";
5
+ import { reachabilityClasses } from "./completion.js";
4
6
  import { bytewise } from "../text/compare.js";
5
7
  /** The containers whose type parameters `DS1006` reports: a function, a class method and a type alias. */
6
8
  const SIGNATURES = new Set(["function", "method", "type"]);
@@ -70,14 +72,16 @@ function parametersOnlyNamedBy(run, namedBy, member) {
70
72
  .map((symbol) => symbol.name);
71
73
  }
72
74
  /** What one write-only finding says, the declarations its deletion takes with it included. */
73
- function writeOnlyMessage(run, namedBy, symbol, held) {
75
+ function writeOnlyMessage(run, written, subject) {
76
+ const { symbol, held } = subject;
74
77
  const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
75
78
  const read = run.input.stores.readInTests.has(symbol.id)
76
79
  ? "read only from test files"
77
80
  : "never read";
78
81
  const cascade = [
79
- ...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
82
+ ...parametersOnlyNamedBy(run, written.namedBy, symbol).map((name) => `type parameter ${name}`),
80
83
  ...[...held.alone].sort(bytewise).map((name) => `import ${name}`),
84
+ ...(written.named.get(symbol.id) ?? []),
81
85
  ];
82
86
  const word = SUBJECT_WORDS[symbol.kind] ?? "member";
83
87
  const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
@@ -86,16 +90,34 @@ function writeOnlyMessage(run, namedBy, symbol, held) {
86
90
  : `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
87
91
  }
88
92
  /**
89
- * `DS1301`: a module-level variable, a type member or a class member the counted
90
- * references store into and never read, named with every position written.
93
+ * The kinds of declaration that fall with a write-only subject: a declaration of its own.
94
+ * A member falls with its container, and a type parameter is the type-parameter clause's.
95
+ */
96
+ const FALLING = new Set([
97
+ "function",
98
+ "class",
99
+ "interface",
100
+ "type",
101
+ "enum",
102
+ "namespace",
103
+ "variable",
104
+ ]);
105
+ const WRITTEN = new WeakMap();
106
+ /**
107
+ * `DS1301`'s subjects: a module-level variable, a type member or a class member the
108
+ * counted references store into and never read.
91
109
  *
92
110
  * The subject is a live declaration: a store is a reference, so the sweep holds a
93
111
  * written one live and its candidates are never this kind's. Every reference that is
94
112
  * not a store reads, and so does an exemption record naming the declaration.
95
113
  */
96
- function writeOnlySymbols(run) {
114
+ function writtenOf(run) {
115
+ const known = WRITTEN.get(run.input);
116
+ if (known !== undefined) {
117
+ return known;
118
+ }
97
119
  const { stores } = run.input;
98
- const { matrix } = run.input.swept;
120
+ const { matrix, sweep } = run.input.swept;
99
121
  const uses = new Map();
100
122
  for (const reference of stores.references) {
101
123
  let held = uses.get(reference.to);
@@ -124,22 +146,49 @@ function writeOnlySymbols(run) {
124
146
  namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
125
147
  }
126
148
  }
127
- const found = [];
128
- for (const symbol of matrix.union.symbols) {
149
+ const subjects = matrix.union.symbols.flatMap((symbol) => {
129
150
  const held = uses.get(symbol.id);
130
- if (held === undefined ||
151
+ return held === undefined ||
131
152
  held.writes.length === 0 ||
132
153
  held.reads > 0 ||
133
154
  !writable(run, symbol) ||
134
155
  stores.exempt.has(symbol.id) ||
135
- run.candidates.has(symbol.id)) {
156
+ run.candidates.has(symbol.id)
157
+ ? []
158
+ : [{ symbol, held }];
159
+ });
160
+ const subjectIds = new Set(subjects.map((subject) => subject.symbol.id));
161
+ const classOf = reachabilityClasses(run.input);
162
+ const named = new Map();
163
+ const falls = new Set();
164
+ for (const symbol of matrix.union.symbols) {
165
+ const chains = sweep.carried.get(symbol.id);
166
+ if (chains === undefined ||
167
+ subjectIds.has(symbol.id) ||
168
+ !FALLING.has(symbol.kind) ||
169
+ classOf(symbol.id) !== "certain") {
136
170
  continue;
137
171
  }
138
- found.push(findingAbout(symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, namedBy, symbol, held), {
139
- writePositions: held.writes,
140
- }));
172
+ const innermost = chains.map((chain) => chain.find((id) => subjectIds.has(id)));
173
+ if (innermost.includes(undefined)) {
174
+ continue;
175
+ }
176
+ falls.add(symbol.id);
177
+ for (const subject of new Set(innermost)) {
178
+ const name = `${WORDS[symbol.kind]} ${symbol.name}`;
179
+ named.set(subject ?? "", [...(named.get(subject ?? "") ?? []), name]);
180
+ }
141
181
  }
142
- return found;
182
+ const written = { subjects, namedBy, named, falls };
183
+ WRITTEN.set(run.input, written);
184
+ return written;
185
+ }
186
+ /** `DS1301`: each subject, named with every position written and what falls with it. */
187
+ function writeOnlySymbols(run) {
188
+ const written = writtenOf(run);
189
+ return written.subjects.map((subject) => findingAbout(subject.symbol, CODE.writeOnlySymbol, writeOnlyMessage(run, written, subject), {
190
+ writePositions: subject.held.writes,
191
+ }));
143
192
  }
144
193
  /**
145
194
  * The candidates no reference of any file names whose container is not itself dead, of
@@ -184,7 +233,14 @@ function unusedTypeParameters(run) {
184
233
  * supersedes narrowing it.
185
234
  */
186
235
  export function writeOnlyDeclarations(input) {
187
- return new Set(writeOnlySymbols(indexed(input)).map((finding) => declarationKey(finding.symbol.ref, finding.position.path)));
236
+ return new Set(writtenOf(indexed(input)).subjects.map(({ symbol }) => declarationKey(symbol.ref, symbol.position.path)));
237
+ }
238
+ /**
239
+ * The declarations that fall with a write-only subject, whatever the severity: a finding
240
+ * about one would name code its subject's deletion already deletes.
241
+ */
242
+ export function fallingWithWrites(input) {
243
+ return writtenOf(indexed(input)).falls;
188
244
  }
189
245
  /**
190
246
  * The findings of the reads-and-writes kinds, `DS1301`, `DS1005` and `DS1006`, in that
@@ -1,5 +1,5 @@
1
1
  import { CODE } from "../contract/contract.generated.js";
2
- import { testsUnseenAPI } from "./completion.js";
2
+ import { WORDS } from "../inventory.js";
3
3
  import { unreachableExports } from "./visibility-narrowing.js";
4
4
  /** The kinds of declaration that are members of a class, an interface, a type or an enum. */
5
5
  const MEMBERS = new Set([
@@ -19,24 +19,6 @@ const NOT_REPORTED_HERE = new Set([
19
19
  "interface-method",
20
20
  "type-parameter",
21
21
  ]);
22
- /** The reader's word for each kind of declaration a finding of this emitter names. */
23
- const WORDS = {
24
- file: "file",
25
- function: "function",
26
- class: "class",
27
- interface: "interface",
28
- type: "type alias",
29
- enum: "enum",
30
- namespace: "namespace",
31
- variable: "variable",
32
- "export-alias": "re-export",
33
- method: "method",
34
- "class-member": "class member",
35
- "interface-method": "interface method",
36
- "type-member": "type member",
37
- "enum-member": "enum member",
38
- "type-parameter": "type parameter",
39
- };
40
22
  /** What the test-of-dead-code kind says, which states the rule it applies. */
41
23
  const TEST_OF_DEAD_CODE = "every target symbol this test references is reported dead";
42
24
  /**
@@ -50,7 +32,7 @@ const TEST_OF_DEAD_CODE = "every target symbol this test references is reported
50
32
  function codeOf(judged) {
51
33
  const { candidate, symbol } = judged;
52
34
  if (candidate.testOfDeadCode) {
53
- return judged.testsUnseenAPI ? undefined : CODE.testOfDeadCode;
35
+ return CODE.testOfDeadCode;
54
36
  }
55
37
  if ((judged.test && !candidate.unreferencedTest) || NOT_REPORTED_HERE.has(symbol.kind)) {
56
38
  return undefined;
@@ -201,7 +183,6 @@ function emitterFindings(input, fallen) {
201
183
  const union = input.swept.matrix.union;
202
184
  const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
203
185
  const unreachable = unreachableExports(input);
204
- const unseenAPI = testsUnseenAPI(input);
205
186
  let brands;
206
187
  const found = [];
207
188
  for (const candidate of input.swept.sweep.candidates) {
@@ -220,7 +201,6 @@ function emitterFindings(input, fallen) {
220
201
  test: union.test[at] === true,
221
202
  deprecated: input.deprecated.has(symbol.id),
222
203
  unreachable: unreachable.has(symbol.id),
223
- testsUnseenAPI: candidate.testOfDeadCode && unseenAPI(candidate.id),
224
204
  };
225
205
  const code = codeOf(judged);
226
206
  if (code === undefined ||
@@ -1,7 +1,9 @@
1
1
  import { CODE } from "../contract/contract.generated.js";
2
- import { OUTSIDE } from "../graph.js";
2
+ import { OUTSIDE } from "../liveness/graph.js";
3
+ import { WORDS } from "../inventory.js";
3
4
  import { declarationKey } from "../ref.js";
4
- import { runnerCalls } from "../sweep.js";
5
+ import { runnerCalls } from "../liveness/sweep.js";
6
+ import { exposedTypes, exposes } from "../exposure.js";
5
7
  import { writeOnlyDeclarations } from "./reads-and-writes.js";
6
8
  /**
7
9
  * The root kinds by which a consumer importing the package reaches a declaration. A
@@ -9,17 +11,6 @@ import { writeOnlyDeclarations } from "./reads-and-writes.js";
9
11
  * from outside the reference graph: a runtime, a tool, a framework or the configuration.
10
12
  */
11
13
  const PUBLISHING = new Set(["manifest-entry", "published-api"]);
12
- /** The reader's word for each kind of declaration this emitter reports. */
13
- const WORDS = {
14
- function: "function",
15
- class: "class",
16
- interface: "interface",
17
- type: "type alias",
18
- enum: "enum",
19
- namespace: "namespace",
20
- variable: "variable",
21
- "export-alias": "re-export",
22
- };
23
14
  function publishes(kinds) {
24
15
  return [...(kinds ?? [])].some((kind) => PUBLISHING.has(kind));
25
16
  }
@@ -102,9 +93,6 @@ function topLevelExport(facts, at, symbol) {
102
93
  const parent = facts.graph.parent[at] ?? OUTSIDE;
103
94
  return symbol.exported && parent !== OUTSIDE && facts.graph.symbols[parent]?.kind === "file";
104
95
  }
105
- function wordOf(symbol) {
106
- return WORDS[symbol.kind] ?? symbol.kind;
107
- }
108
96
  /** The visibility each narrowing code names, which only the narrowing codes carry. */
109
97
  const NARROWER = {
110
98
  [CODE.redundantExportKeyword]: "file",
@@ -145,7 +133,7 @@ function narrowing(facts, at, symbol, world) {
145
133
  (facts.named[at] ?? 0) === 0) {
146
134
  return undefined;
147
135
  }
148
- const word = wordOf(symbol);
136
+ const word = WORDS[symbol.kind];
149
137
  switch (facts.reach[at]) {
150
138
  case "file":
151
139
  if (facts.published[file] === true && !closed) {
@@ -153,7 +141,7 @@ function narrowing(facts, at, symbol, world) {
153
141
  }
154
142
  return findingOf(symbol, CODE.redundantExportKeyword, `exported ${word} is referenced only inside the file that declares it`);
155
143
  case "package":
156
- if (!closed || !publishes(facts.rootedBy[at])) {
144
+ if (!closed || !publishes(facts.rootedBy[at]) || exposes(world.exposed, symbol)) {
157
145
  return undefined;
158
146
  }
159
147
  return findingOf(symbol, CODE.unnecessaryExport, `exported ${word} is referenced only inside the package that declares it`);
@@ -181,7 +169,7 @@ function unreachable(facts, at, symbol, candidate, input) {
181
169
  const found = candidate.relation === "reachability"
182
170
  ? "is referenced only from declarations that are themselves dead"
183
171
  : "has no reference in the target";
184
- return findingOf(symbol, CODE.unreachableExport, `exported ${wordOf(symbol)} ${found}, and nothing outside can import the file that declares it`);
172
+ return findingOf(symbol, CODE.unreachableExport, `exported ${WORDS[symbol.kind]} ${found}, and nothing outside can import the file that declares it`);
185
173
  }
186
174
  /**
187
175
  * The visibility-narrowing kinds over one run: `DS1501`, `DS1101` and `DS1503`. A finding
@@ -194,7 +182,13 @@ export function narrowings(input) {
194
182
  const facts = factsOf(input.swept.matrix.union);
195
183
  const { declared, loaded } = input.boundary.consumers;
196
184
  const closed = input.config.consumers.complete && declared.every((id) => loaded.includes(id));
197
- const world = { closed, writeOnly: writeOnlyDeclarations(input) };
185
+ const world = {
186
+ closed,
187
+ writeOnly: writeOnlyDeclarations(input),
188
+ exposed: closed
189
+ ? exposedTypes(facts.graph, facts.reach.map((reach) => reach === "outside"), input.boundary.carriers)
190
+ : new Set(),
191
+ };
198
192
  const candidates = new Map(input.swept.sweep.candidates.map((candidate) => [candidate.id, candidate]));
199
193
  const generated = input.config.analysis.generated_files === "include" ? undefined : input.files.generated;
200
194
  const findings = [];
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The framework-lifecycle class: a member a `ts.lifecycle_contracts` entry lists is held
3
- * back on each class that is the entry's component, being a class a decorator or a call
4
- * the entry's `components` name is attached to or passes, or one extending, directly or
5
- * through a chain, a class its `bases` name. A component's members are its type's, so a
6
- * member a base class declares answers for every subclass.
2
+ * The framework-lifecycle class: a member a lifecycle contract lists is held back on each
3
+ * class that is the contract's component, being a class a decorator or a call the
4
+ * contract's `components` name is attached to or passes, or one extending, directly or
5
+ * through a chain, a class its `bases` name. The contracts are the platform's custom
6
+ * elements and each `ts.lifecycle_contracts` entry. A component's members are its type's,
7
+ * so a member a base class declares answers for every subclass.
7
8
  */
8
9
  import { isClassDeclaration, isClassExpression, isHeritageClause, SyntaxKind, } from "@typescript/native/ast";
9
10
  import { aliasChains } from "./alias-chain.js";
@@ -13,6 +14,29 @@ import { entriesAt, names, namesADeclaration, resolveEntries, spelledEntry, } fr
13
14
  import { nodeKey } from "./inventory.js";
14
15
  import { renderPosition } from "./position.js";
15
16
  import { must } from "./query.js";
17
+ /**
18
+ * The platform's own contract (references-ts.38): `CustomElementRegistry.define` reads
19
+ * these members off the class it registers, its constructor's and its prototype's
20
+ * (https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define),
21
+ * and calls each callback as a custom element reaction.
22
+ */
23
+ const CUSTOM_ELEMENTS = {
24
+ components: [{ global: "CustomElementRegistry.define" }],
25
+ members: [
26
+ "observedAttributes:static",
27
+ "disabledFeatures:static",
28
+ "formAssociated:static",
29
+ "connectedCallback",
30
+ "disconnectedCallback",
31
+ "connectedMoveCallback",
32
+ "adoptedCallback",
33
+ "attributeChangedCallback",
34
+ "formAssociatedCallback",
35
+ "formResetCallback",
36
+ "formDisabledCallback",
37
+ "formStateRestoreCallback",
38
+ ],
39
+ };
16
40
  /** Every contract's components and bases, resolved in one pass over the entries. */
17
41
  function resolveContracts(project, held, contracts) {
18
42
  const entries = contracts.flatMap((contract) => [
@@ -155,16 +179,17 @@ function byBases(project, held, chains, contracts) {
155
179
  }
156
180
  /**
157
181
  * The framework-lifecycle detector: each member a contract lists, on each class that
158
- * is a component of that contract's framework, recorded where the class was found to
159
- * be one.
182
+ * is a component of that contract's framework, recorded once per place where the class
183
+ * was found to be one.
160
184
  */
161
185
  export function frameworkLifecycle(input) {
162
186
  const { project, held, targetRoot } = input;
163
- if (input.ts.lifecycle_contracts.length === 0) {
187
+ const declared = [CUSTOM_ELEMENTS, ...input.ts.lifecycle_contracts];
188
+ const contracts = resolveContracts(project, held, declared);
189
+ if (contracts.every((one) => one.components.length === 0 && one.bases.length === 0)) {
164
190
  return [];
165
191
  }
166
192
  const chains = aliasChains(project, held);
167
- const contracts = resolveContracts(project, held, input.ts.lifecycle_contracts);
168
193
  const components = [
169
194
  ...byComponents(project, held, chains, contracts),
170
195
  ...byBases(project, held, chains, contracts),
@@ -173,22 +198,24 @@ export function frameworkLifecycle(input) {
173
198
  return [];
174
199
  }
175
200
  const ids = new Set(components.map((one) => one.id));
176
- const sides = input.ts.lifecycle_contracts.some((contract) => contract.members.some((name) => name.endsWith(":static")))
201
+ const sides = declared.some((contract) => contract.members.some((name) => name.endsWith(":static")))
177
202
  ? ["instance", "static"]
178
203
  : ["instance"];
179
204
  const members = sides.map((side) => classMembers(project, held, ids, side));
180
205
  const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
181
- const found = [];
206
+ const found = new Map();
182
207
  for (const component of components) {
183
208
  const listed = new Set(component.contract.members);
184
209
  const site = renderPosition(component.site.getSourceFile(), targetRoot, component.site.getStart());
185
210
  for (const member of members.flatMap((side) => side.get(component.id) ?? [])) {
186
211
  const symbol = byId.get(member.id);
187
212
  const spelled = symbol === undefined ? undefined : memberComponent(symbol, byId);
188
- if (spelled !== undefined && listed.has(spelled)) {
189
- found.push({ id: member.id, detail: component.detail, site });
213
+ // A configured contract may name the platform's registry too; one record answers both.
214
+ const key = `${member.id}\0${component.detail}\0${site.path}:${String(site.line)}:${String(site.column)}`;
215
+ if (spelled !== undefined && listed.has(spelled) && !found.has(key)) {
216
+ found.set(key, { id: member.id, detail: component.detail, site });
190
217
  }
191
218
  }
192
219
  }
193
- return found;
220
+ return [...found.values()];
194
221
  }