@cplieger/deadset-ts 5.1.0 → 5.2.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.
- package/README.md +1 -1
- package/dist/src/analysis.js +79 -37
- package/dist/src/config.js +3 -0
- package/dist/src/configuration-files.js +24 -25
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +46 -18
- package/dist/src/convention-rows.js +7 -0
- package/dist/src/conventions.js +97 -33
- package/dist/src/dependencies.js +8 -0
- package/dist/src/entry-point-gaps.js +10 -26
- package/dist/src/entry-points.js +4 -0
- package/dist/src/exempt.js +23 -8
- package/dist/src/file-facts.js +23 -20
- package/dist/src/findings/completion.js +27 -11
- package/dist/src/findings/interfaces.js +2 -6
- package/dist/src/findings/intra-function.js +15 -3
- package/dist/src/findings/unused-declarations.js +5 -6
- package/dist/src/findings/visibility-narrowing.js +6 -1
- package/dist/src/generated-file.js +72 -12
- package/dist/src/graph.js +6 -5
- package/dist/src/html-entries.js +137 -0
- package/dist/src/inventory.js +13 -3
- package/dist/src/kinds.js +2 -2
- package/dist/src/manifest.js +34 -5
- package/dist/src/matrix.js +9 -3
- package/dist/src/namespace-objects.js +158 -0
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/references.js +150 -4
- package/dist/src/roots.js +172 -39
- package/dist/src/session.js +4 -3
- package/dist/src/sweep.js +17 -0
- package/dist/src/template-field.js +60 -3
- package/dist/src/test-support.js +35 -23
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +122 -49
- package/src/config.ts +3 -0
- package/src/configuration-files.ts +29 -25
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +51 -20
- package/src/convention-rows.ts +15 -0
- package/src/conventions.ts +119 -30
- package/src/dependencies.ts +9 -0
- package/src/entry-point-gaps.ts +11 -28
- package/src/entry-points.ts +7 -1
- package/src/exempt.ts +29 -12
- package/src/file-facts.ts +25 -21
- package/src/findings/completion.ts +31 -11
- package/src/findings/interfaces.ts +2 -9
- package/src/findings/intra-function.ts +17 -3
- package/src/findings/unused-declarations.ts +5 -8
- package/src/findings/visibility-narrowing.ts +7 -1
- package/src/generated-file.ts +80 -12
- package/src/graph.ts +9 -8
- package/src/html-entries.ts +170 -0
- package/src/inventory.ts +15 -3
- package/src/kinds.ts +2 -2
- package/src/manifest.ts +49 -7
- package/src/matrix.ts +21 -3
- package/src/namespace-objects.ts +216 -0
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/references.ts +206 -9
- package/src/roots.ts +210 -53
- package/src/session.ts +4 -2
- package/src/sweep.ts +19 -0
- package/src/template-field.ts +65 -3
- package/src/test-support.ts +34 -22
- package/src/version.ts +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module namespace objects: the binding of a namespace import, the value an `await` of a
|
|
3
|
+
* dynamic `import()` produces, the first parameter of the first callback passed to `then`
|
|
4
|
+
* on one, and a `const` binding initialized with one of these. A use reads the exports it
|
|
5
|
+
* names and no other: a property access by name or by a literal index, or a destructured
|
|
6
|
+
* property, names one export, and any other use may read every export.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
isArrowFunction,
|
|
11
|
+
isAwaitExpression,
|
|
12
|
+
isBindingElement,
|
|
13
|
+
isCallExpression,
|
|
14
|
+
isElementAccessExpression,
|
|
15
|
+
isExpressionStatement,
|
|
16
|
+
isFunctionExpression,
|
|
17
|
+
isIdentifier,
|
|
18
|
+
isImportDeclaration,
|
|
19
|
+
isNamespaceImport,
|
|
20
|
+
isNoSubstitutionTemplateLiteral,
|
|
21
|
+
isObjectBindingPattern,
|
|
22
|
+
isParenthesizedExpression,
|
|
23
|
+
isPropertyAccessExpression,
|
|
24
|
+
isQualifiedName,
|
|
25
|
+
isStringLiteral,
|
|
26
|
+
isVariableDeclaration,
|
|
27
|
+
isVariableDeclarationList,
|
|
28
|
+
isVoidExpression,
|
|
29
|
+
NodeFlags,
|
|
30
|
+
SyntaxKind,
|
|
31
|
+
type Identifier,
|
|
32
|
+
type Node,
|
|
33
|
+
type SourceFile,
|
|
34
|
+
} from "@typescript/native/unstable/ast";
|
|
35
|
+
|
|
36
|
+
/** What one use of a namespace object reads beyond the names the checker resolves. */
|
|
37
|
+
export type NamespaceUse =
|
|
38
|
+
/** Every export of the module `specifier` names. */
|
|
39
|
+
| { readonly kind: "whole"; readonly specifier: Node }
|
|
40
|
+
/** The export a literal index names, the literal being the node to resolve. */
|
|
41
|
+
| { readonly kind: "indexed"; readonly literal: Node };
|
|
42
|
+
|
|
43
|
+
function isLiteralText(node: Node): boolean {
|
|
44
|
+
return isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The outermost expression one node is, through the parentheses written around it. */
|
|
48
|
+
function parenthesized(node: Node): Node {
|
|
49
|
+
let at = node;
|
|
50
|
+
while (isParenthesizedExpression(at.parent)) {
|
|
51
|
+
at = at.parent;
|
|
52
|
+
}
|
|
53
|
+
return at;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether one call is a dynamic `import()` whose argument is literal text. */
|
|
57
|
+
function isLiteralImport(node: Node): boolean {
|
|
58
|
+
if (!isCallExpression(node) || node.expression.kind !== SyntaxKind.ImportKeyword) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const [argument] = node.arguments;
|
|
62
|
+
return argument !== undefined && isLiteralText(argument);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The node a `const` binding's name is visible below: the block or file its statement is in. */
|
|
66
|
+
function scopeOf(declaration: Node): Node {
|
|
67
|
+
const statement = declaration.parent.parent;
|
|
68
|
+
return statement.parent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every identifier below `scope` that spells `name`, other than `declared` and a member's name. */
|
|
72
|
+
function occurrences(scope: Node, name: string, declared: Node): readonly Identifier[] {
|
|
73
|
+
const found: Identifier[] = [];
|
|
74
|
+
const visit = (node: Node): void => {
|
|
75
|
+
if (isIdentifier(node)) {
|
|
76
|
+
const parent = node.parent;
|
|
77
|
+
const member =
|
|
78
|
+
(isPropertyAccessExpression(parent) && parent.name === node) ||
|
|
79
|
+
(isQualifiedName(parent) && parent.right === node) ||
|
|
80
|
+
(isBindingElement(parent) && parent.propertyName === node);
|
|
81
|
+
if (node !== declared && node.text === name && !member) {
|
|
82
|
+
found.push(node);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
node.forEachChild(visit);
|
|
87
|
+
};
|
|
88
|
+
scope.forEachChild(visit);
|
|
89
|
+
return found;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Every use of the namespace objects one file holds, keyed by the node the use is written
|
|
94
|
+
* at, that reads more than a name the checker resolves: an indexed read by a literal, and
|
|
95
|
+
* any use that may read every export. A shadowing declaration of a binding's name inside
|
|
96
|
+
* its scope is read as the binding, which can only keep an export live.
|
|
97
|
+
*/
|
|
98
|
+
export function namespaceUses(file: SourceFile): ReadonlyMap<Node, NamespaceUse> {
|
|
99
|
+
const uses = new Map<Node, NamespaceUse>();
|
|
100
|
+
|
|
101
|
+
const binding = (name: Identifier, scope: Node, specifier: Node, seen: Set<Node>): void => {
|
|
102
|
+
if (seen.has(name)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
seen.add(name);
|
|
106
|
+
for (const one of occurrences(scope, name.text, name)) {
|
|
107
|
+
value(one, specifier, seen);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** One use of a namespace object's value, at `node`. */
|
|
112
|
+
const value = (node: Node, specifier: Node, seen: Set<Node>): void => {
|
|
113
|
+
const at = parenthesized(node);
|
|
114
|
+
const parent = at.parent;
|
|
115
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (
|
|
119
|
+
(isPropertyAccessExpression(parent) && parent.expression === at) ||
|
|
120
|
+
(isQualifiedName(parent) && parent.left === at)
|
|
121
|
+
) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (isElementAccessExpression(parent) && parent.expression === at) {
|
|
125
|
+
const index = parent.argumentExpression;
|
|
126
|
+
uses.set(
|
|
127
|
+
node,
|
|
128
|
+
isLiteralText(index) ? { kind: "indexed", literal: index } : { kind: "whole", specifier },
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (isVariableDeclaration(parent) && parent.initializer === at) {
|
|
133
|
+
const pattern = parent.name;
|
|
134
|
+
if (isObjectBindingPattern(pattern)) {
|
|
135
|
+
const named = pattern.elements.every(
|
|
136
|
+
(element) =>
|
|
137
|
+
element.dotDotDotToken === undefined &&
|
|
138
|
+
element.propertyName?.kind !== SyntaxKind.ComputedPropertyName,
|
|
139
|
+
);
|
|
140
|
+
if (!named) {
|
|
141
|
+
uses.set(node, { kind: "whole", specifier });
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const list = parent.parent;
|
|
146
|
+
if (
|
|
147
|
+
isIdentifier(pattern) &&
|
|
148
|
+
isVariableDeclarationList(list) &&
|
|
149
|
+
(list.flags & NodeFlags.Const) !== 0
|
|
150
|
+
) {
|
|
151
|
+
binding(pattern, scopeOf(parent), specifier, seen);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
uses.set(node, { kind: "whole", specifier });
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** One use of a dynamic `import()` call, at `call`. */
|
|
159
|
+
const imported = (call: Node, specifier: Node, seen: Set<Node>): void => {
|
|
160
|
+
const at = parenthesized(call);
|
|
161
|
+
const parent = at.parent;
|
|
162
|
+
if (isExpressionStatement(parent) || isVoidExpression(parent)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (isAwaitExpression(parent)) {
|
|
166
|
+
value(parent, specifier, seen);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const then = parent.parent;
|
|
170
|
+
if (
|
|
171
|
+
isPropertyAccessExpression(parent) &&
|
|
172
|
+
parent.expression === at &&
|
|
173
|
+
parent.name.text === "then" &&
|
|
174
|
+
isCallExpression(then) &&
|
|
175
|
+
then.expression === parent
|
|
176
|
+
) {
|
|
177
|
+
const [callback] = then.arguments;
|
|
178
|
+
if (callback !== undefined && (isArrowFunction(callback) || isFunctionExpression(callback))) {
|
|
179
|
+
const [first] = callback.parameters;
|
|
180
|
+
if (first === undefined || isObjectBindingPattern(first.name)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (isIdentifier(first.name)) {
|
|
184
|
+
binding(first.name, callback, specifier, seen);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
uses.set(call, { kind: "whole", specifier });
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const visit = (node: Node): void => {
|
|
193
|
+
if (isImportDeclaration(node)) {
|
|
194
|
+
const clause = node.importClause;
|
|
195
|
+
const bindings = clause?.namedBindings;
|
|
196
|
+
if (
|
|
197
|
+
clause !== undefined &&
|
|
198
|
+
clause.phaseModifier !== SyntaxKind.TypeKeyword &&
|
|
199
|
+
bindings !== undefined &&
|
|
200
|
+
isNamespaceImport(bindings)
|
|
201
|
+
) {
|
|
202
|
+
binding(bindings.name, file, node.moduleSpecifier, new Set());
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (isLiteralImport(node) && isCallExpression(node)) {
|
|
207
|
+
const [argument] = node.arguments;
|
|
208
|
+
if (argument !== undefined) {
|
|
209
|
+
imported(node, argument, new Set());
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
node.forEachChild(visit);
|
|
213
|
+
};
|
|
214
|
+
file.forEachChild(visit);
|
|
215
|
+
return uses;
|
|
216
|
+
}
|
|
@@ -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
|
+
}
|
package/src/overrides.ts
ADDED
|
@@ -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 `.`
|