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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/dist/src/alias-chain.js +0 -5
  2. package/dist/src/amd.js +81 -0
  3. package/dist/src/analysis.js +82 -91
  4. package/dist/src/augmentations.js +24 -25
  5. package/dist/src/bindings.js +382 -0
  6. package/dist/src/calls.js +279 -43
  7. package/dist/src/commands/tools.js +5 -9
  8. package/dist/src/component-files.js +3 -2
  9. package/dist/src/conformance-record.js +1 -1
  10. package/dist/src/consumers.js +6 -9
  11. package/dist/src/contextual-uses.js +1 -17
  12. package/dist/src/contract/config.generated.js +4 -1
  13. package/dist/src/contract/contract.generated.js +2 -2
  14. package/dist/src/contract/report.generated.js +6 -2
  15. package/dist/src/dependencies.js +25 -29
  16. package/dist/src/destructuring.js +1 -19
  17. package/dist/src/element-access.js +179 -164
  18. package/dist/src/entries/build-scripts.js +2 -2
  19. package/dist/src/entries/configuration-files.js +3 -7
  20. package/dist/src/entries/conventions.js +2 -2
  21. package/dist/src/entries/html-entries.js +7 -10
  22. package/dist/src/entries/mdx-entries.js +2 -2
  23. package/dist/src/entries/static-module.js +2 -1
  24. package/dist/src/exempt.js +0 -26
  25. package/dist/src/exposure.js +400 -0
  26. package/dist/src/facts.js +4 -4
  27. package/dist/src/findings/completion.js +44 -21
  28. package/dist/src/findings/emitters.js +22 -33
  29. package/dist/src/{implementations.js → findings/implementations.js} +3 -3
  30. package/dist/src/{in-operands.js → findings/in-operands.js} +2 -2
  31. package/dist/src/findings/intra-function.js +3 -3
  32. package/dist/src/findings/reads-and-writes.js +71 -15
  33. package/dist/src/findings/unused-declarations.js +2 -22
  34. package/dist/src/findings/visibility-narrowing.js +14 -20
  35. package/dist/src/framework-lifecycle.js +41 -14
  36. package/dist/src/import-bindings.js +73 -28
  37. package/dist/src/index/sites.js +557 -0
  38. package/dist/src/indexed-access.js +523 -478
  39. package/dist/src/interface-satisfaction.js +1 -1
  40. package/dist/src/inventory.js +18 -0
  41. package/dist/src/keyof-comparisons.js +124 -103
  42. package/dist/src/{components.js → liveness/components.js} +1 -1
  43. package/dist/src/{graph.js → liveness/graph.js} +45 -20
  44. package/dist/src/liveness/matrix.js +99 -0
  45. package/dist/src/liveness/sweep.js +558 -0
  46. package/dist/src/load/expected-module.js +2 -2
  47. package/dist/src/load/specifiers.js +2 -4
  48. package/dist/src/load/synthesize.js +5 -0
  49. package/dist/src/load/workspace-resolution.js +2 -2
  50. package/dist/src/module-declarations.js +70 -85
  51. package/dist/src/overrides.js +134 -112
  52. package/dist/src/paths.js +46 -7
  53. package/dist/src/platform-calls.js +2 -2
  54. package/dist/src/platform-symbols.js +13 -19
  55. package/dist/src/query.js +46 -0
  56. package/dist/src/references/read-rule.js +1 -0
  57. package/dist/src/references/read-rules.js +48 -0
  58. package/dist/src/references/sink.js +237 -0
  59. package/dist/src/references/sources.js +450 -0
  60. package/dist/src/references.js +110 -1285
  61. package/dist/src/roots/configured-declarations.js +3 -0
  62. package/dist/src/roots/published-reach.js +4 -4
  63. package/dist/src/roots/root-set.js +1 -1
  64. package/dist/src/roots/unseen-loads.js +38 -10
  65. package/dist/src/serialization-contract.js +8 -3
  66. package/dist/src/stores.js +4 -2
  67. package/dist/src/symbol-keys.js +89 -83
  68. package/dist/src/tests/file-class.js +76 -0
  69. package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
  70. package/dist/src/{test-support.js → tests/support.js} +2 -11
  71. package/dist/src/type-parameter-reads.js +178 -139
  72. package/dist/src/unimplemented-callees.js +262 -242
  73. package/dist/src/verbs/analyze.js +3 -3
  74. package/dist/src/verbs/explain.js +75 -132
  75. package/dist/src/verbs/print-retained.js +3 -3
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/alias-chain.ts +0 -7
  79. package/src/amd.ts +130 -0
  80. package/src/analysis.ts +125 -128
  81. package/src/augmentations.ts +42 -28
  82. package/src/bindings.ts +460 -0
  83. package/src/calls.ts +338 -46
  84. package/src/commands/tools.ts +5 -11
  85. package/src/component-files.ts +4 -2
  86. package/src/conformance-record.ts +1 -1
  87. package/src/consumers.ts +8 -11
  88. package/src/contextual-uses.ts +2 -22
  89. package/src/contract/config.generated.ts +4 -1
  90. package/src/contract/contract.generated.ts +2 -2
  91. package/src/contract/report.generated.ts +6 -2
  92. package/src/dependencies.ts +27 -28
  93. package/src/destructuring.ts +1 -31
  94. package/src/element-access.ts +188 -182
  95. package/src/entries/build-scripts.ts +2 -2
  96. package/src/entries/configuration-files.ts +11 -8
  97. package/src/entries/conventions.ts +2 -1
  98. package/src/entries/html-entries.ts +15 -14
  99. package/src/entries/mdx-entries.ts +2 -2
  100. package/src/entries/static-module.ts +2 -1
  101. package/src/exempt.ts +31 -46
  102. package/src/exposure.ts +492 -0
  103. package/src/facts.ts +5 -3
  104. package/src/finding.ts +1 -1
  105. package/src/findings/boundary.ts +9 -0
  106. package/src/findings/completion.ts +48 -23
  107. package/src/findings/emitter.ts +1 -1
  108. package/src/findings/emitters.ts +23 -41
  109. package/src/{implementations.ts → findings/implementations.ts} +4 -4
  110. package/src/{in-operands.ts → findings/in-operands.ts} +3 -3
  111. package/src/findings/interfaces.ts +3 -3
  112. package/src/findings/intra-function.ts +3 -3
  113. package/src/findings/reads-and-writes.ts +99 -26
  114. package/src/findings/unused-declarations.ts +4 -28
  115. package/src/findings/visibility-narrowing.ts +25 -26
  116. package/src/framework-lifecycle.ts +42 -14
  117. package/src/import-bindings.ts +98 -29
  118. package/src/index/sites.ts +781 -0
  119. package/src/indexed-access.ts +570 -527
  120. package/src/interface-satisfaction.ts +2 -2
  121. package/src/inventory.ts +19 -0
  122. package/src/keyof-comparisons.ts +131 -116
  123. package/src/{components.ts → liveness/components.ts} +3 -3
  124. package/src/{graph.ts → liveness/graph.ts} +93 -27
  125. package/src/liveness/matrix.ts +162 -0
  126. package/src/liveness/sweep.ts +855 -0
  127. package/src/load/expected-module.ts +8 -2
  128. package/src/load/specifiers.ts +8 -5
  129. package/src/load/synthesize.ts +6 -0
  130. package/src/load/workspace-resolution.ts +9 -2
  131. package/src/module-declarations.ts +74 -98
  132. package/src/overrides.ts +162 -134
  133. package/src/paths.ts +54 -8
  134. package/src/platform-calls.ts +3 -3
  135. package/src/platform-symbols.ts +11 -27
  136. package/src/query.ts +81 -0
  137. package/src/references/read-rule.ts +60 -0
  138. package/src/references/read-rules.ts +55 -0
  139. package/src/references/sink.ts +314 -0
  140. package/src/references/sources.ts +632 -0
  141. package/src/references.ts +175 -1733
  142. package/src/report.ts +1 -1
  143. package/src/roots/configured-declarations.ts +3 -0
  144. package/src/roots/published-reach.ts +9 -4
  145. package/src/roots/root-set.ts +1 -1
  146. package/src/roots/unseen-loads.ts +49 -11
  147. package/src/serialization-contract.ts +8 -3
  148. package/src/stores.ts +15 -3
  149. package/src/symbol-keys.ts +102 -102
  150. package/src/tests/file-class.ts +135 -0
  151. package/src/{outside-tests.ts → tests/outside.ts} +16 -10
  152. package/src/{test-support.ts → tests/support.ts} +9 -20
  153. package/src/type-parameter-reads.ts +187 -158
  154. package/src/unimplemented-callees.ts +297 -280
  155. package/src/verbs/analyze.ts +3 -2
  156. package/src/verbs/explain.ts +83 -158
  157. package/src/verbs/print-retained.ts +3 -3
  158. package/dist/src/matrix.js +0 -273
  159. package/dist/src/sweep.js +0 -222
  160. package/src/matrix.ts +0 -463
  161. package/src/sweep.ts +0 -361
@@ -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);
@@ -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
+ };
package/src/overrides.ts CHANGED
@@ -15,14 +15,14 @@ import {
15
15
  SyntaxKind,
16
16
  type HeritageClauseElement,
17
17
  type Node,
18
+ type SourceFile,
18
19
  } from "@typescript/native/ast";
19
- import type { AliasChains } from "./alias-chain.ts";
20
20
  import { memberComponent } from "./class-members.ts";
21
- import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
21
+ import { nodeKey, type InventorySymbol } from "./inventory.ts";
22
22
  import { SymbolFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/sync";
23
23
  import { UNANSWERED } from "./query.ts";
24
24
  import { nameComponent } from "./ref.ts";
25
- import type { Reference } from "./references.ts";
25
+ import type { Read, ReadRule } from "./references/read-rule.ts";
26
26
  import type { ProjectView } from "./session.ts";
27
27
 
28
28
  const CLASS_DECLARATIONS: ReadonlySet<SyntaxKind> = new Set([
@@ -41,141 +41,157 @@ function nameOf(entry: HeritageClauseElement): Node {
41
41
  return isQualifiedName(entry.typeName) ? entry.typeName.right : entry.typeName;
42
42
  }
43
43
 
44
- /** Each class of the target whose `extends` clause names a class, with the name node. */
45
- function extending<Brand>(
46
- project: ProjectView<Brand>,
47
- held: Inventory,
48
- ): readonly { readonly id: string; readonly base: Node }[] {
49
- const found: { id: string; base: Node }[] = [];
50
- for (const file of project.ownSourceFiles()) {
51
- const visit = (node: Node): void => {
52
- if (isClassDeclaration(node) || isClassExpression(node)) {
53
- const id = held.declarations.get(nodeKey(file, node));
54
- const clause = node.heritageClauses?.find((one) => one.token === SyntaxKind.ExtendsKeyword);
55
- const base = clause?.types[0];
56
- if (id !== undefined && base !== undefined) {
57
- found.push({ id, base: nameOf(base) });
58
- }
59
- }
60
- node.forEachChild(visit);
61
- };
62
- file.forEachChild(visit);
63
- }
64
- return found;
44
+ /** One class whose `extends` clause names a class, with the name node and its file. */
45
+ interface Extending {
46
+ readonly node: Node;
47
+ readonly file: SourceFile;
48
+ readonly base: Node;
65
49
  }
66
50
 
67
51
  /**
68
- * The references each base class member makes to its overrides, for the classes of one
69
- * project that extend another class of the target, and each class makes to its members
70
- * that override a member of a class declared outside the target. A member a nearer base
71
- * already overrides is reached through that override. A private member is not
72
- * overridden. Each reference is placed at the override and made by its file.
52
+ * The reads each base class member makes of its overrides, for the classes of the target that
53
+ * extend another class of the target, and each class makes of its members that override a
54
+ * member of a class declared outside the target. A member a nearer base already overrides is
55
+ * reached through that override. A private member is not overridden. Each read is placed at
56
+ * the override. A class whose base the checker leaves unanswered holds every member it could
57
+ * override.
73
58
  */
74
- export function overrideReferences<Brand>(
75
- project: ProjectView<Brand>,
76
- held: Inventory,
77
- chains: AliasChains,
78
- testFiles: ReadonlySet<string>,
79
- ): readonly Reference[] {
80
- const classes = extending(project, held);
81
- if (classes.length === 0) {
82
- return [];
83
- }
84
- const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
85
- const members = new Map<string, Map<string, InventorySymbol>>();
86
- for (const symbol of held.symbols) {
87
- const component = memberComponent(symbol, byId);
88
- if (
89
- component === undefined ||
90
- !OVERRIDABLE.has(symbol.kind) ||
91
- symbol.visibility === "private" ||
92
- symbol.visibility === "private-name"
93
- ) {
94
- continue;
95
- }
96
- const own = members.get(symbol.parent) ?? new Map<string, InventorySymbol>();
97
- own.set(component, symbol);
98
- members.set(symbol.parent, own);
99
- }
100
- const bases = new Map<string, readonly string[]>();
101
- const outsideBases = new Map<string, TSSymbol>();
102
- const own = project.ownPaths();
103
- const symbols = project.symbolsAt(classes.map((one) => project.handle(one.base)));
104
- classes.forEach(({ id }, index) => {
105
- const symbol = symbols[index];
106
- if (symbol !== undefined && symbol !== UNANSWERED) {
107
- const inTarget = chains
108
- .chainOf(symbol)
109
- .map((target) => target.id)
110
- .filter((target) => byId.get(target)?.kind === "class" && target !== id);
111
- bases.set(id, inTarget);
112
- const declared =
113
- (symbol.flags & SymbolFlags.Alias) === 0 ? symbol : project.queries.aliased(symbol);
114
- if (
115
- inTarget.length === 0 &&
116
- declared !== UNANSWERED &&
117
- (((declared.flags & SymbolFlags.Class) !== 0 &&
118
- // The target's augmentation of an outside class merges an interface into it,
119
- // so only a class declaration says where the class is.
120
- declared.declarations.some(
121
- (handle) => CLASS_DECLARATIONS.has(handle.kind) && !own.has(handle.path),
122
- )) ||
123
- ((declared.flags & SymbolFlags.Interface) !== 0 &&
124
- declared.declarations.some(
125
- (handle) => handle.kind === SyntaxKind.VariableDeclaration && !own.has(handle.path),
126
- )))
127
- ) {
128
- outsideBases.set(id, declared);
129
- }
130
- }
131
- });
132
-
133
- /** The nearest member of one component the class or a class it extends declares. */
134
- const declaredAbove = (classId: string, component: string, seen: Set<string>): string[] => {
135
- if (seen.has(classId)) {
136
- return [];
137
- }
138
- seen.add(classId);
139
- const own = members.get(classId)?.get(component);
140
- if (own !== undefined) {
141
- return [own.id];
142
- }
143
- return (bases.get(classId) ?? []).flatMap((base) => declaredAbove(base, component, seen));
144
- };
145
-
146
- const found: Reference[] = [];
147
- const outsideNames = outsideMemberNames(project, bases, outsideBases);
148
- for (const [classId, names] of outsideNames) {
149
- for (const [component, member] of members.get(classId) ?? []) {
150
- if (names.has(component)) {
151
- found.push({
152
- from: classId,
153
- to: member.id,
154
- position: member.position,
155
- use: "read",
156
- resolution: "override",
157
- test: testFiles.has(member.position.path),
59
+ export const overrides: ReadRule = {
60
+ start() {
61
+ const extending: Extending[] = [];
62
+ return {
63
+ visit(node, { file }) {
64
+ if (isClassDeclaration(node) || isClassExpression(node)) {
65
+ const clause = node.heritageClauses?.find(
66
+ (one) => one.token === SyntaxKind.ExtendsKeyword,
67
+ );
68
+ const base = clause?.types[0];
69
+ if (base !== undefined) {
70
+ extending.push({ node, file, base: nameOf(base) });
71
+ }
72
+ }
73
+ },
74
+ reads({ project, held, chains }) {
75
+ const found: Read[] = [];
76
+ const classes = extending.flatMap((one) => {
77
+ const id = held.declarations.get(nodeKey(one.file, one.node));
78
+ return id === undefined ? [] : [{ ...one, id }];
158
79
  });
159
- }
160
- }
161
- }
162
- for (const [classId, baseIds] of bases) {
163
- for (const [component, member] of members.get(classId) ?? []) {
164
- const seen = new Set([classId]);
165
- for (const base of baseIds.flatMap((one) => declaredAbove(one, component, seen))) {
166
- found.push({
167
- from: base,
168
- to: member.id,
169
- position: member.position,
170
- use: "read",
171
- resolution: "override",
172
- test: testFiles.has(member.position.path),
80
+ if (classes.length === 0) {
81
+ return found;
82
+ }
83
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
84
+ const members = new Map<string, Map<string, InventorySymbol>>();
85
+ for (const symbol of held.symbols) {
86
+ const component = memberComponent(symbol, byId);
87
+ if (
88
+ component === undefined ||
89
+ !OVERRIDABLE.has(symbol.kind) ||
90
+ symbol.visibility === "private" ||
91
+ symbol.visibility === "private-name"
92
+ ) {
93
+ continue;
94
+ }
95
+ const own = members.get(symbol.parent) ?? new Map<string, InventorySymbol>();
96
+ own.set(component, symbol);
97
+ members.set(symbol.parent, own);
98
+ }
99
+ const bases = new Map<string, readonly string[]>();
100
+ const outsideBases = new Map<string, TSSymbol>();
101
+ const fileOf = new Map(classes.map((one) => [one.id, one.file]));
102
+ const own = project.ownPaths();
103
+ const unanswered = new Set<string>();
104
+ const symbols = project.symbolsAt(classes.map((one) => project.handle(one.base)));
105
+ classes.forEach(({ id }, index) => {
106
+ const symbol = symbols[index];
107
+ if (symbol === UNANSWERED) {
108
+ unanswered.add(id);
109
+ return;
110
+ }
111
+ if (symbol === undefined) {
112
+ return;
113
+ }
114
+ const inTarget = chains
115
+ .chainOf(symbol)
116
+ .map((target) => target.id)
117
+ .filter((target) => byId.get(target)?.kind === "class" && target !== id);
118
+ bases.set(id, inTarget);
119
+ const declared =
120
+ (symbol.flags & SymbolFlags.Alias) === 0 ? symbol : project.queries.aliased(symbol);
121
+ if (declared === UNANSWERED) {
122
+ unanswered.add(id);
123
+ return;
124
+ }
125
+ if (
126
+ inTarget.length === 0 &&
127
+ (((declared.flags & SymbolFlags.Class) !== 0 &&
128
+ // The target's augmentation of an outside class merges an interface into it,
129
+ // so only a class declaration says where the class is.
130
+ declared.declarations.some(
131
+ (handle) => CLASS_DECLARATIONS.has(handle.kind) && !own.has(handle.path),
132
+ )) ||
133
+ ((declared.flags & SymbolFlags.Interface) !== 0 &&
134
+ declared.declarations.some(
135
+ (handle) =>
136
+ handle.kind === SyntaxKind.VariableDeclaration && !own.has(handle.path),
137
+ )))
138
+ ) {
139
+ outsideBases.set(id, declared);
140
+ }
173
141
  });
174
- }
175
- }
176
- }
177
- return found;
178
- }
142
+
143
+ /** The nearest member of one component the class or a class it extends declares. */
144
+ const declaredAbove = (classId: string, component: string, seen: Set<string>): string[] => {
145
+ if (seen.has(classId)) {
146
+ return [];
147
+ }
148
+ seen.add(classId);
149
+ const member = members.get(classId)?.get(component);
150
+ if (member !== undefined) {
151
+ return [member.id];
152
+ }
153
+ return (bases.get(classId) ?? []).flatMap((base) => declaredAbove(base, component, seen));
154
+ };
155
+
156
+ const read = (classId: string, from: string, member: InventorySymbol): void => {
157
+ const file = fileOf.get(classId);
158
+ if (file !== undefined) {
159
+ found.push({
160
+ kind: "named",
161
+ from,
162
+ to: member.id,
163
+ file,
164
+ at: member.position,
165
+ use: "read",
166
+ via: "override",
167
+ });
168
+ }
169
+ };
170
+ const outside = outsideMemberNames(project, bases, outsideBases);
171
+ for (const classId of [...unanswered, ...outside.unanswered]) {
172
+ const ids = [...(members.get(classId)?.values() ?? [])].map((member) => member.id);
173
+ found.push({ kind: "held", from: classId, ids });
174
+ }
175
+ for (const [classId, names] of outside.names) {
176
+ for (const [component, member] of members.get(classId) ?? []) {
177
+ if (names.has(component)) {
178
+ read(classId, classId, member);
179
+ }
180
+ }
181
+ }
182
+ for (const [classId, baseIds] of bases) {
183
+ for (const [component, member] of members.get(classId) ?? []) {
184
+ const seen = new Set([classId]);
185
+ for (const base of baseIds.flatMap((one) => declaredAbove(one, component, seen))) {
186
+ read(classId, base, member);
187
+ }
188
+ }
189
+ }
190
+ return found;
191
+ },
192
+ };
193
+ },
194
+ };
179
195
 
180
196
  /**
181
197
  * Per class of the target with a class declared outside the target in its `extends`
@@ -186,12 +202,24 @@ function outsideMemberNames<Brand>(
186
202
  project: ProjectView<Brand>,
187
203
  bases: ReadonlyMap<string, readonly string[]>,
188
204
  outsideBases: ReadonlyMap<string, TSSymbol>,
189
- ): ReadonlyMap<string, ReadonlySet<string>> {
205
+ ): {
206
+ readonly names: ReadonlyMap<string, ReadonlySet<string>>;
207
+ readonly unanswered: readonly string[];
208
+ } {
190
209
  const direct = new Map<string, ReadonlySet<string>>();
210
+ const unanswered: string[] = [];
191
211
  const error = errorType(project);
192
212
  for (const [classId, base] of outsideBases) {
193
213
  const instance = project.queries.declaredTypeOf(base);
194
214
  const statics = project.queries.typeOfSymbol(base);
215
+ if (
216
+ instance === UNANSWERED ||
217
+ statics === UNANSWERED ||
218
+ project.queries.propertiesOf(instance) === UNANSWERED ||
219
+ (statics !== undefined && project.queries.propertiesOf(statics) === UNANSWERED)
220
+ ) {
221
+ unanswered.push(classId);
222
+ }
195
223
  const components = [
196
224
  ...namesOf(project, instance === UNANSWERED ? undefined : instance).map(nameComponent),
197
225
  ...(instance !== UNANSWERED && reaches(project, instance, error) ? ["toString"] : []),
@@ -220,7 +248,7 @@ function outsideMemberNames<Brand>(
220
248
  names.set(classId, found);
221
249
  }
222
250
  }
223
- return names;
251
+ return { names, unanswered };
224
252
  }
225
253
 
226
254
  /** The global `Error` instance type's identifier, where the program's library declares one. */