@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
@@ -1,11 +1,11 @@
1
1
  import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.js";
2
- import { runAnalysis } from "../analysis.js";
3
- import { listing } from "../components.js";
4
- import { OUTSIDE, swept as judged } from "../graph.js";
5
- import { referenceKey } from "../matrix.js";
2
+ import { productionMode, runAnalysis } from "../analysis.js";
3
+ import { listing } from "../liveness/components.js";
4
+ import { swept as judged } from "../liveness/graph.js";
6
5
  import { partialNotes } from "../partial.js";
7
6
  import { resolvePath } from "../paths.js";
8
7
  import { positionKey } from "../position.js";
8
+ import { madeByTestCode } from "../tests/file-class.js";
9
9
  import { resolve } from "../resolve.js";
10
10
  import {} from "./verb.js";
11
11
  import { UsageError } from "../usage.js";
@@ -79,150 +79,94 @@ function subjectOf(symbols, named) {
79
79
  .sort(bytewise);
80
80
  return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
81
81
  }
82
- /** Every reference of the run once, a reference several configurations make counted once. */
83
- function referencesOf(configured) {
84
- const seen = new Set();
85
- const held = [];
86
- for (const reference of configured.flatMap((one) => one.references)) {
87
- const key = referenceKey(reference);
88
- if (!seen.has(key)) {
89
- seen.add(key);
90
- held.push(reference);
91
- }
92
- }
93
- return held;
94
- }
95
- /** Every root of the run once per reason, in the order the projects name them. */
96
- function rootsOf(configured) {
97
- const seen = new Set();
98
- return configured
99
- .flatMap((one) => one.roots)
100
- .filter((root) => {
101
- const key = [root.id, root.kind, root.source].join("\u0000");
102
- if (seen.has(key)) {
103
- return false;
104
- }
105
- seen.add(key);
106
- return true;
107
- });
108
- }
109
- /**
110
- * The shortest path from a root or a loaded consumer to one declaration, walked breadth first
111
- * from every root at once in the order the run names them, a consumer's reference being the
112
- * first hop of a path of its own, and following the references the sweep follows: a
113
- * reference that reads a module reaches what the module exports, and an evaluation or a root
114
- * reaches the module alone. Under `production` no reference a test file made is followed,
115
- * which is the set the report's sweep counts.
116
- */
117
- function shortestPath(analysis, to, production) {
118
- const graph = analysis.swept.matrix.union;
119
- const out = new Map();
120
- const consumed = [];
121
- for (const reference of referencesOf(analysis.configured)) {
122
- if (production && reference.test) {
123
- continue;
124
- }
125
- const { consumer } = reference;
126
- if (consumer !== undefined) {
127
- consumed.push({ ...reference, consumer });
128
- continue;
129
- }
130
- const held = out.get(reference.from);
131
- if (held === undefined) {
132
- out.set(reference.from, [reference]);
133
- }
134
- else {
135
- held.push(reference);
136
- }
137
- }
138
- const steps = [];
139
- const seen = new Set();
140
- const expanded = new Set();
141
- const enter = (step) => {
142
- if (!seen.has(step.at)) {
143
- seen.add(step.at);
144
- steps.push(step);
145
- }
146
- };
147
- const follow = (reference, previous, origin) => {
148
- enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
149
- const target = graph.at(reference.to);
150
- if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
151
- return;
152
- }
153
- expanded.add(reference.to);
154
- for (const exported of graph.exportsOf[target] ?? []) {
155
- const id = graph.symbols[exported]?.id;
156
- if (id !== undefined) {
157
- enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
158
- }
159
- }
160
- };
161
- for (const root of rootsOf(analysis.configured)) {
162
- enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
163
- }
164
- // A consumer's reference is one hop long, so it joins the walk where the roots' first
165
- // hops do.
166
- const firstHops = steps.length;
167
- for (let head = 0;; head += 1) {
168
- if (head === firstHops) {
169
- for (const reference of consumed) {
170
- follow(reference, -1, { k: "consumer", consumer: reference.consumer });
82
+ /** Where a path starts, as a line names it. */
83
+ function originText(origin, graph) {
84
+ const id = (at) => graph.symbols[at]?.id ?? "";
85
+ switch (origin.k) {
86
+ case "root":
87
+ return `root ${origin.rooted.kind} ${id(origin.rooted.at)}`;
88
+ case "consumed": {
89
+ const reference = graph.references[origin.edge.reference];
90
+ if (reference?.consumer !== undefined) {
91
+ return `consumer ${reference.consumer}`;
171
92
  }
93
+ return reference?.unheld === true
94
+ ? `test file no project holds ${reference.from.replace(/#$/u, "")}`
95
+ : `declaration no configuration judges ${reference?.from ?? ""}`;
172
96
  }
173
- const step = steps[head];
174
- if (step === undefined) {
175
- break;
176
- }
177
- if (step.at === to) {
178
- const path = [];
179
- for (let at = step; at?.hop !== undefined; at = steps[at.previous]) {
180
- path.unshift(at.hop);
181
- }
182
- return { origin: step.origin, path };
183
- }
184
- for (const reference of out.get(step.at) ?? []) {
185
- follow(reference, head, step.origin);
186
- }
97
+ case "exemption":
98
+ return `exemption ${origin.record.class} ${positionKey(origin.record.site)}`;
99
+ case "mark":
100
+ return `suppression of ${id(origin.at)}`;
101
+ case "unanswered":
102
+ return `unanswered question about ${id(origin.at)}`;
187
103
  }
188
- return undefined;
189
104
  }
190
105
  /** What made a reference: a declaration of the target, or a loaded consumer. */
191
106
  function madeBy(reference) {
192
107
  return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
193
108
  }
194
- function referenceLine(reference, tail) {
195
- return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${reference.test ? "\tfrom a test file" : ""}\n`;
109
+ /**
110
+ * One reference of an answer, marked where test code made it, a test-support file's included,
111
+ * and naming the configurations that make it where they are not every one of the run.
112
+ */
113
+ function referenceLine(reference, tail, support, configurations) {
114
+ const partial = configurations.making.length < configurations.run.length;
115
+ return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${madeByTestCode(reference, support) ? "\tfrom a test file" : ""}${partial ? `\tmade in ${configurations.making.join(" ")}` : ""}\n`;
196
116
  }
197
117
  /** Why a declaration no finding names and no exemption held back is live, or how it fell. */
198
118
  function liveness(analysis, subject, cascade) {
199
- const { sweep } = analysis.swept;
119
+ const { matrix, sweep } = analysis.swept;
120
+ const graph = matrix.union;
200
121
  const candidate = sweep.candidates.find((one) => one.id === subject.id);
201
122
  if (candidate === undefined && !judged(subject.kind)) {
202
123
  return `answer: unjudged\n unjudged: neither liveness relation judges a ${subject.kind}\n`;
203
124
  }
204
125
  let text = `answer: ${candidate === undefined ? "live" : "dead"}\n`;
205
126
  text += ` live under: ${listed(sweep.liveUnder.get(subject.id) ?? [])}\n`;
206
- for (const root of rootsOf(analysis.configured).filter((one) => one.id === subject.id)) {
207
- text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
127
+ const at = graph.at(subject.id);
128
+ const seen = new Set();
129
+ for (const root of graph.rooted.filter((one) => one.at === at)) {
130
+ const key = `${root.kind}\u0000${root.source}`;
131
+ if (!seen.has(key)) {
132
+ seen.add(key);
133
+ text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
134
+ }
208
135
  }
209
- const found = shortestPath(analysis, subject.id, true) ?? shortestPath(analysis, subject.id, false);
210
- if (found !== undefined && found.path.length > 0) {
211
- const { origin } = found;
212
- text += ` reached from: ${origin.k === "root" ? `root ${origin.root.kind} ${origin.root.id}` : `consumer ${origin.consumer}`}\n`;
213
- for (const hop of found.path) {
214
- text += referenceLine(hop.reference, `${madeBy(hop.reference)} -> ${hop.reference.to}`);
136
+ const support = new Set(analysis.configured.flatMap((one) => one.supportFiles ?? []));
137
+ const line = (edge, tail) => {
138
+ const reference = graph.references[edge.reference];
139
+ return reference === undefined
140
+ ? ""
141
+ : referenceLine(reference, tail(reference), support, {
142
+ making: graph.configurations[edge.reference] ?? [],
143
+ run: matrix.configurations,
144
+ });
145
+ };
146
+ const found = sweep.path(subject.id);
147
+ if (found !== undefined && found.hops.length > 0) {
148
+ text += ` reached from: ${originText(found.origin, graph)}\n`;
149
+ for (const hop of found.hops) {
150
+ text += line(hop.edge, (reference) => `${madeBy(reference)} -> ${reference.to}`);
215
151
  if (hop.k === "export") {
216
- text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
152
+ const reference = graph.references[hop.edge.reference];
153
+ text += ` export: ${reference?.to ?? ""} -> ${graph.symbols[hop.to]?.id ?? ""}\n`;
217
154
  }
218
155
  }
219
156
  }
220
157
  else if (found === undefined) {
221
- const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
222
- text += " unreachable: no path of references reaches it from a root or a loaded consumer\n";
158
+ const references = graph.references.flatMap((one, place) => one.to === subject.id ? [place] : []);
159
+ text +=
160
+ " unreachable: no path of references reaches it from a root, a loaded consumer or a test file no project holds\n";
223
161
  text += ` references: ${String(references.length)}\n`;
224
- for (const reference of references) {
225
- text += referenceLine(reference, `from ${madeBy(reference)}`);
162
+ for (const place of references) {
163
+ const reference = graph.references[place];
164
+ if (reference !== undefined) {
165
+ text += referenceLine(reference, `from ${madeBy(reference)}`, support, {
166
+ making: graph.configurations[place] ?? [],
167
+ run: matrix.configurations,
168
+ });
169
+ }
226
170
  }
227
171
  }
228
172
  if (candidate !== undefined) {
@@ -279,7 +223,7 @@ function explanation(analysis, subject, cascade) {
279
223
  text += `position: ${subject.id}\n`;
280
224
  text += `configurations: ${listed(analysis.swept.matrix.heldIn[at] ?? [])}\n`;
281
225
  const found = analysis.result.findings.find((one) => positionKey(one.position) === subject.id);
282
- const retained = analysis.swept.retained.filter((one) => one.id === subject.id);
226
+ const retained = analysis.swept.sweep.retained.filter((one) => one.id === subject.id);
283
227
  if (found !== undefined) {
284
228
  text += reported(found);
285
229
  }
@@ -289,7 +233,7 @@ function explanation(analysis, subject, cascade) {
289
233
  text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
290
234
  }
291
235
  }
292
- else if (analysis.swept.heldByUnanswered.includes(subject.id)) {
236
+ else if (analysis.swept.sweep.held.includes(subject.id)) {
293
237
  text +=
294
238
  "answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
295
239
  }
@@ -301,18 +245,17 @@ function explanation(analysis, subject, cascade) {
301
245
  /**
302
246
  * Explains one symbol from the analysis the report is built from: why a reported symbol is
303
247
  * reported, which exemption classes held a retained one back, that an unanswered question held
304
- * a held one, and for any other declaration the relations that hold it live and the shortest
305
- * path from a root or a loaded consumer, or the references it has where no path reaches it. A
306
- * request naming no one declaration exits with the usage code and names what partially
248
+ * a held one, and for any other declaration the relations that hold it live and the path the
249
+ * sweep's strongest tier reaching it follows, or the references it has where no path reaches
250
+ * it. A request naming no one declaration exits with the usage code and names what partially
307
251
  * matches it. Where the analysis was partial, the error stream says so.
308
252
  */
309
253
  export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
310
254
  const named = namedSymbol(option);
311
255
  const { config, provenance } = resolve(inputs());
312
256
  const scoped = scope();
313
- const analysis = runAnalysis(openClient(config), host, scoped, config, provenance, {
314
- production: true,
315
- });
257
+ const mode = productionMode(config);
258
+ const analysis = runAnalysis(openClient(config), host, scoped, config, provenance, mode);
316
259
  const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
317
260
  const notes = partialNotes({
318
261
  notBuilt: analysis.run.notBuilt,
@@ -1,5 +1,5 @@
1
1
  import { EXIT_CLEAN } from "../contract/contract.generated.js";
2
- import { runSweep } from "../analysis.js";
2
+ import { productionMode, runSweep } from "../analysis.js";
3
3
  import { retainedLines } from "../exempt.js";
4
4
  import { partialNotes } from "../partial.js";
5
5
  import { resolvePath } from "../paths.js";
@@ -18,9 +18,9 @@ export const printRetainedVerb = ({ out, err, host, inputs, scope, openClient })
18
18
  const scoped = scope();
19
19
  const swept = runSweep(openClient(config), host, scoped, config, {
20
20
  marked: [],
21
- mode: { production: true },
21
+ mode: productionMode(config),
22
22
  });
23
- for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
23
+ for (const line of retainedLines(swept.matrix.union.symbols, swept.sweep.retained)) {
24
24
  out.write(`${line}\n`);
25
25
  }
26
26
  for (const note of partialNotes({
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.16",
3
+ "version": "8.0.0-dev.18",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.16",
3
+ "version": "8.0.0-dev.18",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -48,7 +48,7 @@
48
48
  "prettier": "3.9.10",
49
49
  "typescript": "npm:@typescript/typescript6@6.0.2",
50
50
  "typescript-eslint": "8.72.1",
51
- "vitest": "5.0.5"
51
+ "vitest": "5.1.0"
52
52
  },
53
53
  "publishConfig": {
54
54
  "access": "public"
@@ -49,8 +49,6 @@ export interface AliasChains {
49
49
  * link is declared.
50
50
  */
51
51
  nextDeclared(symbol: TSSymbol): readonly ChainTarget[];
52
- /** How many steps the compiler was asked for: one per alias, whatever the chains it heads. */
53
- readonly steps: number;
54
52
  }
55
53
 
56
54
  /**
@@ -65,7 +63,6 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
65
63
  const ownFiles = project.ownPaths();
66
64
  const steps = new Map<number, Answer<TSSymbol | undefined>>();
67
65
  const chains = new Map<number, readonly ChainTarget[]>();
68
- let asked = 0;
69
66
 
70
67
  const declarationsOf = (symbol: TSSymbol): string[] => {
71
68
  const ids: string[] = [];
@@ -102,7 +99,6 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
102
99
  return steps.get(symbol.id);
103
100
  }
104
101
  const next = project.aliasStepOf(symbol);
105
- asked += 1;
106
102
  steps.set(symbol.id, next);
107
103
  return next;
108
104
  };
@@ -197,8 +193,5 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
197
193
  declarationsOf,
198
194
  chainOf,
199
195
  nextDeclared,
200
- get steps() {
201
- return asked;
202
- },
203
196
  };
204
197
  }
package/src/amd.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * AMD's module forms (references-ts.37), read from the syntax because the binder declares
3
+ * none of them: `define([<ids>], factory)`, `define("<name>", [<ids>], factory)` and
4
+ * `require([<ids>], callback)` (https://github.com/amdjs/amdjs-api/blob/master/AMD.md).
5
+ */
6
+
7
+ import {
8
+ isArrayLiteralExpression,
9
+ isArrowFunction,
10
+ isFunctionExpression,
11
+ isCallExpression,
12
+ isIdentifier,
13
+ isNoSubstitutionTemplateLiteral,
14
+ isStringLiteral,
15
+ type Identifier,
16
+ type Node,
17
+ type StringLiteralLikeNode,
18
+ } from "@typescript/native/ast";
19
+ import { hasURLScheme, isAbsolutePath, resolvePath, startsRelativePath } from "./paths.ts";
20
+ import { parenthesesRemoved } from "./unwrap.ts";
21
+
22
+ /** The platform function an AMD call is written as. */
23
+ type AmdPlatform = "define" | "require";
24
+
25
+ /** One AMD call's dependency list. */
26
+ export interface AmdLoad {
27
+ /** The name the call is written with; {@link callsPlatform} owns whether it is AMD's. */
28
+ readonly callee: Identifier;
29
+ /** Each dependency id written as literal text, AMD's own `require`, `exports` and `module` left out. */
30
+ readonly ids: readonly AmdDependency[];
31
+ /** Whether the list, or an element of it, is computed, so the call may load any module. */
32
+ readonly computed: boolean;
33
+ }
34
+
35
+ /** One dependency id of an AMD call. */
36
+ interface AmdDependency {
37
+ readonly id: StringLiteralLikeNode;
38
+ /**
39
+ * Whether the factory may receive the module's value: it declares a parameter at the id's
40
+ * place, a rest parameter included, or it is written as anything but a function.
41
+ */
42
+ readonly received: boolean;
43
+ }
44
+
45
+ /** The ids AMD binds to the factory's CommonJS parameters, which load no module. */
46
+ const FREE_VARIABLES: ReadonlySet<string> = new Set(["require", "exports", "module"]);
47
+
48
+ /** The platform function `callee` is written as. */
49
+ export function amdPlatform(callee: Identifier): AmdPlatform {
50
+ return callee.text === "define" ? "define" : "require";
51
+ }
52
+
53
+ /**
54
+ * The dependency list one call writes, where it is a call of a bare `define` or `require` in
55
+ * one of AMD's forms. A `define` with no list, of a factory or a value alone, loads nothing;
56
+ * a `require` whose first argument is no array literal is CommonJS's.
57
+ */
58
+ export function amdLoadOf(node: Node): AmdLoad | undefined {
59
+ if (!isCallExpression(node)) {
60
+ return undefined;
61
+ }
62
+ const callee = parenthesesRemoved(node.expression);
63
+ if (!isIdentifier(callee) || (callee.text !== "define" && callee.text !== "require")) {
64
+ return undefined;
65
+ }
66
+ const written = node.arguments.map((one) => parenthesesRemoved(one));
67
+ const [first] = written;
68
+ if (first === undefined) {
69
+ return undefined;
70
+ }
71
+ const define = callee.text === "define";
72
+ const named = define && isLiteralText(first);
73
+ const list = written[named ? 1 : 0];
74
+ if (list !== undefined && isArrayLiteralExpression(list)) {
75
+ const elements = list.elements.map((one) => parenthesesRemoved(one));
76
+ const factory = written[named ? 2 : 1];
77
+ const ids = elements.flatMap((one, at) =>
78
+ isLiteralText(one) && !FREE_VARIABLES.has(one.text)
79
+ ? [{ id: one, received: receives(factory, at) }]
80
+ : [],
81
+ );
82
+ return { callee, ids, computed: elements.some((one) => !isLiteralText(one)) };
83
+ }
84
+ if (!define) {
85
+ return undefined;
86
+ }
87
+ // A factory after the list's place makes what stands there a list, here a computed one.
88
+ return { callee, ids: [], computed: written.length > (named ? 2 : 1) };
89
+ }
90
+
91
+ function isLiteralText(node: Node): node is StringLiteralLikeNode {
92
+ return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node);
93
+ }
94
+
95
+ /** Whether a factory may receive the dependency at place `at` of its list. */
96
+ function receives(factory: Node | undefined, at: number): boolean {
97
+ if (factory === undefined) {
98
+ return false;
99
+ }
100
+ if (!isFunctionExpression(factory) && !isArrowFunction(factory)) {
101
+ return true;
102
+ }
103
+ const { parameters } = factory;
104
+ return at < parameters.length || parameters.some((one) => one.dotDotDotToken !== undefined);
105
+ }
106
+
107
+ /**
108
+ * The path a dependency id names against the calling file's directory: a relative module id,
109
+ * `./` or `../` with no `.js` ending, with `.js` appended. RequireJS reads any other id
110
+ * through its configuration or as a URL against the page, which the analysis does not know.
111
+ */
112
+ export function amdPath(dir: string, id: string): string | undefined {
113
+ return startsRelativePath(id) && !id.endsWith(".js") ? resolvePath(dir, `${id}.js`) : undefined;
114
+ }
115
+
116
+ /**
117
+ * Whether one call may load a file the analysis cannot name: a computed list, or an id that
118
+ * RequireJS reads as a URL, one ending in `.js`, starting with `/` or carrying a scheme. Any
119
+ * other id {@link amdPath} reads no path from is a package's.
120
+ */
121
+ export function amdUnresolved(load: AmdLoad): boolean {
122
+ return (
123
+ load.computed ||
124
+ load.ids.some(
125
+ ({ id }) =>
126
+ amdPath("/", id.text) === undefined &&
127
+ (id.text.endsWith(".js") || isAbsolutePath(id.text) || hasURLScheme(id.text)),
128
+ )
129
+ );
130
+ }