@cplieger/deadset-ts 5.1.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The test files no compiler configuration of a run holds. The runner executes them all
3
+ * the same, so each module one names is a test reference: the own file a relative
4
+ * specifier names, and the declarations that file exports under the names read from it.
5
+ * Such a file is read without a program, so what its body references beyond the modules
6
+ * it names is not known.
7
+ */
8
+
9
+ import {
10
+ isCallExpression,
11
+ isExportDeclaration,
12
+ isImportDeclaration,
13
+ isNamespaceExport,
14
+ isNamespaceImport,
15
+ isNoSubstitutionTemplateLiteral,
16
+ isStringLiteral,
17
+ SyntaxKind,
18
+ type Node,
19
+ type SourceFile,
20
+ } from "@typescript/native/unstable/ast";
21
+ import { RESOLVED_EXTENSIONS, type ParseFile } from "./configuration-files.ts";
22
+ import { emittedFrom } from "./emit-map.ts";
23
+ import type { Host } from "./host.ts";
24
+ import type { Inventory } from "./inventory.ts";
25
+ import { dirnamePath, joinPath, normalizePath, queriedModulePath } from "./paths.ts";
26
+ import { renderPosition } from "./position.ts";
27
+ import { nameComponent } from "./ref.ts";
28
+ import type { Reference } from "./references.ts";
29
+
30
+ /** The paths below the target root one relative specifier may name, written path first. */
31
+ function candidates(dir: string, specifier: string): readonly string[] {
32
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
33
+ return [];
34
+ }
35
+ const path = normalizePath(dir === "" ? specifier : `${dir}/${specifier}`);
36
+ return [
37
+ path,
38
+ ...emittedFrom(path, { sourceDirs: [] }),
39
+ ...RESOLVED_EXTENSIONS.map((extension) => path + extension),
40
+ ...RESOLVED_EXTENSIONS.map((extension) => `${path}/index${extension}`),
41
+ ];
42
+ }
43
+
44
+ /** One module a test file names, and the export names it reads from it; `evaluates` reads none. */
45
+ interface NamedModule {
46
+ readonly specifier: Node;
47
+ readonly text: string;
48
+ readonly names: readonly string[];
49
+ readonly evaluates: boolean;
50
+ }
51
+
52
+ /** The text of a specifier written as literal text, or undefined for any other expression. */
53
+ function literalText(node: Node | undefined): string | undefined {
54
+ return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node))
55
+ ? node.text
56
+ : undefined;
57
+ }
58
+
59
+ /**
60
+ * Every module one file names: an import declaration, a re-export, and an `import()` call
61
+ * of literal text anywhere in the file. A namespace form, a star re-export, a bare import
62
+ * and an `import()` evaluate the module.
63
+ */
64
+ function modulesNamed(file: SourceFile): readonly NamedModule[] {
65
+ const found: NamedModule[] = [];
66
+ const name = (specifier: Node, names: readonly string[], evaluates: boolean): void => {
67
+ const text = literalText(specifier);
68
+ if (text !== undefined) {
69
+ found.push({ specifier, text, names, evaluates });
70
+ }
71
+ };
72
+ for (const statement of file.statements) {
73
+ if (isImportDeclaration(statement)) {
74
+ const clause = statement.importClause;
75
+ const bindings = clause?.namedBindings;
76
+ name(
77
+ statement.moduleSpecifier,
78
+ [
79
+ ...(clause?.name === undefined ? [] : ["default"]),
80
+ ...(bindings === undefined || isNamespaceImport(bindings)
81
+ ? []
82
+ : bindings.elements.map((one) => (one.propertyName ?? one.name).text)),
83
+ ],
84
+ bindings === undefined ? clause?.name === undefined : isNamespaceImport(bindings),
85
+ );
86
+ } else if (isExportDeclaration(statement) && statement.moduleSpecifier !== undefined) {
87
+ const clause = statement.exportClause;
88
+ const star = clause === undefined || isNamespaceExport(clause);
89
+ name(
90
+ statement.moduleSpecifier,
91
+ star ? [] : clause.elements.map((one) => (one.propertyName ?? one.name).text),
92
+ star,
93
+ );
94
+ }
95
+ }
96
+ const visit = (node: Node): void => {
97
+ if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
98
+ const [argument] = node.arguments;
99
+ if (argument !== undefined) {
100
+ name(argument, [], true);
101
+ }
102
+ }
103
+ node.forEachChild(visit);
104
+ };
105
+ file.forEachChild(visit);
106
+ return found;
107
+ }
108
+
109
+ /** One test file read without a program. */
110
+ export interface OutsideTest {
111
+ /** The path below the target root. */
112
+ readonly path: string;
113
+ readonly file: SourceFile;
114
+ }
115
+
116
+ /** Each of the given paths below the target root, parsed; one that cannot be read is skipped. */
117
+ export function testFilesAt(
118
+ host: Host,
119
+ targetRoot: string,
120
+ paths: readonly string[],
121
+ parse: ParseFile,
122
+ ): readonly OutsideTest[] {
123
+ return paths.flatMap((path) => {
124
+ const fileName = joinPath(targetRoot, path);
125
+ let text: string;
126
+ try {
127
+ text = host.readFile(fileName);
128
+ } catch {
129
+ return [];
130
+ }
131
+ return [{ path, file: parse(fileName, text) }];
132
+ });
133
+ }
134
+
135
+ /**
136
+ * The references the given test files make to one project's own files through the
137
+ * modules they name ({@link modulesNamed}). A name read from a module references each
138
+ * top-level declaration the file exports under it; an evaluating form references the
139
+ * file. A query after the path names the file as {@link queriedModulePath} reads it.
140
+ */
141
+ export function outsideTestReferences(
142
+ tests: readonly OutsideTest[],
143
+ held: Inventory,
144
+ targetRoot: string,
145
+ ): readonly Reference[] {
146
+ const fileAt = new Map(
147
+ held.symbols
148
+ .filter((symbol) => symbol.kind === "file")
149
+ .map((symbol) => [symbol.position.path, symbol.id]),
150
+ );
151
+ // Keyed on the reference's fragment, which spells the export name where the display
152
+ // name of a default export is its declared name.
153
+ const exported = new Map<string, string[]>();
154
+ for (const symbol of held.symbols) {
155
+ if (symbol.exported) {
156
+ const fragment = symbol.ref.slice(symbol.ref.lastIndexOf("#") + 1).replace(/:alias$/u, "");
157
+ const key = `${symbol.parent}\u0000${fragment}`;
158
+ exported.set(key, [...(exported.get(key) ?? []), symbol.id]);
159
+ }
160
+ }
161
+ const found: Reference[] = [];
162
+ for (const { path, file } of tests) {
163
+ const from = `${path}#`;
164
+ const dir = dirnamePath(path) === "." ? "" : dirnamePath(path);
165
+ for (const named of modulesNamed(file)) {
166
+ const written = named.text.includes("?") ? queriedModulePath(named.text) : named.text;
167
+ if (written === undefined) {
168
+ continue;
169
+ }
170
+ const fileId = candidates(dir, written)
171
+ .map((one) => fileAt.get(one))
172
+ .find((one) => one !== undefined);
173
+ if (fileId === undefined) {
174
+ continue;
175
+ }
176
+ const position = renderPosition(file, targetRoot, named.specifier.getStart());
177
+ const read = (to: string, use: Reference["use"]): void => {
178
+ found.push({ from, to, position, use, resolution: "syntax", test: true, unheld: true });
179
+ };
180
+ if (named.evaluates) {
181
+ read(fileId, "evaluation");
182
+ }
183
+ for (const name of named.names) {
184
+ for (const id of exported.get(`${fileId}\u0000${nameComponent(name)}`) ?? []) {
185
+ read(id, "read");
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return found;
191
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The overrides of the target's class members. A call through the base class, `this`
3
+ * in the base's own body or a value typed as the base, names the base's member and
4
+ * runs the override of whichever subclass the value is, so the base member's
5
+ * declaration references every override as a use of it.
6
+ */
7
+
8
+ import {
9
+ isClassDeclaration,
10
+ isClassExpression,
11
+ isExpressionWithTypeArguments,
12
+ isPropertyAccessExpression,
13
+ isQualifiedName,
14
+ SyntaxKind,
15
+ type HeritageClauseElement,
16
+ type Node,
17
+ } from "@typescript/native/unstable/ast";
18
+ import type { AliasChains } from "./alias-chain.ts";
19
+ import { memberComponent } from "./class-members.ts";
20
+ import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
21
+ import { UNANSWERED } from "./query.ts";
22
+ import type { Reference } from "./references.ts";
23
+ import type { ProjectView } from "./session.ts";
24
+
25
+ /** The member kinds a subclass overrides. */
26
+ const OVERRIDABLE: ReadonlySet<string> = new Set(["method", "class-member"]);
27
+
28
+ /** The node naming the class one `extends` entry names. */
29
+ function nameOf(entry: HeritageClauseElement): Node {
30
+ if (isExpressionWithTypeArguments(entry)) {
31
+ return isPropertyAccessExpression(entry.expression) ? entry.expression.name : entry.expression;
32
+ }
33
+ return isQualifiedName(entry.typeName) ? entry.typeName.right : entry.typeName;
34
+ }
35
+
36
+ /** Each class of the target whose `extends` clause names a class, with the name node. */
37
+ function extending<Brand>(
38
+ project: ProjectView<Brand>,
39
+ held: Inventory,
40
+ ): readonly { readonly id: string; readonly base: Node }[] {
41
+ const found: { id: string; base: Node }[] = [];
42
+ for (const file of project.ownSourceFiles()) {
43
+ const visit = (node: Node): void => {
44
+ if (isClassDeclaration(node) || isClassExpression(node)) {
45
+ const id = held.declarations.get(nodeKey(file, node));
46
+ const clause = node.heritageClauses?.find((one) => one.token === SyntaxKind.ExtendsKeyword);
47
+ const base = clause?.types[0];
48
+ if (id !== undefined && base !== undefined) {
49
+ found.push({ id, base: nameOf(base) });
50
+ }
51
+ }
52
+ node.forEachChild(visit);
53
+ };
54
+ file.forEachChild(visit);
55
+ }
56
+ return found;
57
+ }
58
+
59
+ /**
60
+ * The references each base class member makes to its overrides, for the classes of one
61
+ * project that extend another class of the target. A member a nearer base already
62
+ * overrides is reached through that override. A private member is not overridden.
63
+ * Each reference is placed at the override and made by its file.
64
+ */
65
+ export function overrideReferences<Brand>(
66
+ project: ProjectView<Brand>,
67
+ held: Inventory,
68
+ chains: AliasChains,
69
+ testFiles: ReadonlySet<string>,
70
+ ): readonly Reference[] {
71
+ const classes = extending(project, held);
72
+ if (classes.length === 0) {
73
+ return [];
74
+ }
75
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
76
+ const members = new Map<string, Map<string, InventorySymbol>>();
77
+ for (const symbol of held.symbols) {
78
+ const component = memberComponent(symbol, byId);
79
+ if (
80
+ component === undefined ||
81
+ !OVERRIDABLE.has(symbol.kind) ||
82
+ symbol.visibility === "private" ||
83
+ symbol.visibility === "private-name"
84
+ ) {
85
+ continue;
86
+ }
87
+ const own = members.get(symbol.parent) ?? new Map<string, InventorySymbol>();
88
+ own.set(component, symbol);
89
+ members.set(symbol.parent, own);
90
+ }
91
+ const bases = new Map<string, readonly string[]>();
92
+ const symbols = project.symbolsAt(classes.map((one) => project.handle(one.base)));
93
+ classes.forEach(({ id }, index) => {
94
+ const symbol = symbols[index];
95
+ if (symbol !== undefined && symbol !== UNANSWERED) {
96
+ bases.set(
97
+ id,
98
+ chains
99
+ .chainOf(symbol)
100
+ .map((target) => target.id)
101
+ .filter((target) => byId.get(target)?.kind === "class" && target !== id),
102
+ );
103
+ }
104
+ });
105
+
106
+ /** The nearest member of one component the class or a class it extends declares. */
107
+ const declaredAbove = (classId: string, component: string, seen: Set<string>): string[] => {
108
+ if (seen.has(classId)) {
109
+ return [];
110
+ }
111
+ seen.add(classId);
112
+ const own = members.get(classId)?.get(component);
113
+ if (own !== undefined) {
114
+ return [own.id];
115
+ }
116
+ return (bases.get(classId) ?? []).flatMap((base) => declaredAbove(base, component, seen));
117
+ };
118
+
119
+ const found: Reference[] = [];
120
+ for (const [classId, baseIds] of bases) {
121
+ for (const [component, member] of members.get(classId) ?? []) {
122
+ const seen = new Set([classId]);
123
+ for (const base of baseIds.flatMap((one) => declaredAbove(one, component, seen))) {
124
+ found.push({
125
+ from: base,
126
+ to: member.id,
127
+ position: member.position,
128
+ use: "read",
129
+ resolution: "override",
130
+ test: testFiles.has(member.position.path),
131
+ });
132
+ }
133
+ }
134
+ }
135
+ return found;
136
+ }
package/src/paths.ts CHANGED
@@ -72,6 +72,24 @@ export function resolvePath(against: string, path: string): string {
72
72
  return isAbsolutePath(path) ? normalizePath(path) : joinPath(against, path);
73
73
  }
74
74
 
75
+ /**
76
+ * The relative path before the query of a relative module specifier such as
77
+ * `./worker.ts?worker`, the module a bundler loads; undefined for a specifier with no
78
+ * query, one that is not relative, and one whose query has a `raw` parameter, which
79
+ * reads the file as text and so evaluates nothing.
80
+ */
81
+ export function queriedModulePath(specifier: string): string | undefined {
82
+ const query = specifier.indexOf("?");
83
+ if (query < 0 || !(specifier.startsWith("./") || specifier.startsWith("../"))) {
84
+ return undefined;
85
+ }
86
+ const raw = specifier
87
+ .slice(query + 1)
88
+ .split("&")
89
+ .some((parameter) => parameter.split("=")[0] === "raw");
90
+ return raw ? undefined : specifier.slice(0, query);
91
+ }
92
+
75
93
  /**
76
94
  * One path as it reads below a root, or undefined when it is not below the root.
77
95
  * Both are normalized first, so a root written with a trailing separator or a `.`
package/src/references.ts CHANGED
@@ -37,7 +37,8 @@ import { contextualUses, type FlowAt, type LiteralAt } from "./contextual-uses.t
37
37
  import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
38
38
  import { globExpression } from "./glob.ts";
39
39
  import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
40
- import { relativePath } from "./paths.ts";
40
+ import { overrideReferences } from "./overrides.ts";
41
+ import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.ts";
41
42
  import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
42
43
  import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED, type Answer } from "./query.ts";
43
44
  import type { ProjectView } from "./session.ts";
@@ -71,12 +72,12 @@ import { handedOn, valuesOf } from "./value-flow.ts";
71
72
  export type Use = "read" | "write" | "evaluation" | "decorator";
72
73
 
73
74
  /**
74
- * Which path answered for one reference: `batch`, the batched lookup over a file's names;
75
- * `resolved-symbol`, the per-node lookup for a batch's residue; `shorthand`, the value
76
- * `{ a }` reads; `alias`, a step along an import or export chain; `syntax`, the tree alone
77
- * (a decorator's target, a component's markup); `destructured`, the property a pattern
78
- * names; `contextual`, the member a literal property writes or a value's position reads;
79
- * `by-name`, every declaration spelled as the site is, where the checker failed.
75
+ * Which path answered for one reference: `batch`, a file's batched lookup; `resolved-symbol`,
76
+ * the per-node lookup for its residue; `shorthand`, the value `{ a }` reads; `alias`, a step
77
+ * along an import or export chain; `syntax`, the tree alone (a decorator's target, a
78
+ * component's markup); `destructured`, the property a pattern names; `contextual`, the member
79
+ * a literal property writes or a value's position reads; `override`, the override a base
80
+ * member's call dispatches to; `by-name`, every declaration so spelled, where the checker failed.
80
81
  */
81
82
  export type Resolution =
82
83
  | "batch"
@@ -86,6 +87,7 @@ export type Resolution =
86
87
  | "syntax"
87
88
  | "destructured"
88
89
  | "contextual"
90
+ | "override"
89
91
  | "by-name";
90
92
 
91
93
  /** One use of one declaration by one declaration. */
@@ -112,6 +114,8 @@ export interface Reference {
112
114
  * the consumer's root.
113
115
  */
114
116
  readonly consumer?: string;
117
+ /** Made by a target file no project of the run holds, so it enters as a consumer's reference does. */
118
+ readonly unheld?: true;
115
119
  /**
116
120
  * On a write, the local names of the writing file's import bindings that only the
117
121
  * values its file writes into the same declaration use, which deleting the
@@ -476,6 +480,24 @@ function evaluatedBy(node: Node): Node | undefined {
476
480
  return undefined;
477
481
  }
478
482
 
483
+ /**
484
+ * The own file a module specifier with a query evaluates ({@link queriedModulePath}): the
485
+ * checker resolves the whole specifier to whatever module declaration matches it.
486
+ */
487
+ function queriedFile(
488
+ file: SourceFile,
489
+ specifier: Node,
490
+ fileIdByName: ReadonlyMap<string, string>,
491
+ ): string | undefined {
492
+ if (!isStringLiteral(specifier) && !isNoSubstitutionTemplateLiteral(specifier)) {
493
+ return undefined;
494
+ }
495
+ const path = queriedModulePath(specifier.text);
496
+ return path === undefined
497
+ ? undefined
498
+ : fileIdByName.get(resolvePath(dirnamePath(file.fileName), path));
499
+ }
500
+
479
501
  /**
480
502
  * The re-exports one node writes, if it is an export declaration. A specifier and a
481
503
  * namespace export are declarations whose names are where they are written, so no use is
@@ -1000,6 +1022,9 @@ export function references<Brand>(
1000
1022
  const fileId = declaredAt(file) ?? "";
1001
1023
  return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
1002
1024
  });
1025
+ const fileIdByName = new Map(
1026
+ walked.filter((one) => fileIds.has(one.fileId)).map((one) => [one.file.fileName, one.fileId]),
1027
+ );
1003
1028
  const contextual = contextualUses(
1004
1029
  project,
1005
1030
  held,
@@ -1160,6 +1185,17 @@ export function references<Brand>(
1160
1185
  }
1161
1186
 
1162
1187
  for (const evaluation of evaluations) {
1188
+ const queried = queriedFile(file, evaluation.node, fileIdByName);
1189
+ if (queried !== undefined) {
1190
+ found.push({
1191
+ from: evaluation.from,
1192
+ to: queried,
1193
+ position: renderPosition(file, root, evaluation.node.getStart()),
1194
+ use: "evaluation",
1195
+ resolution: "syntax",
1196
+ test,
1197
+ });
1198
+ }
1163
1199
  const module = answered.get(evaluation.node);
1164
1200
  if (module === undefined) {
1165
1201
  continue;
@@ -1298,6 +1334,9 @@ export function references<Brand>(
1298
1334
  }
1299
1335
  }
1300
1336
 
1337
+ if (consumer === undefined) {
1338
+ found.push(...overrideReferences(project, held, chains, new Set(tests)));
1339
+ }
1301
1340
  found.sort(
1302
1341
  (a, b) =>
1303
1342
  byPosition(a.position, b.position) ||