@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.1

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 (153) hide show
  1. package/dist/bin/mapper-connection.js +7 -4
  2. package/dist/src/analysis.js +116 -16
  3. package/dist/src/augmentations.js +21 -8
  4. package/dist/src/automatic-types.js +189 -0
  5. package/dist/src/base-url.js +18 -0
  6. package/dist/src/component-files.js +96 -31
  7. package/dist/src/components.js +7 -6
  8. package/dist/src/config.js +1 -0
  9. package/dist/src/configuration-files.js +54 -15
  10. package/dist/src/configured-declarations.js +24 -2
  11. package/dist/src/conformance-record.js +2 -2
  12. package/dist/src/contextual-uses.js +26 -26
  13. package/dist/src/convention-rows.js +75 -0
  14. package/dist/src/conventions.js +101 -5
  15. package/dist/src/decorator.js +21 -18
  16. package/dist/src/dependencies.js +185 -30
  17. package/dist/src/destructuring.js +77 -2
  18. package/dist/src/discover.js +188 -85
  19. package/dist/src/edges.js +1 -1
  20. package/dist/src/entry-point-gaps.js +7 -10
  21. package/dist/src/entry-points.js +112 -11
  22. package/dist/src/findings/completion.js +116 -21
  23. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/intra-function.js +44 -13
  26. package/dist/src/findings/non-code-artifacts.js +66 -33
  27. package/dist/src/findings/unused-declarations.js +21 -1
  28. package/dist/src/findings/visibility-narrowing.js +22 -3
  29. package/dist/src/generated-file.js +11 -0
  30. package/dist/src/git-ignore.js +86 -0
  31. package/dist/src/graph.js +4 -1
  32. package/dist/src/import-bindings.js +97 -0
  33. package/dist/src/indexed-access.js +130 -28
  34. package/dist/src/interface-satisfaction.js +30 -12
  35. package/dist/src/intra-function-parts.js +313 -50
  36. package/dist/src/inventory.js +63 -8
  37. package/dist/src/json-document.js +5 -5
  38. package/dist/src/kinds.js +13 -13
  39. package/dist/src/matrix.js +18 -9
  40. package/dist/src/mdx-entries.js +197 -0
  41. package/dist/src/module-declarations.js +9 -13
  42. package/dist/src/namespace-objects.js +28 -18
  43. package/dist/src/overrides.js +45 -9
  44. package/dist/src/paths.js +9 -0
  45. package/dist/src/print.js +1 -0
  46. package/dist/src/ref.js +10 -0
  47. package/dist/src/references.js +29 -13
  48. package/dist/src/reflective-lookup.js +8 -6
  49. package/dist/src/report.js +4 -2
  50. package/dist/src/reporters/template.js +22 -13
  51. package/dist/src/resolve.js +2 -0
  52. package/dist/src/roots.js +110 -38
  53. package/dist/src/schema.js +1 -0
  54. package/dist/src/serialization-contract.js +97 -20
  55. package/dist/src/session.js +26 -1
  56. package/dist/src/source-files.js +39 -14
  57. package/dist/src/spelled-names.js +58 -0
  58. package/dist/src/suppress-file.js +12 -4
  59. package/dist/src/suppress-inline.js +21 -3
  60. package/dist/src/suppress.js +7 -5
  61. package/dist/src/sweep.js +27 -10
  62. package/dist/src/symbol-keys.js +103 -2
  63. package/dist/src/template-field.js +9 -6
  64. package/dist/src/test-support.js +121 -17
  65. package/dist/src/type-errors.js +116 -35
  66. package/dist/src/type-parameter-reads.js +172 -0
  67. package/dist/src/value-reach.js +17 -5
  68. package/dist/src/verbs/analyze.js +19 -1
  69. package/dist/src/verbs/explain.js +4 -2
  70. package/dist/src/version-range.js +10 -5
  71. package/dist/src/version.js +1 -1
  72. package/dist/src/workflow-steps.js +119 -22
  73. package/dist/src/workspace-programs.js +7 -3
  74. package/dist/src/workspace-resolution.js +72 -13
  75. package/dist/src/workspace.js +1 -1
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/analysis.ts +175 -16
  79. package/src/augmentations.ts +21 -10
  80. package/src/automatic-types.ts +246 -0
  81. package/src/base-url.ts +24 -0
  82. package/src/component-files.ts +116 -31
  83. package/src/components.ts +7 -6
  84. package/src/config.ts +3 -0
  85. package/src/configuration-files.ts +80 -18
  86. package/src/configured-declarations.ts +33 -4
  87. package/src/conformance-record.ts +2 -2
  88. package/src/contextual-uses.ts +27 -27
  89. package/src/convention-rows.ts +103 -0
  90. package/src/conventions.ts +121 -7
  91. package/src/decorator.ts +28 -18
  92. package/src/dependencies.ts +249 -43
  93. package/src/destructuring.ts +93 -2
  94. package/src/discover.ts +234 -86
  95. package/src/edges.ts +1 -1
  96. package/src/entry-point-gaps.ts +8 -11
  97. package/src/entry-points.ts +133 -11
  98. package/src/finding.ts +10 -0
  99. package/src/findings/completion.ts +147 -24
  100. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  101. package/src/findings/emitters.ts +1 -1
  102. package/src/findings/intra-function.ts +56 -14
  103. package/src/findings/non-code-artifacts.ts +81 -41
  104. package/src/findings/unused-declarations.ts +25 -1
  105. package/src/findings/visibility-narrowing.ts +32 -3
  106. package/src/generated-file.ts +16 -0
  107. package/src/git-ignore.ts +98 -0
  108. package/src/graph.ts +6 -0
  109. package/src/host.ts +5 -9
  110. package/src/import-bindings.ts +128 -0
  111. package/src/indexed-access.ts +152 -30
  112. package/src/interface-satisfaction.ts +33 -13
  113. package/src/intra-function-parts.ts +415 -64
  114. package/src/inventory.ts +83 -7
  115. package/src/json-document.ts +7 -7
  116. package/src/kinds.ts +13 -13
  117. package/src/matrix.ts +32 -14
  118. package/src/mdx-entries.ts +241 -0
  119. package/src/module-declarations.ts +11 -13
  120. package/src/namespace-objects.ts +34 -21
  121. package/src/overrides.ts +56 -9
  122. package/src/paths.ts +10 -0
  123. package/src/print.ts +1 -0
  124. package/src/ref.ts +12 -0
  125. package/src/references.ts +38 -17
  126. package/src/reflective-lookup.ts +9 -7
  127. package/src/report.ts +16 -3
  128. package/src/reporters/template.ts +21 -13
  129. package/src/resolve.ts +3 -0
  130. package/src/roots.ts +163 -45
  131. package/src/schema.ts +1 -0
  132. package/src/serialization-contract.ts +108 -17
  133. package/src/session.ts +29 -1
  134. package/src/source-files.ts +66 -16
  135. package/src/spelled-names.ts +102 -0
  136. package/src/suppress-file.ts +14 -3
  137. package/src/suppress-inline.ts +21 -2
  138. package/src/suppress.ts +9 -5
  139. package/src/sweep.ts +32 -11
  140. package/src/symbol-keys.ts +129 -2
  141. package/src/template-field.ts +9 -6
  142. package/src/test-support.ts +149 -16
  143. package/src/type-errors.ts +132 -41
  144. package/src/type-parameter-reads.ts +227 -0
  145. package/src/value-reach.ts +19 -6
  146. package/src/verbs/analyze.ts +28 -1
  147. package/src/verbs/explain.ts +5 -2
  148. package/src/version-range.ts +10 -5
  149. package/src/version.ts +1 -1
  150. package/src/workflow-steps.ts +127 -22
  151. package/src/workspace-programs.ts +7 -3
  152. package/src/workspace-resolution.ts +86 -14
  153. package/src/workspace.ts +1 -1
@@ -0,0 +1,24 @@
1
+ import { resolvePath } from "./paths.ts";
2
+ import type { Engine } from "./session.ts";
3
+
4
+ type CompilerOptions = ReturnType<Engine["parseConfigFile"]>["options"];
5
+
6
+ /**
7
+ * The `paths` one configuration's resolved options map under the compilers before version
8
+ * 7, each target absolute: a target is read against the effective `baseUrl`, and a specifier
9
+ * no other pattern maps is looked up below it. Undefined where no `baseUrl` is set, so the
10
+ * options' own `paths` stand. Version 7 reads no `baseUrl`:
11
+ * https://github.com/microsoft/TypeScript/issues/62207
12
+ */
13
+ export function baseUrlPaths(options: CompilerOptions): Record<string, string[]> | undefined {
14
+ const baseUrl: unknown = Reflect.get(options, "baseUrl");
15
+ if (typeof baseUrl !== "string") {
16
+ return undefined;
17
+ }
18
+ return Object.fromEntries(
19
+ Object.entries({ "*": ["*"], ...options.paths }).map(([key, targets]) => [
20
+ key,
21
+ targets.map((target) => resolvePath(baseUrl, target)),
22
+ ]),
23
+ );
24
+ }
@@ -188,18 +188,25 @@ function scriptTagAt(text: string, offset: number): boolean {
188
188
  );
189
189
  }
190
190
 
191
+ /** Where the braces opening at one offset end, and whether a `}` matched them. */
192
+ interface Braces {
193
+ /** Past the matching `}`, or where the scan stopped: a line-start `<script` or the end. */
194
+ readonly end: number;
195
+ readonly closed: boolean;
196
+ }
197
+
191
198
  /**
192
- * The offset past the `}` matching the `{` at `from`, quoted strings skipped, or -1 where
193
- * none matches before the end or before a `<script` start tag that begins a line, so a
194
- * stray brace in markup never hides a block.
199
+ * The braces opening at `from`, quoted strings skipped. The scan stops unclosed at the
200
+ * end, or before a `<script` start tag that begins a line, so a stray brace in markup
201
+ * never hides a block.
195
202
  */
196
- function pastBraces(text: string, from: number): number {
203
+ function scanBraces(text: string, from: number): Braces {
197
204
  let depth = 0;
198
205
  let at = from;
199
206
  while (at < text.length) {
200
207
  const code = text.charCodeAt(at);
201
208
  if (scriptTagAt(text, at)) {
202
- return -1;
209
+ return { end: at, closed: false };
203
210
  }
204
211
  if (code === 0x22 || code === 0x27 || code === 0x60) {
205
212
  at = pastQuoted(text, at);
@@ -210,12 +217,59 @@ function pastBraces(text: string, from: number): number {
210
217
  } else if (code === 0x7d) {
211
218
  depth -= 1;
212
219
  if (depth === 0) {
213
- return at + 1;
220
+ return { end: at + 1, closed: true };
214
221
  }
215
222
  }
216
223
  at += 1;
217
224
  }
218
- return -1;
225
+ return { end: text.length, closed: false };
226
+ }
227
+
228
+ /** The offset past the `}` matching the `{` at `from`, or -1 where {@link scanBraces} finds none. */
229
+ function pastBraces(text: string, from: number): number {
230
+ const braces = scanBraces(text, from);
231
+ return braces.closed ? braces.end : -1;
232
+ }
233
+
234
+ /** How one framework's parser reads the forms the frameworks disagree on. */
235
+ interface Dialect {
236
+ /** Whether `<!-->`, `<!--->` and `--!>` end a comment, the opening dashes counted. */
237
+ readonly shortComments: boolean;
238
+ /** Whether a start tag's unmatched `{` is a character of an attribute name or value. */
239
+ readonly braceIsText: boolean;
240
+ }
241
+
242
+ /**
243
+ * The dialect each framework's parser reads: Vue's tokenizer, Svelte's parser and Astro's
244
+ * tokenizer, the last for any other extension too. `.vue` also closes `--!>`, which Vue
245
+ * does not, so a block Vue drops there is still read.
246
+ * https://github.com/vuejs/core/blob/main/packages/compiler-core/src/tokenizer.ts
247
+ * https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/phases/1-parse/state/element.js
248
+ * https://github.com/withastro/compiler/blob/main/internal/token.go
249
+ */
250
+ function dialectOf(extension: string): Dialect {
251
+ switch (extension.toLowerCase()) {
252
+ case ".svelte":
253
+ return { shortComments: false, braceIsText: false };
254
+ case ".vue":
255
+ return { shortComments: true, braceIsText: true };
256
+ default:
257
+ return { shortComments: true, braceIsText: false };
258
+ }
259
+ }
260
+
261
+ /** The offset past the comment whose `<!--` is at `lt`, or the text's length where none ends it. */
262
+ function pastComment(text: string, lt: number, dialect: Dialect): number {
263
+ if (!dialect.shortComments) {
264
+ const close = text.indexOf("-->", lt + 4);
265
+ return close < 0 ? text.length : close + 3;
266
+ }
267
+ const dashes = text.indexOf("-->", lt + 2);
268
+ const bang = text.indexOf("--!>", lt + 2);
269
+ if (bang >= 0 && (dashes < 0 || bang < dashes)) {
270
+ return bang + 4;
271
+ }
272
+ return dashes < 0 ? text.length : dashes + 3;
219
273
  }
220
274
 
221
275
  interface Attribute {
@@ -234,17 +288,43 @@ interface StartTag {
234
288
  readonly attributes: readonly Attribute[];
235
289
  }
236
290
 
237
- /** The start tag whose `<` is at `from`, to its `>`. A quoted or braced value may hold `>`. */
238
- function readStartTag(text: string, from: number, nameStop: number): StartTag {
291
+ /**
292
+ * Where an unmatched `{` of a start tag ends its braced value: at the first `>`, or where
293
+ * the brace scan stopped where that comes first.
294
+ */
295
+ function unmatchedBraceEnd(text: string, from: number, braces: Braces): number {
296
+ const close = text.indexOf(">", from);
297
+ return close >= 0 && close < braces.end ? close : braces.end;
298
+ }
299
+
300
+ /**
301
+ * The start tag whose `<` is at `from`, to its `>`. A quoted or braced value may hold `>`.
302
+ * Past an unmatched `{`, the tag also ends before a `<script` start tag that begins a line.
303
+ */
304
+ function readStartTag(text: string, from: number, nameStop: number, dialect: Dialect): StartTag {
239
305
  const name = text.slice(from + 1, nameStop).toLowerCase();
240
306
  const attributes: Attribute[] = [];
241
307
  let at = nameStop;
242
308
  let selfClosing = false;
309
+ let unmatched = false;
310
+ /** The braces opening at `at`, and whether they make a braced value rather than text. */
311
+ const bracesAt = (): { readonly braces: Braces; readonly value: boolean } | undefined => {
312
+ if (text.charCodeAt(at) !== 0x7b) {
313
+ return undefined;
314
+ }
315
+ const braces = scanBraces(text, at);
316
+ return { braces, value: braces.closed || !dialect.braceIsText };
317
+ };
318
+ const bracedEnd = (braces: Braces): number =>
319
+ braces.closed ? braces.end : unmatchedBraceEnd(text, at, braces);
243
320
  while (at < text.length) {
244
321
  const code = text.charCodeAt(at);
245
322
  if (code === 0x3e) {
246
323
  return { name, end: at + 1, selfClosing, attributes };
247
324
  }
325
+ if (unmatched && scriptTagAt(text, at)) {
326
+ return { name, end: at, selfClosing, attributes };
327
+ }
248
328
  if (isSpace(code)) {
249
329
  at += 1;
250
330
  continue;
@@ -256,12 +336,13 @@ function readStartTag(text: string, from: number, nameStop: number): StartTag {
256
336
  }
257
337
  selfClosing = false;
258
338
  const start = at;
259
- if (code === 0x7b) {
260
- const past = pastBraces(text, at);
261
- at = past < 0 ? text.length : past;
339
+ const bare = bracesAt();
340
+ unmatched ||= bare?.braces.closed === false;
341
+ if (bare?.value === true) {
342
+ at = bracedEnd(bare.braces);
262
343
  attributes.push({
263
344
  name: "",
264
- value: text.slice(start + 1, at - 1),
345
+ value: text.slice(start + 1, bare.braces.closed ? at - 1 : at),
265
346
  start,
266
347
  end: at,
267
348
  valueStart: start + 1,
@@ -287,16 +368,18 @@ function readStartTag(text: string, from: number, nameStop: number): StartTag {
287
368
  at += 1;
288
369
  }
289
370
  const opening = text.charCodeAt(at);
371
+ const braced = bracesAt();
372
+ unmatched ||= braced?.braces.closed === false;
290
373
  if (opening === 0x22 || opening === 0x27) {
291
- const past = pastQuoted(text, at);
374
+ // An attribute value has no escapes: a backslash before its quote is text.
375
+ const close = text.indexOf(text.charAt(at), at + 1);
292
376
  valueStart = at + 1;
293
- value = text.slice(at + 1, Math.max(past - 1, at + 1));
294
- at = past;
295
- } else if (opening === 0x7b) {
296
- const past = pastBraces(text, at);
297
- const stop = past < 0 ? text.length : past;
377
+ value = text.slice(at + 1, close < 0 ? text.length : close);
378
+ at = close < 0 ? text.length : close + 1;
379
+ } else if (braced?.value === true) {
380
+ const stop = bracedEnd(braced.braces);
298
381
  valueStart = at + 1;
299
- value = text.slice(at + 1, Math.max(stop - 1, at + 1));
382
+ value = text.slice(at + 1, braced.braces.closed ? stop - 1 : stop);
300
383
  at = stop;
301
384
  } else {
302
385
  valueStart = at;
@@ -410,9 +493,11 @@ function scriptsInside(text: string, from: number, to: number): number[] {
410
493
  * The scan keeps a stack of open elements. A comment, a doctype or processing instruction and
411
494
  * an expression in braces are text, and an end tag closes back to the nearest open element of
412
495
  * its name. A raw-text element at the top level runs to its own end tag, and only a `script`
413
- * element whose start tag begins a line at the top level is a block.
496
+ * element whose start tag begins a line at the top level is a block. The file's extension
497
+ * names the framework whose reading of a comment and of a stray brace applies.
414
498
  */
415
- export function readComponent(text: string): ComponentFile {
499
+ export function readComponent(text: string, extension: string): ComponentFile {
500
+ const dialect = dialectOf(extension);
416
501
  const blocks: Block[] = [];
417
502
  const imports: SourceImport[] = [];
418
503
  const warnings: ComponentWarning[] = [];
@@ -442,8 +527,7 @@ export function readComponent(text: string): ComponentFile {
442
527
  }
443
528
  const lt = at;
444
529
  if (text.startsWith("<!--", lt)) {
445
- const close = text.indexOf("-->", lt + 4);
446
- at = close < 0 ? text.length : close + 3;
530
+ at = pastComment(text, lt, dialect);
447
531
  continue;
448
532
  }
449
533
  const next = text.charCodeAt(lt + 1);
@@ -467,7 +551,7 @@ export function readComponent(text: string): ComponentFile {
467
551
  at = lt + 1;
468
552
  continue;
469
553
  }
470
- const tag = readStartTag(text, lt, nameStop);
554
+ const tag = readStartTag(text, lt, nameStop, dialect);
471
555
  const rawText = open.length === 0 && RAW_TEXT.has(tag.name);
472
556
  for (const attribute of rawText ? [] : tag.attributes) {
473
557
  if (attribute.value !== "") {
@@ -542,9 +626,9 @@ export function readComponent(text: string): ComponentFile {
542
626
  }
543
627
 
544
628
  /**
545
- * Whether the module's text exports a default at its top level: an `export default`
546
- * or an export clause naming `default`, outside every comment, string, template and
547
- * regular expression, and outside every bracket.
629
+ * Whether the module's text exports a default at its top level: an `export default`, an
630
+ * `export * as default`, or an export clause naming `default`, outside every comment,
631
+ * string, template and regular expression, and outside every bracket.
548
632
  */
549
633
  export function exportsDefault(text: string): boolean {
550
634
  const words: string[] = [];
@@ -633,7 +717,8 @@ export function exportsDefault(text: string): boolean {
633
717
  const matched = word.exec(text);
634
718
  if (matched !== null) {
635
719
  const name = matched[0];
636
- if (depth === 0 && words.at(-1) === "export" && name === "default") {
720
+ const starAs = words.at(-3) === "export" && words.at(-2) === "*" && words.at(-1) === "as";
721
+ if (depth === 0 && (words.at(-1) === "export" || starAs) && name === "default") {
637
722
  return true;
638
723
  }
639
724
  if (clause && name === "default") {
@@ -662,8 +747,8 @@ export function exportsDefault(text: string): boolean {
662
747
  }
663
748
 
664
749
  /** The module one component file's text is read as, and the span map back to the file. */
665
- export function componentModule(text: string): ComponentModule {
666
- const { blocks, imports } = readComponent(text);
750
+ export function componentModule(text: string, fileExtension: string): ComponentModule {
751
+ const { blocks, imports } = readComponent(text, fileExtension);
667
752
  const out: string[] = [];
668
753
  let at = 0;
669
754
  let language: BlockLanguage | undefined;
package/src/components.ts CHANGED
@@ -208,12 +208,12 @@ function linesSpanned(symbols: readonly (InventorySymbol | undefined)[]): number
208
208
  }
209
209
 
210
210
  /**
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.
211
+ * The dead declarations by subgraph place (`at`, and `position` per graph declaration), a
212
+ * test file's only where it tests dead code or nothing the tests reach. `adjacent` holds each
213
+ * reference one makes to another, but an unreferenced test-file declaration's to production
214
+ * code, which decides nothing; an edge each way between a dead member and its dead container;
215
+ * and one back from each non-ambient production target of an admitted test, which puts the
216
+ * test in its subject's component. `references` holds the first alone.
217
217
  */
218
218
  function deadSubgraph(
219
219
  graph: Graph,
@@ -248,6 +248,7 @@ function deadSubgraph(
248
248
  if (
249
249
  testOfDeadCode[index] === true &&
250
250
  graph.test[edge.to] !== true &&
251
+ graph.ambient[edge.to] !== true &&
251
252
  graph.subject[edge.to] === true
252
253
  ) {
253
254
  adjacent[to]?.push(from);
package/src/config.ts CHANGED
@@ -175,6 +175,8 @@ export interface TSSection {
175
175
  readonly injectionRegistrations: readonly DeclarationEntry[];
176
176
  readonly lifecycleContracts: readonly LifecycleContract[];
177
177
  readonly serializers: readonly DeclarationEntry[];
178
+ /** Whether the maintainer declares the roots the analysis finds every root the target has. */
179
+ readonly rootsComplete: boolean;
178
180
  }
179
181
 
180
182
  /**
@@ -253,6 +255,7 @@ export function defaultConfig(): Config {
253
255
  injectionRegistrations: [],
254
256
  lifecycleContracts: [],
255
257
  serializers: [],
258
+ rootsComplete: false,
256
259
  },
257
260
  };
258
261
  }
@@ -8,10 +8,13 @@
8
8
  */
9
9
 
10
10
  import {
11
+ isCallExpression,
11
12
  isExportDeclaration,
13
+ isIdentifier,
12
14
  isImportDeclaration,
13
15
  isNoSubstitutionTemplateLiteral,
14
16
  isStringLiteral,
17
+ SyntaxKind,
15
18
  type Node,
16
19
  type SourceFile,
17
20
  } from "@typescript/native/ast";
@@ -110,7 +113,7 @@ interface ConfigurationDocument {
110
113
  export type ParseFile = (fileName: string, text: string) => SourceFile;
111
114
 
112
115
  /** One file no program holds that a configuration module outside the program leads to. */
113
- interface OutsideModule {
116
+ export interface OutsideModule {
114
117
  /** The file, parsed alone. */
115
118
  readonly file: SourceFile;
116
119
  /** The directories of the manifests of the configuration files the file was reached from. */
@@ -278,8 +281,15 @@ function readDocument(host: Host, path: string): unknown {
278
281
 
279
282
  /** The configuration files one project holds and sits beside. */
280
283
  interface ConfigurationFiles {
281
- /** Every own file that is a configuration module, with the form its name has. */
282
- readonly modules: readonly { readonly file: SourceFile; readonly form: string }[];
284
+ /**
285
+ * Every own file that is a configuration module, with the form its name has and the
286
+ * directory of the manifest whose dependencies its strings are read against.
287
+ */
288
+ readonly modules: readonly {
289
+ readonly file: SourceFile;
290
+ readonly form: string;
291
+ readonly manifestDir: string;
292
+ }[];
283
293
  /**
284
294
  * Every configuration module in a directory of the project that the program does not
285
295
  * hold, and every file such a file imports or re-exports by a relative specifier that
@@ -345,12 +355,13 @@ export function configurationFiles(
345
355
  return held;
346
356
  };
347
357
 
348
- const modules: { file: SourceFile; form: string }[] = [];
358
+ const modules: { file: SourceFile; form: string; manifestDir: string }[] = [];
349
359
  const dirs = new Set<string>([root]);
350
360
  for (const file of files.byPath.values()) {
351
361
  const form = configurationModuleForm(basename(file.fileName));
352
- if (form !== undefined && holdsManifest(dirnamePath(file.fileName))) {
353
- modules.push({ file, form });
362
+ const fileDir = dirnamePath(file.fileName);
363
+ if (form !== undefined && holdsManifest(fileDir)) {
364
+ modules.push({ file, form, manifestDir: fileDir });
354
365
  }
355
366
  for (
356
367
  let dir = dirnamePath(file.fileName);
@@ -383,7 +394,7 @@ export function configurationFiles(
383
394
  if (path.endsWith(".json")) {
384
395
  pending.push({ path, manifestDir: dir });
385
396
  } else if (held !== undefined) {
386
- modules.push({ file: held, form: "a script's configuration flag" });
397
+ modules.push({ file: held, form: "a script's configuration flag", manifestDir: dir });
387
398
  } else {
388
399
  named.push({ path, manifestDir: dir });
389
400
  }
@@ -431,19 +442,70 @@ export function configurationFiles(
431
442
  };
432
443
  }
433
444
 
434
- /** The relative specifiers one file's imports and re-exports name. */
435
- function relativeSpecifiers(file: SourceFile): readonly string[] {
436
- return file.statements.flatMap((statement) => {
437
- const specifier =
445
+ /** The literal module specifier one `require(...)` or `import(...)` call names, if any. */
446
+ function calledSpecifier(node: Node): Node | undefined {
447
+ if (!isCallExpression(node) || node.arguments.length !== 1) {
448
+ return undefined;
449
+ }
450
+ const callee = node.expression;
451
+ const loads =
452
+ callee.kind === SyntaxKind.ImportKeyword || (isIdentifier(callee) && callee.text === "require");
453
+ return loads ? node.arguments[0] : undefined;
454
+ }
455
+
456
+ /**
457
+ * The relative specifiers one file's imports and re-exports name, and those its
458
+ * `require(...)` and `import(...)` calls name by a literal.
459
+ */
460
+ export function relativeSpecifiers(file: SourceFile): readonly string[] {
461
+ const known = specifiersOf.get(file);
462
+ if (known !== undefined) {
463
+ return known;
464
+ }
465
+ const found: string[] = [];
466
+ const named = (specifier: Node | undefined): void => {
467
+ if (
468
+ specifier !== undefined &&
469
+ (isStringLiteral(specifier) || isNoSubstitutionTemplateLiteral(specifier)) &&
470
+ RELATIVE_PREFIXES.some((prefix) => specifier.text.startsWith(prefix))
471
+ ) {
472
+ found.push(specifier.text);
473
+ }
474
+ };
475
+ const visit = (node: Node): void => {
476
+ named(calledSpecifier(node));
477
+ node.forEachChild(visit);
478
+ };
479
+ for (const statement of file.statements) {
480
+ named(
438
481
  isImportDeclaration(statement) || isExportDeclaration(statement)
439
482
  ? statement.moduleSpecifier
440
- : undefined;
441
- return specifier !== undefined &&
442
- isStringLiteral(specifier) &&
443
- RELATIVE_PREFIXES.some((prefix) => specifier.text.startsWith(prefix))
444
- ? [specifier.text]
445
- : [];
446
- });
483
+ : undefined,
484
+ );
485
+ visit(statement);
486
+ }
487
+ specifiersOf.set(file, found);
488
+ return found;
489
+ }
490
+
491
+ /** What {@link relativeSpecifiers} found in each file, which every project of a run reads alike. */
492
+ const specifiersOf = new WeakMap<SourceFile, readonly string[]>();
493
+
494
+ /**
495
+ * The files no program holds that a chain from `seeds` reads: each seed, and each file
496
+ * below the target root one imports, re-exports or loads by a relative specifier that no
497
+ * program holds either, until no further file joins, each read against the manifest
498
+ * directory its seed names. A file that cannot be read is skipped.
499
+ */
500
+ export function outsideChain(
501
+ host: Host,
502
+ targetRoot: string,
503
+ files: SourceFiles,
504
+ parse: ParseFile,
505
+ seeds: readonly { readonly path: string; readonly manifestDir: string }[],
506
+ ): readonly OutsideModule[] {
507
+ const root = resolvePath(host.workingDirectory(), targetRoot);
508
+ return outsideModules(host, root, [], files, () => false, parse, seeds);
447
509
  }
448
510
 
449
511
  /**
@@ -20,7 +20,7 @@ import { SymbolFlags, type NodeHandle, type Symbol as TSSymbol } from "@typescri
20
20
  import type { AliasChains } from "./alias-chain.ts";
21
21
  import { resolvedTargets } from "./calls.ts";
22
22
  import type { Config, DeclarationEntry } from "./config.ts";
23
- import type { Inventory } from "./inventory.ts";
23
+ import { declaredComponent, type Inventory } from "./inventory.ts";
24
24
  import { must, Unanswerable } from "./query.ts";
25
25
  import { nameComponent } from "./ref.ts";
26
26
  import type { ProjectView } from "./session.ts";
@@ -118,15 +118,44 @@ function candidatesOf<Brand>(
118
118
  ];
119
119
  }
120
120
 
121
- /** The member of one container a path component spells, followed through any alias. */
121
+ /**
122
+ * The properties of the type one container declares, or of its constructor's type for a
123
+ * static member. A member a symbol keys is bound to that type later than the container's
124
+ * symbol table is built, so the table does not hold it.
125
+ */
126
+ function typeMembersOf<Brand>(
127
+ project: ProjectView<Brand>,
128
+ container: TSSymbol,
129
+ isStatic: boolean,
130
+ ): readonly TSSymbol[] {
131
+ const queries = project.queries;
132
+ const type = must(isStatic ? queries.typeOfSymbol(container) : queries.declaredTypeOf(container));
133
+ return type === undefined ? [] : must(queries.propertiesOf(type));
134
+ }
135
+
136
+ /**
137
+ * The member of one container a path component spells, followed through any alias. A
138
+ * component in brackets is a computed key, compared with the key each declaration of a
139
+ * candidate writes: the checker names a member a symbol keys by no spelling of its own.
140
+ */
122
141
  function memberOf<Brand>(
123
142
  project: ProjectView<Brand>,
124
143
  container: TSSymbol,
125
144
  component: string,
126
145
  isStatic: boolean,
127
146
  ): TSSymbol | undefined {
128
- const member = candidatesOf(project, container, isStatic).find(
129
- (candidate) => nameComponent(candidate.name) === component,
147
+ const computed = component.startsWith("[");
148
+ const candidates = candidatesOf(project, container, isStatic);
149
+ const member = (
150
+ computed ? [...candidates, ...typeMembersOf(project, container, isStatic)] : candidates
151
+ ).find(
152
+ (candidate) =>
153
+ nameComponent(candidate.name) === component ||
154
+ (computed &&
155
+ candidate.declarations.some((handle) => {
156
+ const node = project.declarationAt(handle)?.node;
157
+ return node !== undefined && declaredComponent(node) === component;
158
+ })),
130
159
  );
131
160
  return member === undefined ? undefined : unaliased(project, member);
132
161
  }
@@ -2,9 +2,9 @@
2
2
  // Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
3
3
  export const RECORD = {
4
4
  conformance: {
5
- corpusVersion: "5.2.0",
5
+ corpusVersion: "6.0.0",
6
6
  result: "pass",
7
- digest: "sha256:4050d1b67fb757bc12a60f8ece7317a3e8485d3383ec52919cd24b9dfd5509dc",
7
+ digest: "sha256:76c07ac14bda6e302444918d832b01844bac6f29b6f22a742d538804480c0769",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -319,14 +319,15 @@ export function contextualUses<Brand>(
319
319
  return found;
320
320
  };
321
321
 
322
- const calls = new Map<number, Called | undefined>();
322
+ const calls = new Map<number, readonly Called[]>();
323
323
  /**
324
- * The parameter and return types of a type's one call signature, or of its one construct
325
- * signature where it has no call signature, read once per type.
324
+ * The parameter and return types of each of a type's call signatures, or of each of its
325
+ * construct signatures where it has no call signature, read once per type.
326
326
  */
327
- const callOf = (type: Type): Called | undefined => {
328
- if (calls.has(type.id)) {
329
- return calls.get(type.id);
327
+ const callsOf = (type: Type): readonly Called[] => {
328
+ const known = calls.get(type.id);
329
+ if (known !== undefined) {
330
+ return known;
330
331
  }
331
332
  lookups += 1;
332
333
  let kind = SignatureKind.Call;
@@ -336,31 +337,29 @@ export function contextualUses<Brand>(
336
337
  kind = SignatureKind.Construct;
337
338
  signatures = queries.signaturesOf(type, kind);
338
339
  }
339
- let called: Called | undefined;
340
- const [signature, ...more] = isAnswered(signatures) ? signatures : [];
341
- if (signature !== undefined && more.length === 0) {
340
+ const called = (isAnswered(signatures) ? signatures : []).map((signature): Called => {
342
341
  const parameters = signature.parameters.map((_parameter, index) =>
343
342
  queries.parameterType(signature, index),
344
343
  );
345
344
  const result = queries.returnTypeOf(signature);
346
345
  lookups += parameters.length + 1;
347
- called = {
346
+ return {
348
347
  kind,
349
348
  parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
350
349
  result: isAnswered(result) ? result : undefined,
351
350
  };
352
- }
351
+ });
353
352
  calls.set(type.id, called);
354
353
  return called;
355
354
  };
356
355
 
357
356
  /**
358
357
  * Each object type a value of `source` is checked as against the object types of
359
- * `target`: the value's own, and through array elements, a function's parameters,
360
- * checked the other way, and result, and the type arguments two instantiations share
361
- * by place, an array among them, as an array reaches `Iterable<T>`.
362
- * A source that is one of the target's own object types matches it as it stands. Each
363
- * pair is compared once, which ends the walk over a recursive type.
358
+ * `target`: the value's own, and through array elements, every pair of signatures
359
+ * (parameters checked the other way, then results), which reads more than the overload
360
+ * the checker picks, and the type arguments of two instantiations by place, as an array
361
+ * reaches `Iterable<T>`. A source that is one of the target's own object types matches
362
+ * it as it stands. Each pair is compared once, which ends the walk over a recursive type.
364
363
  */
365
364
  const compared = (
366
365
  source: Type | undefined,
@@ -385,7 +384,7 @@ export function contextualUses<Brand>(
385
384
  into.push({ source: one, targets, outside: true });
386
385
  }
387
386
  const element = elementOf(one);
388
- const called = element === undefined ? callOf(one) : undefined;
387
+ const called = element === undefined ? callsOf(one) : [];
389
388
  const sourceArguments = typeArgumentsOf(one);
390
389
  for (const part of targets) {
391
390
  const partElement = elementOf(part);
@@ -399,17 +398,17 @@ export function contextualUses<Brand>(
399
398
  sourceArguments.slice(0, targetArguments.length).forEach((argument, index) => {
400
399
  compared(argument, targetArguments[index], into, seen);
401
400
  });
402
- if (element !== undefined) {
401
+ if (element !== undefined || called.length === 0) {
403
402
  continue;
404
403
  }
405
- const other = called === undefined ? undefined : callOf(part);
406
- if (called === undefined || other?.kind !== called.kind) {
407
- continue;
404
+ for (const other of callsOf(part)) {
405
+ for (const signature of called.filter((one) => one.kind === other.kind)) {
406
+ other.parameters.forEach((parameter, index) => {
407
+ compared(parameter, signature.parameters[index], into, seen);
408
+ });
409
+ compared(signature.result, other.result, into, seen);
410
+ }
408
411
  }
409
- other.parameters.forEach((parameter, index) => {
410
- compared(parameter, called.parameters[index], into, seen);
411
- });
412
- compared(called.result, other.result, into, seen);
413
412
  }
414
413
  }
415
414
  };
@@ -431,7 +430,7 @@ export function contextualUses<Brand>(
431
430
  if (element !== undefined) {
432
431
  return mayCarry(element, seen);
433
432
  }
434
- return callOf(part) !== undefined || typeArgumentsOf(part).some((one) => mayCarry(one, seen));
433
+ return callsOf(part).length > 0 || typeArgumentsOf(part).some((one) => mayCarry(one, seen));
435
434
  });
436
435
  };
437
436
 
@@ -467,11 +466,12 @@ export function contextualUses<Brand>(
467
466
  continue;
468
467
  }
469
468
  for (const [name, member] of sourceMembers) {
469
+ // A member the position shares with the source is no read of it. A member with no
470
+ // declaration, as a mapped type over literal keys has, is never one it shares.
470
471
  const read = tables.some((table) => {
471
472
  const declared = table.get(name);
472
473
  return (
473
474
  declared !== undefined &&
474
- declared.handles.size > 0 &&
475
475
  ![...declared.handles].some((handle) => member.handles.has(handle))
476
476
  );
477
477
  });