@cplieger/deadset-ts 4.0.1 → 5.0.0

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 (117) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +229 -36
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +186 -0
  10. package/dist/src/conformance-record.js +2 -2
  11. package/dist/src/convention-rows.js +383 -0
  12. package/dist/src/conventions.js +456 -0
  13. package/dist/src/dependencies.js +30 -3
  14. package/dist/src/discover.js +10 -3
  15. package/dist/src/emit-map.js +72 -0
  16. package/dist/src/entry-point-gaps.js +81 -35
  17. package/dist/src/entry-points.js +47 -32
  18. package/dist/src/exempt.js +11 -3
  19. package/dist/src/findings/completion.js +19 -29
  20. package/dist/src/findings/emitters.js +22 -1
  21. package/dist/src/findings/unused-declarations.js +7 -2
  22. package/dist/src/graph.js +13 -5
  23. package/dist/src/inventory.js +4 -2
  24. package/dist/src/kinds.js +2 -2
  25. package/dist/src/manifest.js +37 -11
  26. package/dist/src/matrix.js +21 -13
  27. package/dist/src/partial.js +3 -1
  28. package/dist/src/pnpm-workspace.js +120 -0
  29. package/dist/src/position.js +17 -2
  30. package/dist/src/print.js +2 -0
  31. package/dist/src/references.js +209 -30
  32. package/dist/src/report.js +26 -6
  33. package/dist/src/reporters/annotations.js +10 -3
  34. package/dist/src/reporters/template.js +3 -0
  35. package/dist/src/resolve.js +31 -6
  36. package/dist/src/roots.js +37 -8
  37. package/dist/src/run.js +13 -3
  38. package/dist/src/schema.js +2 -0
  39. package/dist/src/session.js +141 -34
  40. package/dist/src/setup-failure.js +23 -0
  41. package/dist/src/source-files.js +4 -61
  42. package/dist/src/sweep.js +4 -2
  43. package/dist/src/template-field.js +38 -6
  44. package/dist/src/test-support.js +59 -0
  45. package/dist/src/type-errors.js +252 -0
  46. package/dist/src/type-query-alias.js +92 -0
  47. package/dist/src/verbs/analyze.js +24 -4
  48. package/dist/src/verbs/explain.js +4 -1
  49. package/dist/src/verbs/print-projects.js +17 -56
  50. package/dist/src/verbs/print-retained.js +7 -2
  51. package/dist/src/verbs/print-roots.js +7 -2
  52. package/dist/src/version-range.js +255 -0
  53. package/dist/src/version.js +1 -1
  54. package/dist/src/workspace-manifests.js +68 -0
  55. package/dist/src/workspace-programs.js +342 -0
  56. package/dist/src/workspace-resolution.js +307 -0
  57. package/dist/src/workspace-run.js +75 -0
  58. package/dist/src/workspace.js +358 -0
  59. package/jsr.json +1 -1
  60. package/package.json +2 -1
  61. package/src/analysis.ts +394 -70
  62. package/src/component-files.ts +718 -0
  63. package/src/component-layer.ts +160 -0
  64. package/src/config.ts +11 -2
  65. package/src/configuration-files.ts +235 -0
  66. package/src/conformance-record.ts +2 -2
  67. package/src/convention-rows.ts +440 -0
  68. package/src/conventions.ts +640 -0
  69. package/src/dependencies.ts +36 -2
  70. package/src/discover.ts +10 -6
  71. package/src/emit-map.ts +93 -0
  72. package/src/entry-point-gaps.ts +92 -38
  73. package/src/entry-points.ts +66 -36
  74. package/src/exempt.ts +21 -3
  75. package/src/findings/completion.ts +24 -33
  76. package/src/findings/emitters.ts +32 -1
  77. package/src/findings/unused-declarations.ts +9 -2
  78. package/src/graph.ts +15 -4
  79. package/src/host.ts +23 -0
  80. package/src/index.ts +1 -1
  81. package/src/inventory.ts +13 -2
  82. package/src/kinds.ts +2 -2
  83. package/src/manifest.ts +47 -14
  84. package/src/matrix.ts +43 -15
  85. package/src/partial.ts +6 -1
  86. package/src/pnpm-workspace.ts +135 -0
  87. package/src/position.ts +23 -2
  88. package/src/print.ts +2 -0
  89. package/src/references.ts +227 -28
  90. package/src/report.ts +56 -6
  91. package/src/reporters/annotations.ts +12 -3
  92. package/src/reporters/template.ts +3 -0
  93. package/src/resolve.ts +59 -6
  94. package/src/roots.ts +60 -9
  95. package/src/run.ts +15 -5
  96. package/src/schema.ts +2 -0
  97. package/src/session.ts +218 -37
  98. package/src/setup-failure.ts +39 -0
  99. package/src/source-files.ts +4 -66
  100. package/src/sweep.ts +9 -2
  101. package/src/template-field.ts +42 -7
  102. package/src/test-support.ts +77 -0
  103. package/src/type-errors.ts +340 -0
  104. package/src/type-query-alias.ts +133 -0
  105. package/src/verbs/analyze.ts +38 -4
  106. package/src/verbs/explain.ts +4 -1
  107. package/src/verbs/print-projects.ts +17 -69
  108. package/src/verbs/print-retained.ts +7 -2
  109. package/src/verbs/print-roots.ts +7 -2
  110. package/src/verbs/verb.ts +6 -3
  111. package/src/version-range.ts +309 -0
  112. package/src/version.ts +1 -1
  113. package/src/workspace-manifests.ts +100 -0
  114. package/src/workspace-programs.ts +486 -0
  115. package/src/workspace-resolution.ts +393 -0
  116. package/src/workspace-run.ts +148 -0
  117. package/src/workspace.ts +463 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The `packages` list of a `pnpm-workspace.yaml`. The document is read as far as that
3
+ * one key needs and no further: a top-level `packages` key holding a block sequence or
4
+ * a one-line flow sequence of plain or quoted strings. Any other value of the key is
5
+ * refused rather than guessed at, because a list read wrong names the wrong packages.
6
+ */
7
+
8
+ /** What the document says about `packages`. */
9
+ export type PnpmPackages =
10
+ | { readonly kind: "listed"; readonly patterns: readonly string[] }
11
+ /** The document declares no `packages` key. */
12
+ | { readonly kind: "absent" }
13
+ /** The key holds something this reader does not read, with what it holds. */
14
+ | { readonly kind: "refused"; readonly reason: string };
15
+
16
+ /** The characters a plain scalar may not start with. */
17
+ const INDICATORS = new Set(["[", "]", "{", "}", ",", "&", "*", "!", "|", ">", "%", "@", "`", "#"]);
18
+
19
+ /** One line with its comment removed, a `#` inside quotes kept. */
20
+ function uncommented(line: string): string {
21
+ let quote: string | undefined;
22
+ for (let at = 0; at < line.length; at += 1) {
23
+ const character = line[at];
24
+ if (quote !== undefined) {
25
+ if (character === quote) {
26
+ quote = undefined;
27
+ }
28
+ continue;
29
+ }
30
+ if (character === "'" || character === '"') {
31
+ quote = character;
32
+ } else if (character === "#" && (at === 0 || /\s/u.test(line[at - 1] ?? ""))) {
33
+ return line.slice(0, at).trimEnd();
34
+ }
35
+ }
36
+ return line.trimEnd();
37
+ }
38
+
39
+ /** One scalar as written, or `undefined` where it is not a string this reader reads. */
40
+ function scalarOf(text: string): string | undefined {
41
+ const value = text.trim();
42
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
43
+ const inner = value.slice(1, -1);
44
+ return /'(?!')/u.test(inner.replaceAll("''", "")) ? undefined : inner.replaceAll("''", "'");
45
+ }
46
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
47
+ const inner = value.slice(1, -1);
48
+ return inner.includes("\\") || inner.includes('"') ? undefined : inner;
49
+ }
50
+ if (
51
+ value === "" ||
52
+ INDICATORS.has(value[0] ?? "") ||
53
+ value.startsWith("'") ||
54
+ value.startsWith('"')
55
+ ) {
56
+ return undefined;
57
+ }
58
+ if (value.startsWith("- ") || value.startsWith("? ") || /:(?:\s|$)/u.test(value)) {
59
+ return undefined;
60
+ }
61
+ return value;
62
+ }
63
+
64
+ /** The entries of a one-line flow sequence, or `undefined` where one does not read. */
65
+ function flowOf(text: string): readonly string[] | undefined {
66
+ const inner = text.slice(1, -1).trim();
67
+ if (inner === "") {
68
+ return [];
69
+ }
70
+ const entries = inner.split(",");
71
+ if (entries.length > 1 && entries.at(-1)?.trim() === "") {
72
+ entries.pop();
73
+ }
74
+ const found: string[] = [];
75
+ for (const entry of entries) {
76
+ const value = scalarOf(entry);
77
+ if (value === undefined) {
78
+ return undefined;
79
+ }
80
+ found.push(value);
81
+ }
82
+ return found;
83
+ }
84
+
85
+ const KEY = /^(?:packages|'packages'|"packages")\s*:(?:\s(.*))?$/u;
86
+
87
+ /** The `packages` list one `pnpm-workspace.yaml` text declares. */
88
+ export function pnpmPackages(text: string): PnpmPackages {
89
+ const lines = text.split(/\r?\n/u).map(uncommented);
90
+ const keys = lines.flatMap((line, at) => (KEY.test(line) ? [at] : []));
91
+ const [at] = keys;
92
+ if (at === undefined) {
93
+ return { kind: "absent" };
94
+ }
95
+ if (keys.length > 1) {
96
+ return { kind: "refused", reason: "the packages key is declared more than once" };
97
+ }
98
+ const rest = (KEY.exec(lines[at] ?? "")?.[1] ?? "").trim();
99
+ if (rest !== "") {
100
+ const listed = rest.startsWith("[") && rest.endsWith("]") ? flowOf(rest) : undefined;
101
+ return listed === undefined
102
+ ? {
103
+ kind: "refused",
104
+ reason: `packages holds ${JSON.stringify(rest)}, not a sequence of strings`,
105
+ }
106
+ : { kind: "listed", patterns: listed };
107
+ }
108
+ const patterns: string[] = [];
109
+ for (const line of lines.slice(at + 1)) {
110
+ if (line.trim() === "") {
111
+ continue;
112
+ }
113
+ const item = /^(\s*)-(?:\s+(.*))?$/u.exec(line);
114
+ if (item === null) {
115
+ if (/^\s/u.test(line)) {
116
+ return {
117
+ kind: "refused",
118
+ reason: `packages holds ${JSON.stringify(line.trim())}, not a sequence item`,
119
+ };
120
+ }
121
+ break;
122
+ }
123
+ const value = scalarOf(item[2] ?? "");
124
+ if (value === undefined) {
125
+ return {
126
+ kind: "refused",
127
+ reason: `the packages item ${JSON.stringify(line.trim())} is not a plain or quoted string`,
128
+ };
129
+ }
130
+ patterns.push(value);
131
+ }
132
+ return patterns.length === 0
133
+ ? { kind: "refused", reason: "packages holds no sequence" }
134
+ : { kind: "listed", patterns };
135
+ }
package/src/position.ts CHANGED
@@ -34,18 +34,39 @@ export class PositionError extends Error {
34
34
  }
35
35
  }
36
36
 
37
+ /** Whether the compiler read one file through a content mapper, which makes it a component file. */
38
+ export function isComponentFile(file: SourceFile): boolean {
39
+ return file.contentMapper !== undefined;
40
+ }
41
+
42
+ /**
43
+ * The length of the file as it is written. A component file's module holds the file's
44
+ * text at its own offsets and appends what it needs past its end.
45
+ */
46
+ export function writtenLength(file: SourceFile): number {
47
+ return isComponentFile(file) ? file.originalText.length : file.text.length;
48
+ }
49
+
37
50
  /**
38
51
  * The position of one offset of one file, rendered against the target root.
39
52
  *
40
53
  * `offset` is a UTF-16 code unit offset into the file's text, which is what every
41
- * node of the tree carries.
54
+ * node of the tree carries. An offset a component file's module appends renders where
55
+ * the module's span map places it in the file.
42
56
  */
43
57
  export function renderPosition(file: SourceFile, root: string, offset: number): Position {
44
58
  const path = relativePath(root, file.fileName);
45
59
  if (path === undefined) {
46
60
  throw new PositionError(file.fileName, root);
47
61
  }
48
- const { line, character } = file.getLineAndCharacterOfPosition(offset);
62
+ const written =
63
+ isComponentFile(file) && offset >= file.originalText.length
64
+ ? Math.min(
65
+ file.spanMap?.virtualToOriginalPosition(offset).position ?? offset,
66
+ file.originalText.length,
67
+ )
68
+ : offset;
69
+ const { line, character } = file.getLineAndCharacterOfPosition(written);
49
70
  return { path, line: line + 1, column: character + 1 };
50
71
  }
51
72
 
package/src/print.ts CHANGED
@@ -75,6 +75,8 @@ export function printConfig(config: Config, provenance: Provenance): string {
75
75
  ts: {
76
76
  test_files: config.ts.testFiles,
77
77
  entry_files: config.ts.entryFiles,
78
+ component_extensions: config.ts.componentExtensions,
79
+ disabled_conventions: config.ts.disabledConventions,
78
80
  injection_registrations: config.ts.injectionRegistrations.map(declarationOf),
79
81
  lifecycle_contracts: config.ts.lifecycleContracts.map((entry) => ({
80
82
  components: entry.components.map(declarationOf),
package/src/references.ts CHANGED
@@ -6,9 +6,11 @@ import {
6
6
  isDeleteExpression,
7
7
  isElementAccessExpression,
8
8
  isExportDeclaration,
9
+ isExpressionStatement,
9
10
  isExternalModuleReference,
10
11
  isForInStatement,
11
12
  isForOfStatement,
13
+ isForStatement,
12
14
  isImportDeclaration,
13
15
  isImportEqualsDeclaration,
14
16
  isNamespaceExport,
@@ -23,6 +25,7 @@ import {
23
25
  isSpreadAssignment,
24
26
  isSpreadElement,
25
27
  isStringLiteral,
28
+ isVoidExpression,
26
29
  SyntaxKind,
27
30
  type Node,
28
31
  type SourceFile,
@@ -31,11 +34,12 @@ import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
31
34
  import { aliasChains, type ChainTarget } from "./alias-chain.ts";
32
35
  import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
33
36
  import { globExpression } from "./glob.ts";
34
- import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
37
+ import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
35
38
  import { relativePath } from "./paths.ts";
36
- import { byPosition, renderPosition, type Position } from "./position.ts";
39
+ import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
37
40
  import { DEFAULT_BATCH_CAP, UNANSWERED, type Answer } from "./query.ts";
38
41
  import type { ProjectView } from "./session.ts";
42
+ import { typeQueryAliases } from "./type-query-alias.ts";
39
43
 
40
44
  /**
41
45
  * Every use one project's own files make of the project's own declarations.
@@ -64,11 +68,11 @@ import type { ProjectView } from "./session.ts";
64
68
  export type Use = "read" | "write" | "evaluation" | "decorator";
65
69
 
66
70
  /**
67
- * Which path answered for one reference: `batch`, the batched lookup over a file's name
68
- * nodes; `resolved-symbol`, the per-node lookup for the residue a batch left;
69
- * `shorthand`, the lookup for the value `{ a }` reads; `alias`, a step along an import
70
- * or export chain; `syntax`, the tree alone, which says what a decorator is attached to;
71
- * `destructured`, the property a pattern names, on the destructured value's type; and
71
+ * Which path answered for one reference: `batch`, the batched lookup over a file's name nodes;
72
+ * `resolved-symbol`, the per-node lookup for the residue a batch left; `shorthand`, the lookup
73
+ * for the value `{ a }` reads; `alias`, a step along an import or export chain; `syntax`, the
74
+ * tree alone, which says what a decorator is attached to and what a component's markup may
75
+ * read; `destructured`, the property a pattern names, on the destructured value's type; and
72
76
  * `by-name`, every declaration bearing the name a site spells, where the checker failed.
73
77
  */
74
78
  export type Resolution =
@@ -184,9 +188,8 @@ export interface ReferenceOptions {
184
188
 
185
189
  /**
186
190
  * The operators whose left side one assignment stores into. A compound assignment is
187
- * among them: it reads its target only to write the result back, so nothing else
188
- * receives what it read, and a declaration whose every other reference is one of these
189
- * carries no information out of itself.
191
+ * among them: where its value is discarded it reads its target only to write the result
192
+ * back, so nothing else receives what it read.
190
193
  */
191
194
  const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
192
195
  SyntaxKind.EqualsToken,
@@ -207,6 +210,40 @@ const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
207
210
  SyntaxKind.CaretEqualsToken,
208
211
  ]);
209
212
 
213
+ /**
214
+ * The assignments that read their target to decide whether to store at all, so the read
215
+ * carries information out whatever becomes of the result.
216
+ */
217
+ const LOGICAL_ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
218
+ SyntaxKind.BarBarEqualsToken,
219
+ SyntaxKind.AmpersandAmpersandEqualsToken,
220
+ SyntaxKind.QuestionQuestionEqualsToken,
221
+ ]);
222
+
223
+ /**
224
+ * Whether the value of one store expression is discarded: it is a statement of its own, a
225
+ * clause of a `for` loop, the operand of `void`, or the left operand of a comma.
226
+ */
227
+ function valueDiscarded(node: Node): boolean {
228
+ let child = node;
229
+ let parent = node.parent;
230
+ while (isParenthesizedExpression(parent)) {
231
+ child = parent;
232
+ parent = parent.parent;
233
+ }
234
+ if (isExpressionStatement(parent) || isVoidExpression(parent)) {
235
+ return true;
236
+ }
237
+ if (isForStatement(parent)) {
238
+ return parent.initializer === child || parent.incrementor === child;
239
+ }
240
+ return (
241
+ isBinaryExpression(parent) &&
242
+ parent.operatorToken.kind === SyntaxKind.CommaToken &&
243
+ parent.left === child
244
+ );
245
+ }
246
+
210
247
  /** Whether one node is an identifier or a private name, the two forms a name takes. */
211
248
  function isName(node: Node): boolean {
212
249
  return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
@@ -277,12 +314,20 @@ function markWrite(target: Node, writes: Set<number>): void {
277
314
  * Records every store one node performs: an assignment, a compound assignment, an
278
315
  * increment or a decrement, a `delete`, and an iteration that assigns to a name declared
279
316
  * elsewhere. An iteration that declares its own name stores into nothing the inventory
280
- * holds.
317
+ * holds. A store that reads its target as well records it in `reads`: a logical
318
+ * assignment, and a compound assignment, an increment or a decrement whose value is used.
281
319
  */
282
- function markStores(node: Node, writes: Set<number>): void {
320
+ function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
283
321
  if (isBinaryExpression(node)) {
284
- if (ASSIGNMENTS.has(node.operatorToken.kind)) {
322
+ const operator = node.operatorToken.kind;
323
+ if (ASSIGNMENTS.has(operator)) {
285
324
  markWrite(node.left, writes);
325
+ if (
326
+ LOGICAL_ASSIGNMENTS.has(operator) ||
327
+ (operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))
328
+ ) {
329
+ markWrite(node.left, reads);
330
+ }
286
331
  }
287
332
  return;
288
333
  }
@@ -292,6 +337,9 @@ function markStores(node: Node, writes: Set<number>): void {
292
337
  node.operator === SyntaxKind.MinusMinusToken
293
338
  ) {
294
339
  markWrite(node.operand, writes);
340
+ if (!valueDiscarded(node)) {
341
+ markWrite(node.operand, reads);
342
+ }
295
343
  }
296
344
  return;
297
345
  }
@@ -448,12 +496,14 @@ function linksOf(
448
496
  * order, each with its enclosing declaration and its use, and every re-export the file
449
497
  * writes. One walk answers all of it, because a second would have to agree with this one
450
498
  * about which nodes are names. A declaration's own declared name is left out, which is
451
- * what makes a declaration referenced only from its own site an unreferenced one.
499
+ * what makes a declaration referenced only from its own site an unreferenced one, and so
500
+ * is every node of `unread`, a subtree that is no use.
452
501
  */
453
502
  function sitesOf(
454
503
  file: SourceFile,
455
504
  declarations: ReadonlyMap<string, string>,
456
505
  fileId: string,
506
+ unread: ReadonlySet<Node>,
457
507
  ): FileSites {
458
508
  const found: Site[] = [];
459
509
  const reads: Read[] = [];
@@ -461,6 +511,7 @@ function sitesOf(
461
511
  const evaluations: Evaluation[] = [];
462
512
  const decorated: Decorated[] = [];
463
513
  const writes = new Set<number>();
514
+ const alsoRead = new Set<number>();
464
515
  const declared = new Set<number>();
465
516
  const shorthands = new Map<number, Node>();
466
517
  let enclosing = fileId;
@@ -484,19 +535,23 @@ function sitesOf(
484
535
  };
485
536
 
486
537
  const visit = (node: Node): void => {
538
+ if (unread.has(node)) {
539
+ return;
540
+ }
487
541
  if (isName(node)) {
488
542
  if (!declared.has(node.pos)) {
489
- found.push({
490
- node,
491
- shorthand: shorthands.get(node.pos),
492
- from: enclosing,
493
- use: writes.has(node.pos) ? "write" : "read",
494
- });
543
+ const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
544
+ if (writes.has(node.pos)) {
545
+ found.push({ ...site, use: "write" });
546
+ }
547
+ if (!writes.has(node.pos) || alsoRead.has(node.pos)) {
548
+ found.push({ ...site, use: "read" });
549
+ }
495
550
  }
496
551
  return;
497
552
  }
498
553
  markNames(node);
499
- markStores(node, writes);
554
+ markStores(node, writes, alsoRead);
500
555
  for (const read of propertyReadsOf(node)) {
501
556
  reads.push({ ...read, from: enclosing });
502
557
  }
@@ -523,6 +578,9 @@ function sitesOf(
523
578
  return { uses: found, reads, links, evaluations, decorated };
524
579
  }
525
580
 
581
+ /** The rule a file the analysis classified as test-support code is counted under. */
582
+ const TEST_SUPPORT_RULE = "test-support";
583
+
526
584
  /**
527
585
  * The name one test-file rule takes. A rule's name is a word list the report's own
528
586
  * vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
@@ -539,17 +597,20 @@ function patternRule(index: number): string {
539
597
  export function testFileRulesOf(
540
598
  testFiles: readonly string[],
541
599
  paths: readonly string[],
600
+ supportFiles: readonly string[] = [],
542
601
  ): readonly TestFileRule[] {
543
602
  const distinct = [...new Set(paths)];
544
- return testFiles
545
- .map((pattern, index) => {
603
+ const support = new Set(supportFiles).size;
604
+ return [
605
+ ...testFiles.map((pattern, index) => {
546
606
  const expression = globExpression(pattern);
547
607
  return {
548
608
  rule: patternRule(index),
549
609
  matched: distinct.filter((path) => expression.test(path)).length,
550
610
  };
551
- })
552
- .sort((a, b) => compare(a.rule, b.rule));
611
+ }),
612
+ ...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
613
+ ].sort((a, b) => compare(a.rule, b.rule));
553
614
  }
554
615
 
555
616
  const importedCache = new WeakMap<
@@ -599,6 +660,37 @@ function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string |
599
660
  return names;
600
661
  }
601
662
 
663
+ /**
664
+ * The name node of every import binding one file's top level declares, and whether it
665
+ * binds a whole module.
666
+ */
667
+ function importBindings(file: SourceFile): readonly { node: Node; whole: boolean }[] {
668
+ const found: { node: Node; whole: boolean }[] = [];
669
+ for (const statement of file.statements) {
670
+ if (isImportEqualsDeclaration(statement)) {
671
+ found.push({ node: statement.name, whole: true });
672
+ continue;
673
+ }
674
+ const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
675
+ if (clause === undefined) {
676
+ continue;
677
+ }
678
+ if (clause.name !== undefined) {
679
+ found.push({ node: clause.name, whole: false });
680
+ }
681
+ const bindings = clause.namedBindings;
682
+ if (bindings === undefined) {
683
+ continue;
684
+ }
685
+ if (bindings.kind === SyntaxKind.NamespaceImport) {
686
+ found.push({ node: bindings.name, whole: true });
687
+ } else {
688
+ found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
689
+ }
690
+ }
691
+ return found;
692
+ }
693
+
602
694
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
603
695
  function compare(a: string, b: string): number {
604
696
  if (a === b) {
@@ -656,6 +748,20 @@ export function references<Brand>(
656
748
  const fileIds = new Set(
657
749
  held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
658
750
  );
751
+ let topLevel: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
752
+ /** The declarations one file's top level holds. */
753
+ const declaredIn = (fileId: string): readonly InventorySymbol[] => {
754
+ if (topLevel === undefined) {
755
+ const byParent = new Map<string, InventorySymbol[]>();
756
+ for (const symbol of held.symbols) {
757
+ if (fileIds.has(symbol.parent)) {
758
+ byParent.set(symbol.parent, [...(byParent.get(symbol.parent) ?? []), symbol]);
759
+ }
760
+ }
761
+ topLevel = byParent;
762
+ }
763
+ return topLevel.get(fileId) ?? [];
764
+ };
659
765
 
660
766
  /** The identifier of the declaration one node of this program is, where it is one. */
661
767
  const declaredAt = (node: Node): string | undefined =>
@@ -693,6 +799,49 @@ export function references<Brand>(
693
799
  ];
694
800
  };
695
801
 
802
+ // A type-query alias's own type names its export without using it; a use of the
803
+ // global is the use, so it names every link of the export's chain as well.
804
+ const aliases = project.ownSourceFiles().flatMap(typeQueryAliases);
805
+ const unread = new Set(aliases.map((alias) => alias.type));
806
+ const aliased = new Map<string, readonly ChainTarget[]>();
807
+ if (aliases.length > 0) {
808
+ const exported = project.symbolsAt(aliases.map((alias) => project.handle(alias.exported)));
809
+ aliases.forEach((alias, index) => {
810
+ const id = declaredAt(alias.declaration);
811
+ const symbol = exported[index];
812
+ if (id === undefined || symbol === undefined) {
813
+ return;
814
+ }
815
+ aliased.set(
816
+ id,
817
+ symbol === UNANSWERED
818
+ ? (declarationsByName(held).get(alias.name) ?? []).map((one) => ({
819
+ id: one,
820
+ stepped: true,
821
+ guessed: true,
822
+ }))
823
+ : chains.chainOf(symbol).map((target) => ({ ...target, stepped: true })),
824
+ );
825
+ });
826
+ }
827
+ /** One use's targets, then every link an alias global among them stands for, each once. */
828
+ const throughAliases = (targets: readonly ChainTarget[]): readonly ChainTarget[] => {
829
+ if (aliased.size === 0) {
830
+ return targets;
831
+ }
832
+ const all = [...targets];
833
+ const seen = new Set(targets.map((target) => target.id));
834
+ for (const target of targets) {
835
+ for (const forwarded of aliased.get(target.id) ?? []) {
836
+ if (!seen.has(forwarded.id)) {
837
+ seen.add(forwarded.id);
838
+ all.push(forwarded);
839
+ }
840
+ }
841
+ }
842
+ return all;
843
+ };
844
+
696
845
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
697
846
  const symbolFor = (
698
847
  site: Site,
@@ -716,11 +865,16 @@ export function references<Brand>(
716
865
  if (test) {
717
866
  tests.push(path);
718
867
  }
868
+ const fileId = declaredAt(file) ?? "";
719
869
  const { uses, reads, links, evaluations, decorated } = sitesOf(
720
870
  file,
721
871
  declarations,
722
- declaredAt(file) ?? "",
872
+ fileId,
873
+ unread,
723
874
  );
875
+ // A component file's markup may use any binding its blocks declare at the top level.
876
+ const markup = isComponentFile(file);
877
+ const bindings = markup ? importBindings(file) : [];
724
878
 
725
879
  // A decorator is written inside the declaration it is attached to and receives it,
726
880
  // so the declaration is used where the decorator is written, whatever the
@@ -746,6 +900,7 @@ export function references<Brand>(
746
900
  ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
747
901
  ...links.map((link) => link.node),
748
902
  ...evaluations.map((evaluation) => evaluation.node),
903
+ ...bindings.map((binding) => binding.node),
749
904
  ]),
750
905
  ];
751
906
  const answered = new Map<Node, Answer<TSSymbol>>();
@@ -809,6 +964,49 @@ export function references<Brand>(
809
964
  }
810
965
  }
811
966
 
967
+ if (markup) {
968
+ for (const symbol of declaredIn(fileId)) {
969
+ found.push({
970
+ from: fileId,
971
+ to: symbol.id,
972
+ position: symbol.position,
973
+ use: "read",
974
+ resolution: "syntax",
975
+ test,
976
+ });
977
+ }
978
+ for (const binding of bindings) {
979
+ const symbol = answered.get(binding.node);
980
+ if (symbol === undefined) {
981
+ continue;
982
+ }
983
+ const position = renderPosition(file, root, binding.node.getStart());
984
+ const chained =
985
+ symbol === UNANSWERED
986
+ ? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
987
+ : chains.chainOf(symbol);
988
+ // The markup may name any export of a module a binding holds whole.
989
+ const targets = binding.whole
990
+ ? chained.flatMap((target) => [
991
+ target,
992
+ ...declaredIn(target.id)
993
+ .filter((one) => one.exported)
994
+ .map((one) => ({ ...target, id: one.id })),
995
+ ])
996
+ : chained;
997
+ for (const target of targets) {
998
+ found.push({
999
+ from: fileId,
1000
+ to: target.id,
1001
+ position,
1002
+ use: "read",
1003
+ resolution: target.guessed ? "by-name" : target.stepped ? "alias" : "batch",
1004
+ test,
1005
+ });
1006
+ }
1007
+ }
1008
+ }
1009
+
812
1010
  // A pattern names a property of the value it destructures, which no name node of
813
1011
  // the pattern resolves to: a shorthand's name is the local it binds.
814
1012
  destructured.resolve(reads).forEach((symbol, index) => {
@@ -851,10 +1049,11 @@ export function references<Brand>(
851
1049
  const position = renderPosition(file, root, site.node.getStart());
852
1050
  // A re-export something imports is used and so is the declaration behind it, so
853
1051
  // the use names every link of the chain its name stands at the head of.
854
- const targets =
1052
+ const targets = throughAliases(
855
1053
  symbol === UNANSWERED
856
1054
  ? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
857
- : chains.chainOf(symbol);
1055
+ : chains.chainOf(symbol),
1056
+ );
858
1057
  for (const target of targets) {
859
1058
  found.push({
860
1059
  from: site.from,