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

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 (131) 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 +58 -22
  4. package/dist/src/augmentations.js +23 -24
  5. package/dist/src/bindings.js +382 -0
  6. package/dist/src/calls.js +25 -8
  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/report.generated.js +6 -2
  14. package/dist/src/dependencies.js +25 -29
  15. package/dist/src/destructuring.js +1 -19
  16. package/dist/src/element-access.js +179 -164
  17. package/dist/src/entries/build-scripts.js +2 -2
  18. package/dist/src/entries/configuration-files.js +3 -7
  19. package/dist/src/entries/conventions.js +2 -2
  20. package/dist/src/entries/html-entries.js +7 -10
  21. package/dist/src/entries/mdx-entries.js +2 -2
  22. package/dist/src/entries/static-module.js +2 -1
  23. package/dist/src/facts.js +4 -4
  24. package/dist/src/findings/completion.js +40 -4
  25. package/dist/src/{implementations.js → findings/implementations.js} +3 -3
  26. package/dist/src/{in-operands.js → findings/in-operands.js} +2 -2
  27. package/dist/src/framework-lifecycle.js +41 -14
  28. package/dist/src/graph.js +12 -4
  29. package/dist/src/import-bindings.js +73 -28
  30. package/dist/src/index/sites.js +557 -0
  31. package/dist/src/indexed-access.js +523 -478
  32. package/dist/src/interface-satisfaction.js +1 -1
  33. package/dist/src/keyof-comparisons.js +124 -103
  34. package/dist/src/load/expected-module.js +2 -2
  35. package/dist/src/load/specifiers.js +2 -4
  36. package/dist/src/load/synthesize.js +5 -0
  37. package/dist/src/load/workspace-resolution.js +2 -2
  38. package/dist/src/matrix.js +6 -4
  39. package/dist/src/module-declarations.js +70 -85
  40. package/dist/src/overrides.js +134 -112
  41. package/dist/src/paths.js +46 -7
  42. package/dist/src/platform-calls.js +2 -2
  43. package/dist/src/platform-symbols.js +13 -19
  44. package/dist/src/references/read-rule.js +1 -0
  45. package/dist/src/references/read-rules.js +48 -0
  46. package/dist/src/references/sink.js +151 -0
  47. package/dist/src/references/sources.js +450 -0
  48. package/dist/src/references.js +110 -1285
  49. package/dist/src/roots/configured-declarations.js +3 -0
  50. package/dist/src/roots/root-set.js +1 -1
  51. package/dist/src/roots/unseen-loads.js +38 -10
  52. package/dist/src/serialization-contract.js +8 -3
  53. package/dist/src/stores.js +4 -2
  54. package/dist/src/sweep.js +25 -13
  55. package/dist/src/symbol-keys.js +89 -83
  56. package/dist/src/tests/file-class.js +76 -0
  57. package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
  58. package/dist/src/{test-support.js → tests/support.js} +2 -11
  59. package/dist/src/type-parameter-reads.js +178 -139
  60. package/dist/src/unimplemented-callees.js +262 -242
  61. package/dist/src/verbs/analyze.js +3 -3
  62. package/dist/src/verbs/explain.js +50 -28
  63. package/dist/src/verbs/print-retained.js +2 -2
  64. package/jsr.json +1 -1
  65. package/package.json +2 -2
  66. package/src/alias-chain.ts +0 -7
  67. package/src/amd.ts +130 -0
  68. package/src/analysis.ts +86 -23
  69. package/src/augmentations.ts +41 -27
  70. package/src/bindings.ts +460 -0
  71. package/src/calls.ts +25 -7
  72. package/src/commands/tools.ts +5 -11
  73. package/src/component-files.ts +4 -2
  74. package/src/conformance-record.ts +1 -1
  75. package/src/consumers.ts +8 -11
  76. package/src/contextual-uses.ts +2 -22
  77. package/src/contract/config.generated.ts +4 -1
  78. package/src/contract/report.generated.ts +6 -2
  79. package/src/dependencies.ts +27 -28
  80. package/src/destructuring.ts +1 -31
  81. package/src/element-access.ts +188 -182
  82. package/src/entries/build-scripts.ts +2 -2
  83. package/src/entries/configuration-files.ts +11 -8
  84. package/src/entries/conventions.ts +2 -1
  85. package/src/entries/html-entries.ts +15 -14
  86. package/src/entries/mdx-entries.ts +2 -2
  87. package/src/entries/static-module.ts +2 -1
  88. package/src/facts.ts +5 -3
  89. package/src/findings/boundary.ts +2 -0
  90. package/src/findings/completion.ts +43 -4
  91. package/src/findings/emitter.ts +1 -1
  92. package/src/{implementations.ts → findings/implementations.ts} +4 -4
  93. package/src/{in-operands.ts → findings/in-operands.ts} +3 -3
  94. package/src/framework-lifecycle.ts +42 -14
  95. package/src/graph.ts +24 -6
  96. package/src/import-bindings.ts +98 -29
  97. package/src/index/sites.ts +781 -0
  98. package/src/indexed-access.ts +570 -527
  99. package/src/interface-satisfaction.ts +1 -1
  100. package/src/keyof-comparisons.ts +131 -116
  101. package/src/load/expected-module.ts +8 -2
  102. package/src/load/specifiers.ts +8 -5
  103. package/src/load/synthesize.ts +6 -0
  104. package/src/load/workspace-resolution.ts +9 -2
  105. package/src/matrix.ts +6 -3
  106. package/src/module-declarations.ts +74 -98
  107. package/src/overrides.ts +162 -134
  108. package/src/paths.ts +54 -8
  109. package/src/platform-calls.ts +3 -3
  110. package/src/platform-symbols.ts +11 -27
  111. package/src/references/read-rule.ts +60 -0
  112. package/src/references/read-rules.ts +55 -0
  113. package/src/references/sink.ts +213 -0
  114. package/src/references/sources.ts +632 -0
  115. package/src/references.ts +168 -1733
  116. package/src/report.ts +1 -1
  117. package/src/roots/configured-declarations.ts +3 -0
  118. package/src/roots/root-set.ts +1 -1
  119. package/src/roots/unseen-loads.ts +49 -11
  120. package/src/serialization-contract.ts +8 -3
  121. package/src/stores.ts +13 -2
  122. package/src/sweep.ts +37 -14
  123. package/src/symbol-keys.ts +102 -102
  124. package/src/tests/file-class.ts +118 -0
  125. package/src/{outside-tests.ts → tests/outside.ts} +16 -10
  126. package/src/{test-support.ts → tests/support.ts} +9 -20
  127. package/src/type-parameter-reads.ts +187 -158
  128. package/src/unimplemented-callees.ts +297 -280
  129. package/src/verbs/analyze.ts +3 -2
  130. package/src/verbs/explain.ts +74 -31
  131. package/src/verbs/print-retained.ts +2 -2
@@ -29,7 +29,7 @@ import {
29
29
  } from "@typescript/native/ast";
30
30
  import { SymbolFlags, type Type } from "@typescript/native/sync";
31
31
  import { aliasChains } from "./alias-chain.ts";
32
- import { emptyImplementation } from "./implementations.ts";
32
+ import { emptyImplementation } from "./findings/implementations.ts";
33
33
  import type { DetectorInput, Evidence } from "./exempt.ts";
34
34
  import type { Reference } from "./references.ts";
35
35
  import type { Exemption } from "./sweep.ts";
@@ -10,7 +10,10 @@ import {
10
10
  isCaseClause,
11
11
  isElementAccessExpression,
12
12
  isIdentifier,
13
+ isNoSubstitutionTemplateLiteral,
14
+ isNumericLiteral,
13
15
  isPropertyAccessExpression,
16
+ isStringLiteral,
14
17
  isSwitchStatement,
15
18
  isTypeOperatorNode,
16
19
  SyntaxKind,
@@ -20,12 +23,8 @@ import {
20
23
  type TypeNode,
21
24
  } from "@typescript/native/ast";
22
25
  import { SymbolFlags, type Type } from "@typescript/native/sync";
23
- import type { AliasChains } from "./alias-chain.ts";
24
- import { nodeKey, type Inventory } from "./inventory.ts";
25
- import { renderPosition } from "./position.ts";
26
26
  import { UNANSWERED } from "./query.ts";
27
- import type { Reference } from "./references.ts";
28
- import type { ProjectView } from "./session.ts";
27
+ import type { Read, ReadRule } from "./references/read-rule.ts";
29
28
  import { parenthesesRemoved } from "./unwrap.ts";
30
29
 
31
30
  /** The operators that compare their operands for equality. */
@@ -83,125 +82,141 @@ function literalNames(parts: readonly Type[]): readonly string[] {
83
82
  }
84
83
 
85
84
  /**
86
- * The references the key comparisons of one project's own files make to the members their
87
- * literal side names, each made by the declaration holding the comparison.
85
+ * The reads the key comparisons of a side's files make of the members their literal side
86
+ * names, each made by the declaration holding the comparison. A question the checker leaves
87
+ * unanswered holds what its answer could have read.
88
88
  */
89
- export function keyofComparisonReferences<Brand>(
90
- project: ProjectView<Brand>,
91
- held: Inventory,
92
- chains: AliasChains,
93
- root: string,
94
- testFiles: ReadonlySet<string>,
95
- ): readonly Reference[] {
96
- const queries = project.queries;
97
- const comparisons: Comparison[] = [];
98
- for (const file of project.ownSourceFiles()) {
99
- const fileId = held.declarations.get(nodeKey(file, file));
100
- if (fileId === undefined) {
101
- continue;
102
- }
103
- const visit = (node: Node, from: string): void => {
104
- const holder = held.declarations.get(nodeKey(file, node)) ?? from;
105
- if (isBinaryExpression(node) && EQUALITIES.has(node.operatorToken.kind)) {
106
- comparisons.push({ file, from: holder, keyed: node.left, other: node.right });
107
- comparisons.push({ file, from: holder, keyed: node.right, other: node.left });
108
- }
109
- if (isSwitchStatement(node)) {
110
- for (const clause of node.caseBlock.clauses) {
111
- if (isCaseClause(clause)) {
112
- comparisons.push({
113
- file,
114
- from: holder,
115
- keyed: node.expression,
116
- other: clause.expression,
89
+ export const keyofComparisons: ReadRule = {
90
+ start() {
91
+ const comparisons: Comparison[] = [];
92
+ return {
93
+ visit(node, { file, from }) {
94
+ if (isBinaryExpression(node) && EQUALITIES.has(node.operatorToken.kind)) {
95
+ comparisons.push({ file, from, keyed: node.left, other: node.right });
96
+ comparisons.push({ file, from, keyed: node.right, other: node.left });
97
+ }
98
+ if (isSwitchStatement(node)) {
99
+ for (const clause of node.caseBlock.clauses) {
100
+ if (isCaseClause(clause)) {
101
+ comparisons.push({ file, from, keyed: node.expression, other: clause.expression });
102
+ }
103
+ }
104
+ }
105
+ },
106
+ reads({ project, chains }) {
107
+ const queries = project.queries;
108
+ const found: Read[] = [];
109
+ const named = comparisons.flatMap((one) => {
110
+ const name = nameOf(one.keyed);
111
+ return name === undefined || isElementAccessExpression(parenthesesRemoved(one.other))
112
+ ? []
113
+ : [{ ...one, name }];
114
+ });
115
+ if (named.length === 0) {
116
+ return found;
117
+ }
118
+ // The operand's own declaration states the key type, which the checker resolves to
119
+ // the literals it admits and so no longer names the type the key indexes.
120
+ const symbols = queries.symbolsAt(named.map((one) => one.name));
121
+ const keyed = named.flatMap((one, at) => {
122
+ const asked = symbols[at];
123
+ const symbol =
124
+ asked === UNANSWERED || asked === undefined || (asked.flags & SymbolFlags.Alias) === 0
125
+ ? asked
126
+ : queries.aliased(asked);
127
+ if (symbol === UNANSWERED) {
128
+ const text = literalText(one.other);
129
+ found.push({
130
+ kind: "held",
131
+ from: one.from,
132
+ ...(text === undefined ? {} : { names: [text] }),
117
133
  });
134
+ return [];
135
+ }
136
+ if (symbol === undefined) {
137
+ return [];
118
138
  }
139
+ const operands = symbol.declarations.flatMap((handle) => {
140
+ const declared = (
141
+ project.declarationAt(handle)?.node as { readonly type?: TypeNode } | undefined
142
+ )?.type;
143
+ return declared === undefined ? [] : keyofOperands(declared);
144
+ });
145
+ return operands.length === 0 ? [] : [{ ...one, operands }];
146
+ });
147
+ if (keyed.length === 0) {
148
+ return found;
119
149
  }
120
- }
121
- node.forEachChild((child) => {
122
- visit(child, holder);
123
- });
124
- };
125
- file.forEachChild((child) => {
126
- visit(child, fileId);
127
- });
128
- }
129
- const named = comparisons.flatMap((one) => {
130
- const name = nameOf(one.keyed);
131
- return name === undefined || isElementAccessExpression(parenthesesRemoved(one.other))
132
- ? []
133
- : [{ ...one, name }];
134
- });
135
- if (named.length === 0) {
136
- return [];
137
- }
138
-
139
- // The operand's own declaration states the key type, which the checker resolves to
140
- // the literals it admits and so no longer names the type the key indexes.
141
- const symbols = queries.symbolsAt(named.map((one) => one.name));
142
- const keyed = named.flatMap((one, at) => {
143
- const asked = symbols[at];
144
- const symbol =
145
- asked === UNANSWERED || asked === undefined || (asked.flags & SymbolFlags.Alias) === 0
146
- ? asked
147
- : queries.aliased(asked);
148
- if (symbol === UNANSWERED || symbol === undefined) {
149
- return [];
150
- }
151
- const operands = symbol.declarations.flatMap((handle) => {
152
- const declared = (
153
- project.declarationAt(handle)?.node as { readonly type?: TypeNode } | undefined
154
- )?.type;
155
- return declared === undefined ? [] : keyofOperands(declared);
156
- });
157
- return operands.length === 0 ? [] : [{ ...one, operands }];
158
- });
159
- if (keyed.length === 0) {
160
- return [];
161
- }
162
- const nodes = keyed.flatMap((one) => [one.other, ...one.operands]);
163
- const types = queries.typesAt(nodes);
164
- const typeOf = new Map<Node, Type>();
165
- nodes.forEach((node, at) => {
166
- const type = types[at];
167
- if (type !== UNANSWERED && type !== undefined) {
168
- typeOf.set(node, type);
169
- }
170
- });
171
- const partsOf = (type: Type): readonly Type[] => {
172
- const parts = queries.constituents(type);
173
- return parts === UNANSWERED ? [type] : parts;
174
- };
175
-
176
- const found: Reference[] = [];
177
- for (const one of keyed) {
178
- const literal = typeOf.get(one.other);
179
- const names = literal === undefined ? [] : literalNames(partsOf(literal));
180
- if (names.length === 0) {
181
- continue;
182
- }
183
- const position = renderPosition(one.file, root, one.other.getStart());
184
- for (const operand of one.operands) {
185
- const object = typeOf.get(operand);
186
- for (const part of object === undefined ? [] : partsOf(object)) {
187
- const properties = queries.propertiesOf(part);
188
- for (const property of properties === UNANSWERED ? [] : properties) {
189
- if (!names.includes(property.name)) {
190
- continue;
150
+ const nodes = keyed.flatMap((one) => [one.other, ...one.operands]);
151
+ const types = queries.typesAt(nodes);
152
+ const typeOf = new Map<Node, Type | typeof UNANSWERED>();
153
+ nodes.forEach((node, at) => {
154
+ const type = types[at];
155
+ if (type !== undefined) {
156
+ typeOf.set(node, type);
191
157
  }
192
- for (const to of chains.declarationsOf(property)) {
158
+ });
159
+ const partsOf = (type: Type): readonly Type[] => {
160
+ const parts = queries.constituents(type);
161
+ return parts === UNANSWERED ? [type] : parts;
162
+ };
163
+ for (const one of keyed) {
164
+ const literal = typeOf.get(one.other);
165
+ const objects = one.operands.map((operand) => typeOf.get(operand));
166
+ if (literal === UNANSWERED) {
167
+ const known = objects.filter((type) => type !== undefined && type !== UNANSWERED);
193
168
  found.push({
169
+ kind: "held",
194
170
  from: one.from,
195
- to,
196
- position,
197
- use: "read",
198
- resolution: "keyed",
199
- test: testFiles.has(position.path),
171
+ types: known.flatMap(partsOf),
172
+ ...(known.length < objects.length ? { members: true as const } : {}),
200
173
  });
174
+ continue;
175
+ }
176
+ const names = literal === undefined ? [] : literalNames(partsOf(literal));
177
+ if (names.length === 0) {
178
+ continue;
179
+ }
180
+ for (const object of objects) {
181
+ if (object === UNANSWERED) {
182
+ found.push({ kind: "held", from: one.from, names });
183
+ continue;
184
+ }
185
+ for (const part of object === undefined ? [] : partsOf(object)) {
186
+ const properties = queries.propertiesOf(part);
187
+ if (properties === UNANSWERED) {
188
+ found.push({ kind: "held", from: one.from, names });
189
+ continue;
190
+ }
191
+ for (const property of properties) {
192
+ if (!names.includes(property.name)) {
193
+ continue;
194
+ }
195
+ for (const to of chains.declarationsOf(property)) {
196
+ found.push({
197
+ kind: "named",
198
+ from: one.from,
199
+ to,
200
+ file: one.file,
201
+ at: one.other,
202
+ use: "read",
203
+ via: "keyed",
204
+ });
205
+ }
206
+ }
207
+ }
201
208
  }
202
209
  }
203
- }
204
- }
205
- }
206
- return found;
210
+ return found;
211
+ },
212
+ };
213
+ },
214
+ };
215
+
216
+ /** The text a string or number literal operand spells, where it is one. */
217
+ function literalText(node: Expression): string | undefined {
218
+ const at = parenthesesRemoved(node);
219
+ return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at)
220
+ ? at.text
221
+ : undefined;
207
222
  }
@@ -8,7 +8,13 @@ import { packageOfSpecifier } from "../dependencies.ts";
8
8
  import { MANIFEST } from "../fs/manifest.ts";
9
9
  import { entriesOf } from "../fs/tree.ts";
10
10
  import type { Host } from "../host.ts";
11
- import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "../paths.ts";
11
+ import {
12
+ cannotStartPackageName,
13
+ dirnamePath,
14
+ joinPath,
15
+ relativePath,
16
+ resolvePath,
17
+ } from "../paths.ts";
12
18
  import { dependencySpecifiers, installedPackage, manifestsAbove } from "./module-lookup.ts";
13
19
  import type { CompilerOptions } from "./plan.ts";
14
20
 
@@ -119,7 +125,7 @@ function expectedModule(
119
125
  ): string | undefined {
120
126
  const { host, targetRoot } = context;
121
127
  const fix = "run the generator or the build that writes it";
122
- if (specifier.startsWith(".") || isAbsolutePath(specifier)) {
128
+ if (cannotStartPackageName(specifier)) {
123
129
  return provided(host, resolvePath(dirnamePath(importer), specifier), false)
124
130
  ? undefined
125
131
  : `which names no file of the target: ${fix}`;
@@ -23,14 +23,17 @@ import type { Failure } from "../failure.ts";
23
23
  import { readManifestAt } from "../fs/manifest.ts";
24
24
  import type { Host } from "../host.ts";
25
25
  import { JAVASCRIPT_FILE } from "../javascript-file.ts";
26
- import { ancestors, dirnamePath, relativePath, resolvePath } from "../paths.ts";
26
+ import {
27
+ ancestors,
28
+ dirnamePath,
29
+ isRelativeSpecifier,
30
+ relativePath,
31
+ resolvePath,
32
+ } from "../paths.ts";
27
33
  import { renderPosition, type Position } from "../position.ts";
28
34
  import { batched, type Engine, type ProjectView } from "../session.ts";
29
35
  import { dependencySpecifiers } from "./module-lookup.ts";
30
36
 
31
- /** A relative specifier: `.`, `..`, or one starting `./` or `../`. */
32
- const RELATIVE = /^\.\.?(?:\/|$)/u;
33
-
34
37
  /** One relative specifier of a JavaScript file that resolves to no file. */
35
38
  export interface UnresolvedSpecifier {
36
39
  readonly specifier: string;
@@ -141,7 +144,7 @@ export function readSpecifiers<Brand>(
141
144
  const resolved = (answers[at]?.resolvedFileName ?? "") !== "";
142
145
  const specifier = usage.node.text;
143
146
  const name = packageOfSpecifier(specifier);
144
- if (RELATIVE.test(specifier)) {
147
+ if (isRelativeSpecifier(specifier)) {
145
148
  const named = resolvePath(dirnamePath(usage.file.fileName), specifier);
146
149
  if (!resolved && !outputs.some((dir) => relativePath(dir, named) !== undefined)) {
147
150
  unresolved.push({
@@ -132,6 +132,12 @@ const RUNTIME_OPTIONS = {
132
132
  node: { module: "nodenext", moduleResolution: "nodenext" },
133
133
  } as const satisfies Record<Runtime, Readonly<Record<string, string>>>;
134
134
 
135
+ /** Whether one configuration is a program the run formed to resolve its modules as Node does. */
136
+ export function formedForNode(configuration: Configuration): boolean {
137
+ const options = configuration.written?.["compilerOptions"];
138
+ return isRecord(options) && options["moduleResolution"] === RUNTIME_OPTIONS.node.moduleResolution;
139
+ }
140
+
135
141
  /** The manifest members that say a package is written for Node. */
136
142
  const NODE_MEMBERS = ["main", "exports", "bin", "type"] as const;
137
143
 
@@ -10,7 +10,14 @@ import { emittedFrom } from "./emit-map.ts";
10
10
  import { packageOfSpecifier } from "../dependencies.ts";
11
11
  import { gitIgnored } from "../git-ignore.ts";
12
12
  import type { Host } from "../host.ts";
13
- import { ancestors, dirnamePath, joinPath, relativePath, resolvePath } from "../paths.ts";
13
+ import {
14
+ ancestors,
15
+ cannotStartPackageName,
16
+ dirnamePath,
17
+ joinPath,
18
+ relativePath,
19
+ resolvePath,
20
+ } from "../paths.ts";
14
21
  import { satisfies } from "../version-range.ts";
15
22
  import type { Workspace, WorkspaceMember } from "./workspace.ts";
16
23
  import { isRecord } from "../json-document.ts";
@@ -140,7 +147,7 @@ function dependsOn(specifier: string, member: WorkspaceMember, fromDir: string):
140
147
  member.version !== undefined && satisfies(member.version, range);
141
148
  if (specifier.startsWith("workspace:")) {
142
149
  const rest = specifier.slice("workspace:".length);
143
- if (rest.startsWith(".") || rest.startsWith("/")) {
150
+ if (cannotStartPackageName(rest)) {
144
151
  return atPath(rest);
145
152
  }
146
153
  const alias = /^((?:@[^/@]+\/)?[^/@]+)@(.*)$/u.exec(rest);
package/src/matrix.ts CHANGED
@@ -10,8 +10,10 @@ import type { InventorySymbol } from "./inventory.ts";
10
10
  import { byPosition } from "./position.ts";
11
11
  import type { Reference } from "./references.ts";
12
12
  import type { Root } from "./roots/root-set.ts";
13
+ import { edgeWithheldBy } from "./tests/file-class.ts";
13
14
  import {
14
15
  reachedWithTheTests,
16
+ splitRefs,
15
17
  sweep,
16
18
  type Candidate,
17
19
  type Liveness,
@@ -270,13 +272,14 @@ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
270
272
  }
271
273
  const held = matrix.heldIn[at] ?? [];
272
274
  const admitted = admittedAt(at);
273
- const testRefs = union.made[at]?.test ?? 0;
275
+ const split = splitRefs(union.made[at], input.mode);
276
+ const testRefs = split.test;
274
277
  const heldByTests =
275
278
  heldBy[union.parent[at] ?? OUTSIDE] ??
276
279
  (union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
277
280
  const unreferenced = union.test[at] === true && !admitted && testReached[at] !== true;
278
281
  heldBy[at] = heldByTests;
279
- const productionRefs = union.made[at]?.production ?? 0;
282
+ const productionRefs = split.production;
280
283
  const counted = all.every((candidate) => candidate.relation === "reference-counting");
281
284
  candidates.push({
282
285
  id: symbol.id,
@@ -325,7 +328,7 @@ function compiledReferences(
325
328
  }
326
329
  for (const edge of edges) {
327
330
  const to = graph.symbols[edge.to]?.id ?? "";
328
- if ((!mode.production || !edge.test) && dead.has(to) && !kept.has(to)) {
331
+ if (!edgeWithheldBy(mode, edge) && dead.has(to) && !kept.has(to)) {
329
332
  kept.add(to);
330
333
  changed = true;
331
334
  }
@@ -15,20 +15,18 @@ import {
15
15
  isImportTypeNode,
16
16
  isLiteralTypeNode,
17
17
  isModuleDeclaration,
18
- isNamespaceExport,
19
- isNamespaceImport,
20
18
  isNoSubstitutionTemplateLiteral,
21
19
  isStringLiteral,
22
20
  SyntaxKind,
23
21
  type Node,
24
22
  type SourceFile,
25
23
  } from "@typescript/native/ast";
24
+ import { namesImported } from "./import-bindings.ts";
26
25
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
27
- import { renderPosition } from "./position.ts";
28
26
  import { UNANSWERED } from "./query.ts";
29
27
  import { aliasFragment, nameComponent } from "./ref.ts";
30
- import type { Reference } from "./references.ts";
31
- import type { ProjectView } from "./session.ts";
28
+ import type { Read, ReadRule } from "./references/read-rule.ts";
29
+ import { isPathSpecifier } from "./paths.ts";
32
30
 
33
31
  /** Whether a node is a specifier's text: a string literal or a template with no substitution. */
34
32
  function literalText(node: Node | undefined): node is Node & { readonly text: string } {
@@ -58,33 +56,12 @@ function specifierOf(node: Node): (Node & { readonly text: string }) | undefined
58
56
  return undefined;
59
57
  }
60
58
 
61
- /**
62
- * The export names an import or a re-export reads from the module it names: none for a
63
- * namespace, a star or a bare form.
64
- */
65
- export function namesImported(node: Node): readonly string[] {
66
- if (isImportDeclaration(node)) {
67
- const clause = node.importClause;
68
- const bindings = clause?.namedBindings;
69
- return [
70
- ...(clause?.name === undefined ? [] : ["default"]),
71
- ...(bindings === undefined || isNamespaceImport(bindings)
72
- ? []
73
- : bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
74
- ];
75
- }
76
- const clause = isExportDeclaration(node) ? node.exportClause : undefined;
77
- return clause === undefined || isNamespaceExport(clause)
78
- ? []
79
- : clause.elements.map((one) => (one.propertyName ?? one.name).text);
80
- }
81
-
82
59
  /**
83
60
  * Whether a specifier matches a declaration's name, the name's one `*` standing for any run.
84
61
  * A relative name is resolved against its own file, so no specifier names it by its text.
85
62
  */
86
63
  function namesModule(specifier: string, name: string): boolean {
87
- if (/^(?:\.{1,2}(?:\/|$)|\/)/u.test(name)) {
64
+ if (isPathSpecifier(name)) {
88
65
  return false;
89
66
  }
90
67
  const star = name.indexOf("*");
@@ -161,76 +138,75 @@ interface Declared {
161
138
  }
162
139
 
163
140
  /**
164
- * The references the imports of one project's own files make to the module declarations
165
- * they name, each at the import's specifier.
141
+ * The reads the imports of a side's files make of the module declarations of the target they
142
+ * name, each at the import's specifier. A specifier the checker leaves unanswered still names
143
+ * a declaration by its name or pattern.
166
144
  */
167
- export function moduleDeclarationReferences<Brand>(
168
- project: ProjectView<Brand>,
169
- held: Inventory,
170
- root: string,
171
- testFiles: ReadonlySet<string>,
172
- ): readonly Reference[] {
173
- const imports: Import[] = [];
174
- const declared: Declared[] = [];
175
- for (const file of project.ownSourceFiles()) {
176
- const fileId = held.declarations.get(nodeKey(file, file));
177
- if (fileId === undefined) {
178
- continue;
179
- }
180
- const visit = (node: Node, from: string): void => {
181
- const own = held.declarations.get(nodeKey(file, node));
182
- if (isModuleDeclaration(node) && literalText(node.name) && own !== undefined) {
183
- declared.push({ id: own, name: node.name });
184
- }
185
- const specifier = specifierOf(node);
186
- if (specifier !== undefined) {
187
- imports.push({ file, from, specifier, names: namesImported(node) });
188
- }
189
- node.forEachChild((child) => {
190
- visit(child, own ?? from);
191
- });
192
- };
193
- file.forEachChild((child) => {
194
- visit(child, fileId);
195
- });
196
- }
197
- if (declared.length === 0 || imports.length === 0) {
198
- return [];
199
- }
200
-
201
- const asked = [...declared.map((one) => one.name), ...imports.map((one) => one.specifier)];
202
- const answers = project.symbolsAt(asked.map((node) => project.handle(node)));
203
- const moduleOf = (at: number): number | undefined => {
204
- const symbol = answers[at];
205
- return symbol === undefined || symbol === UNANSWERED ? undefined : symbol.id;
206
- };
207
-
208
- const named = namedBy(held);
209
- const found: Reference[] = [];
210
- imports.forEach((one, index) => {
211
- const resolved = moduleOf(declared.length + index);
212
- const position = renderPosition(one.file, root, one.specifier.getStart());
213
- const read = (to: string, resolution: Reference["resolution"], test: boolean): void => {
214
- found.push({ from: one.from, to, position, use: "read", resolution, test });
215
- };
216
- const test = testFiles.has(position.path);
217
- const same = new Set(
218
- declared
219
- .filter((_, at) => resolved !== undefined && resolved === moduleOf(at))
220
- .map((declaration) => declaration.id),
221
- );
222
- for (const id of same) {
223
- read(id, "batch", test);
224
- }
225
- for (const declaration of named(one.specifier.text, one.names)) {
226
- // A wildcard declaration is how the compiler types the file a matching specifier
227
- // names, so the import uses it whoever writes it, a test included.
228
- if (declaration.wildcard || !same.has(declaration.id)) {
229
- for (const to of [declaration.id, ...declaration.members]) {
230
- read(to, "syntax", test && !declaration.wildcard);
145
+ export const moduleDeclarations: ReadRule = {
146
+ start() {
147
+ const imports: Import[] = [];
148
+ return {
149
+ visit(node, { file, from }) {
150
+ const specifier = specifierOf(node);
151
+ if (specifier !== undefined) {
152
+ imports.push({ file, from, specifier, names: namesImported(node) });
231
153
  }
232
- }
233
- }
234
- });
235
- return found;
236
- }
154
+ },
155
+ reads({ project, held }) {
156
+ const found: Read[] = [];
157
+ // A module declaration whose name is a string is written at a file's top level.
158
+ const declared: Declared[] = project.ownSourceFiles().flatMap((file) =>
159
+ file.statements.flatMap((statement) => {
160
+ const id = held.declarations.get(nodeKey(file, statement));
161
+ return isModuleDeclaration(statement) && literalText(statement.name) && id !== undefined
162
+ ? [{ id, name: statement.name }]
163
+ : [];
164
+ }),
165
+ );
166
+ if (declared.length === 0 || imports.length === 0) {
167
+ return found;
168
+ }
169
+ const asked = [...declared.map((one) => one.name), ...imports.map((one) => one.specifier)];
170
+ const answers = project.symbolsAt(asked.map((node) => project.handle(node)));
171
+ const moduleOf = (at: number): number | undefined => {
172
+ const symbol = answers[at];
173
+ return symbol === undefined || symbol === UNANSWERED ? undefined : symbol.id;
174
+ };
175
+ const named = namedBy(held);
176
+ imports.forEach((one, index) => {
177
+ const resolved = moduleOf(declared.length + index);
178
+ const read = (to: string, via: "batch" | "syntax", production: boolean): void => {
179
+ found.push({
180
+ kind: "named",
181
+ from: one.from,
182
+ to,
183
+ file: one.file,
184
+ at: one.specifier,
185
+ use: "read",
186
+ via,
187
+ ...(production ? { production: true as const } : {}),
188
+ });
189
+ };
190
+ const same = new Set(
191
+ declared
192
+ .filter((_, at) => resolved !== undefined && resolved === moduleOf(at))
193
+ .map((declaration) => declaration.id),
194
+ );
195
+ for (const id of same) {
196
+ read(id, "batch", false);
197
+ }
198
+ for (const declaration of named(one.specifier.text, one.names)) {
199
+ // A wildcard declaration is how the compiler types the file a matching specifier
200
+ // names, so the import uses it whoever writes it, a test included.
201
+ if (declaration.wildcard || !same.has(declaration.id)) {
202
+ for (const to of [declaration.id, ...declaration.members]) {
203
+ read(to, "syntax", declaration.wildcard);
204
+ }
205
+ }
206
+ }
207
+ });
208
+ return found;
209
+ },
210
+ };
211
+ },
212
+ };