@aoelite/refpact-ts 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +38 -2
- package/dist/baseline.d.ts +26 -0
- package/dist/baseline.js +65 -0
- package/dist/core/index.d.ts +79 -0
- package/dist/core/index.js +90 -0
- package/dist/core/model.d.ts +340 -0
- package/dist/core/model.js +1 -0
- package/dist/core/results.d.ts +237 -0
- package/dist/core/results.js +1 -0
- package/dist/core/wasm.d.ts +7 -0
- package/dist/core/wasm.js +67 -0
- package/dist/custom.d.ts +129 -0
- package/dist/custom.js +217 -0
- package/dist/filters.d.ts +73 -0
- package/dist/filters.js +39 -0
- package/dist/frontend/checks.d.ts +15 -0
- package/dist/frontend/checks.js +12 -0
- package/dist/frontend/classic.d.ts +3 -0
- package/dist/frontend/classic.js +105 -0
- package/dist/frontend/compiler.d.ts +93 -0
- package/dist/frontend/compiler.js +116 -0
- package/dist/frontend/components.d.ts +29 -0
- package/dist/frontend/components.js +111 -0
- package/dist/frontend/declarations.d.ts +45 -0
- package/dist/frontend/declarations.js +158 -0
- package/dist/frontend/dependencies.d.ts +53 -0
- package/dist/frontend/dependencies.js +337 -0
- package/dist/frontend/imports.d.ts +149 -0
- package/dist/frontend/imports.js +765 -0
- package/dist/frontend/native.d.ts +3 -0
- package/dist/frontend/native.js +165 -0
- package/dist/frontend/paths.d.ts +10 -0
- package/dist/frontend/paths.js +18 -0
- package/dist/frontend/project.d.ts +118 -0
- package/dist/frontend/project.js +775 -0
- package/dist/group.d.ts +74 -0
- package/dist/group.js +106 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +27 -0
- package/dist/items.d.ts +133 -0
- package/dist/items.js +179 -0
- package/dist/layers.d.ts +112 -0
- package/dist/layers.js +215 -0
- package/dist/onion.d.ts +78 -0
- package/dist/onion.js +96 -0
- package/dist/patterns.d.ts +14 -0
- package/dist/patterns.js +15 -0
- package/dist/rules.d.ts +408 -0
- package/dist/rules.js +655 -0
- package/package.json +52 -4
- package/wasm/refpact_core.wasm +0 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
/** Finds every construct in one source file that refers to another module. */
|
|
2
|
+
import { checks } from "./checks.js";
|
|
3
|
+
import { jsDocBesideChildren, SyntaxKind } from "./compiler.js";
|
|
4
|
+
/** The reference kinds of the TypeScript front end. */
|
|
5
|
+
export const KINDS = ["import", "import-type", "dynamic-import", "require", "export-from", "export-type-from", "resolve", "global"];
|
|
6
|
+
/**
|
|
7
|
+
* The kind of a place that asks where a module is and does not load it: `require.resolve("m")`,
|
|
8
|
+
* `import.meta.resolve("m")` and `new URL("./m", import.meta.url)`.
|
|
9
|
+
*/
|
|
10
|
+
export const RESOLVE_KIND = "resolve";
|
|
11
|
+
/**
|
|
12
|
+
* The kind of a use of a global of the host, such as `process.env` or `fetch(..)`: a name
|
|
13
|
+
* that no file of the project declares and that the ECMAScript library does not declare.
|
|
14
|
+
*/
|
|
15
|
+
export const GLOBAL_KIND = "global";
|
|
16
|
+
/**
|
|
17
|
+
* The kind of a use of a member of a class, an interface or an enum: `order.total`. Only a
|
|
18
|
+
* project that is loaded with `resolveMembers` has this kind.
|
|
19
|
+
*/
|
|
20
|
+
export const MEMBER_KIND = "member";
|
|
21
|
+
const K = SyntaxKind;
|
|
22
|
+
function textOf(node) {
|
|
23
|
+
return node?.text ?? node?.escapedText ?? "";
|
|
24
|
+
}
|
|
25
|
+
function isStringLiteral(node) {
|
|
26
|
+
return node !== undefined && (node.kind === K.StringLiteral || node.kind === K.NoSubstitutionTemplateLiteral);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The places in the text of a file where one of some words is written.
|
|
30
|
+
*
|
|
31
|
+
* A node that names a module has a known word in its text, such as `import`. So a node
|
|
32
|
+
* whose text has no such word needs no visit, and neither do the nodes below it. That
|
|
33
|
+
* matters with the native compiler, where each visited node is built from bytes that
|
|
34
|
+
* another process sent, and most nodes of a file are in function bodies that name no module.
|
|
35
|
+
*/
|
|
36
|
+
class Mentions {
|
|
37
|
+
/** Where the words begin, in ascending order. `undefined`: every node is visited. */
|
|
38
|
+
starts;
|
|
39
|
+
constructor(sourceFile, words, prune) {
|
|
40
|
+
const text = sourceFile.text;
|
|
41
|
+
// An identifier can be written with an escape, as in `r\u0065quire`. Such a file is
|
|
42
|
+
// read in full.
|
|
43
|
+
if (!prune || text.includes("\\u")) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const starts = [];
|
|
47
|
+
for (const word of words) {
|
|
48
|
+
for (let at = text.indexOf(word); at !== -1; at = text.indexOf(word, at + 1)) {
|
|
49
|
+
starts.push(at);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.starts = starts.sort((a, b) => a - b);
|
|
53
|
+
}
|
|
54
|
+
/** True when a word begins in the text of the node. */
|
|
55
|
+
in(node) {
|
|
56
|
+
const starts = this.starts;
|
|
57
|
+
if (starts === undefined) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
let low = 0;
|
|
61
|
+
let high = starts.length;
|
|
62
|
+
while (low < high) {
|
|
63
|
+
const middle = (low + high) >> 1;
|
|
64
|
+
if (starts[middle] < node.pos) {
|
|
65
|
+
low = middle + 1;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
high = middle;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return low < starts.length && starts[low] < node.end;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Each construct that names a module has one of these words in its text.
|
|
75
|
+
const MODULE_WORDS = ["import", "export", "require"];
|
|
76
|
+
/**
|
|
77
|
+
* Finds every construct of the file that names a module.
|
|
78
|
+
*
|
|
79
|
+
* `prune` lets the visit leave out nodes, see {@link Mentions}. It must be `false` for a
|
|
80
|
+
* JavaScript file: the compiler makes nodes from the types in JSDoc comments, and such a node
|
|
81
|
+
* is in the text before the node that it belongs to, not in it.
|
|
82
|
+
*/
|
|
83
|
+
export function collectModuleUses(sourceFile, prune) {
|
|
84
|
+
const uses = moduleUses(sourceFile, prune);
|
|
85
|
+
if (checks.walk && prune) {
|
|
86
|
+
same(uses, moduleUses(sourceFile, false), (use) => use.specifierNode);
|
|
87
|
+
}
|
|
88
|
+
return uses;
|
|
89
|
+
}
|
|
90
|
+
/** With the test switch: the visit that leaves nodes out must find what the full visit finds. */
|
|
91
|
+
function same(found, all, node) {
|
|
92
|
+
if (found.length !== all.length || found.some((use, index) => node(use) !== node(all[index]))) {
|
|
93
|
+
throw new Error("refpact: a visit that leaves out nodes without a known word does not find what a visit of all nodes finds");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function moduleUses(sourceFile, prune) {
|
|
97
|
+
const mentions = new Mentions(sourceFile, MODULE_WORDS, prune);
|
|
98
|
+
const constants = new Constants(sourceFile);
|
|
99
|
+
const uses = [];
|
|
100
|
+
const visit = (node, parent) => {
|
|
101
|
+
if (!mentions.in(node)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
uses.push(...moduleUse(node, parent, constants));
|
|
105
|
+
if (!prune && jsDocBesideChildren) {
|
|
106
|
+
node.jsDoc?.forEach((comment) => visit(comment, node));
|
|
107
|
+
}
|
|
108
|
+
node.forEachChild((child) => visit(child, node));
|
|
109
|
+
};
|
|
110
|
+
sourceFile.forEachChild((child) => visit(child, undefined));
|
|
111
|
+
return uses;
|
|
112
|
+
}
|
|
113
|
+
function moduleUse(node, parent, constants) {
|
|
114
|
+
const one = (use) => (use === undefined ? [] : [use]);
|
|
115
|
+
switch (node.kind) {
|
|
116
|
+
case K.ImportDeclaration:
|
|
117
|
+
return one(importDeclaration(node));
|
|
118
|
+
case K.ExportDeclaration:
|
|
119
|
+
return one(exportDeclaration(node));
|
|
120
|
+
case K.ImportEqualsDeclaration:
|
|
121
|
+
return one(importEquals(node));
|
|
122
|
+
case K.CallExpression:
|
|
123
|
+
return call(node, parent, constants);
|
|
124
|
+
case K.NewExpression:
|
|
125
|
+
return newUrl(node, constants);
|
|
126
|
+
case K.ImportType:
|
|
127
|
+
return one(importType(node));
|
|
128
|
+
default:
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** The part of a specifier that the code does not say. No specifier has this character. */
|
|
133
|
+
const HOLE = "\0";
|
|
134
|
+
/**
|
|
135
|
+
* The `const` variables of a file that have a value, by name. A name that the file declares
|
|
136
|
+
* more than once is not in it. The file is read in full, and only when a specifier needs it.
|
|
137
|
+
*/
|
|
138
|
+
class Constants {
|
|
139
|
+
sourceFile;
|
|
140
|
+
found;
|
|
141
|
+
constructor(sourceFile) {
|
|
142
|
+
this.sourceFile = sourceFile;
|
|
143
|
+
}
|
|
144
|
+
/** The text of a node as the file writes it. */
|
|
145
|
+
text(node) {
|
|
146
|
+
return this.sourceFile.text.slice(node.getStart(this.sourceFile), node.end);
|
|
147
|
+
}
|
|
148
|
+
get(name) {
|
|
149
|
+
if (this.found === undefined) {
|
|
150
|
+
const found = new Map();
|
|
151
|
+
const visit = (node, parent) => {
|
|
152
|
+
if (node.kind === K.VariableDeclaration && node.name?.kind === K.Identifier) {
|
|
153
|
+
const name = textOf(node.name);
|
|
154
|
+
const constant = parent?.kind === K.VariableDeclarationList && this.text(parent).startsWith("const") && node.initializer !== undefined;
|
|
155
|
+
found.set(name, found.has(name) || !constant ? undefined : { name: node.name, value: node.initializer });
|
|
156
|
+
}
|
|
157
|
+
node.forEachChild((child) => visit(child, node));
|
|
158
|
+
};
|
|
159
|
+
this.sourceFile.forEachChild((child) => visit(child, undefined));
|
|
160
|
+
this.found = found;
|
|
161
|
+
}
|
|
162
|
+
return this.found.get(name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const MOST_SPECIFIERS = 16;
|
|
166
|
+
/**
|
|
167
|
+
* The strings that an expression can be: a string, a template, strings joined with `+`, the
|
|
168
|
+
* two sides of `? :`, and a `const` variable of the file with such a value.
|
|
169
|
+
*/
|
|
170
|
+
function specifiers(node, constants) {
|
|
171
|
+
const used = [];
|
|
172
|
+
const join = (left, right) => left.flatMap((start) => right.map((end) => start + end)).slice(0, MOST_SPECIFIERS);
|
|
173
|
+
const read = (node, depth) => {
|
|
174
|
+
if (node === undefined || depth > 8) {
|
|
175
|
+
return [HOLE];
|
|
176
|
+
}
|
|
177
|
+
switch (node.kind) {
|
|
178
|
+
case K.StringLiteral:
|
|
179
|
+
case K.NoSubstitutionTemplateLiteral:
|
|
180
|
+
return [textOf(node)];
|
|
181
|
+
case K.ParenthesizedExpression:
|
|
182
|
+
case K.AsExpression:
|
|
183
|
+
case K.SatisfiesExpression:
|
|
184
|
+
case K.NonNullExpression:
|
|
185
|
+
return read(node.expression, depth + 1);
|
|
186
|
+
case K.TemplateExpression: {
|
|
187
|
+
let values = [textOf(node.head)];
|
|
188
|
+
for (const span of node.templateSpans ?? []) {
|
|
189
|
+
values = join(join(values, read(span.expression, depth + 1)), [textOf(span.literal)]);
|
|
190
|
+
}
|
|
191
|
+
return values;
|
|
192
|
+
}
|
|
193
|
+
case K.BinaryExpression:
|
|
194
|
+
return node.operatorToken?.kind === K.PlusToken ? join(read(node.left, depth + 1), read(node.right, depth + 1)) : [HOLE];
|
|
195
|
+
case K.ConditionalExpression:
|
|
196
|
+
return [...read(node.whenTrue, depth + 1), ...read(node.whenFalse, depth + 1)].slice(0, MOST_SPECIFIERS);
|
|
197
|
+
case K.Identifier: {
|
|
198
|
+
const constant = constants.get(textOf(node));
|
|
199
|
+
if (constant === undefined) {
|
|
200
|
+
return [HOLE];
|
|
201
|
+
}
|
|
202
|
+
used.push({ use: node, declaration: constant.name });
|
|
203
|
+
return read(constant.value, depth + 1);
|
|
204
|
+
}
|
|
205
|
+
default:
|
|
206
|
+
return [HOLE];
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
return { values: [...new Set(read(node, 0))], constants: used };
|
|
210
|
+
}
|
|
211
|
+
/** One module use for each string that a specifier can be. */
|
|
212
|
+
function computedUses(argument, constants, make) {
|
|
213
|
+
if (isStringLiteral(argument)) {
|
|
214
|
+
return [make(textOf(argument))];
|
|
215
|
+
}
|
|
216
|
+
const found = specifiers(argument, constants);
|
|
217
|
+
return found.values.map((value) => {
|
|
218
|
+
// Parts that are not known and that follow each other are one part.
|
|
219
|
+
const pattern = value.replace(/\0+/g, HOLE);
|
|
220
|
+
const computed = pattern === HOLE ? "unknown" : pattern.includes(HOLE) ? "pattern" : "constant";
|
|
221
|
+
const specifier = computed === "unknown" ? "" : pattern.replaceAll(HOLE, "*");
|
|
222
|
+
// The place of a binding is the place of one module only.
|
|
223
|
+
const { namespace: _namespace, names: _names, ...use } = make(specifier);
|
|
224
|
+
return { ...use, whole: true, names: [], computed, constants: found.constants };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function importDeclaration(node) {
|
|
228
|
+
const specifierNode = node.moduleSpecifier;
|
|
229
|
+
if (!isStringLiteral(specifierNode)) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const clause = node.importClause;
|
|
233
|
+
const typeOnly = clause?.isTypeOnly === true || clause?.phaseModifier === K.TypeKeyword;
|
|
234
|
+
const kind = typeOnly ? "import-type" : "import";
|
|
235
|
+
const names = [];
|
|
236
|
+
let namespace;
|
|
237
|
+
if (clause?.name !== undefined) {
|
|
238
|
+
names.push({ exported: "default", kind, node: clause.name });
|
|
239
|
+
}
|
|
240
|
+
const bindings = clause?.namedBindings;
|
|
241
|
+
if (bindings?.kind === K.NamespaceImport && bindings.name !== undefined) {
|
|
242
|
+
namespace = { local: textOf(bindings.name), node: bindings.name, kind };
|
|
243
|
+
}
|
|
244
|
+
else if (bindings?.kind === K.NamedImports) {
|
|
245
|
+
for (const element of bindings.elements ?? []) {
|
|
246
|
+
if (element.name !== undefined) {
|
|
247
|
+
names.push({
|
|
248
|
+
exported: textOf(element.propertyName ?? element.name),
|
|
249
|
+
kind: typeOnly || element.isTypeOnly === true ? "import-type" : "import",
|
|
250
|
+
node: element.name,
|
|
251
|
+
specifier: true,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
specifier: textOf(specifierNode),
|
|
258
|
+
specifierNode,
|
|
259
|
+
at: namespace?.node ?? node,
|
|
260
|
+
kind,
|
|
261
|
+
// `import "m"`, `import {} from "m"` and `import * as ns from "m"` use the module as a whole.
|
|
262
|
+
whole: names.length === 0 || namespace !== undefined,
|
|
263
|
+
names,
|
|
264
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function exportDeclaration(node) {
|
|
268
|
+
const specifierNode = node.moduleSpecifier;
|
|
269
|
+
if (!isStringLiteral(specifierNode)) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
const typeOnly = node.isTypeOnly === true;
|
|
273
|
+
const kind = typeOnly ? "export-type-from" : "export-from";
|
|
274
|
+
const names = [];
|
|
275
|
+
const clause = node.exportClause;
|
|
276
|
+
if (clause?.kind === K.NamedExports) {
|
|
277
|
+
for (const element of clause.elements ?? []) {
|
|
278
|
+
if (element.name !== undefined) {
|
|
279
|
+
names.push({
|
|
280
|
+
exported: textOf(element.propertyName ?? element.name),
|
|
281
|
+
kind: typeOnly || element.isTypeOnly === true ? "export-type-from" : "export-from",
|
|
282
|
+
node: element.name,
|
|
283
|
+
specifier: true,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
specifier: textOf(specifierNode),
|
|
290
|
+
specifierNode,
|
|
291
|
+
at: node,
|
|
292
|
+
kind,
|
|
293
|
+
// `export * from "m"`, `export * as ns from "m"` and `export {} from "m"` use the module as a whole.
|
|
294
|
+
whole: names.length === 0,
|
|
295
|
+
names,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function importEquals(node) {
|
|
299
|
+
const reference = node.moduleReference;
|
|
300
|
+
if (reference?.kind !== K.ExternalModuleReference || !isStringLiteral(reference.expression) || node.name === undefined) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
const kind = node.isTypeOnly === true ? "import-type" : "require";
|
|
304
|
+
return {
|
|
305
|
+
specifier: textOf(reference.expression),
|
|
306
|
+
specifierNode: reference.expression,
|
|
307
|
+
at: node,
|
|
308
|
+
kind,
|
|
309
|
+
whole: true,
|
|
310
|
+
names: [],
|
|
311
|
+
namespace: { local: textOf(node.name), node: node.name, kind },
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function call(node, parent, constants) {
|
|
315
|
+
const argument = node.arguments?.[0];
|
|
316
|
+
const callee = node.expression;
|
|
317
|
+
if (argument === undefined || callee === undefined) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
if (callee.kind === K.ImportKeyword) {
|
|
321
|
+
return computedUses(argument, constants, (specifier) => ({ specifier, specifierNode: argument, at: node, kind: "dynamic-import", whole: true, names: [] }));
|
|
322
|
+
}
|
|
323
|
+
if (callee.kind === K.PropertyAccessExpression && callee.expression !== undefined) {
|
|
324
|
+
const member = textOf(callee.name);
|
|
325
|
+
const object = callee.expression;
|
|
326
|
+
// `require.resolve("m")` and `import.meta.resolve("m")` ask where a module is.
|
|
327
|
+
if (member === "resolve" && object.kind === K.Identifier && textOf(object) === "require") {
|
|
328
|
+
return computedUses(argument, constants, (specifier) => ({ specifier, specifierNode: argument, at: node, kind: "resolve", whole: true, names: [], requireNode: object }));
|
|
329
|
+
}
|
|
330
|
+
if (object.kind === K.MetaProperty && constants.text(object) === "import.meta") {
|
|
331
|
+
if (member === "resolve") {
|
|
332
|
+
return computedUses(argument, constants, (specifier) => ({ specifier, specifierNode: argument, at: node, kind: "resolve", whole: true, names: [] }));
|
|
333
|
+
}
|
|
334
|
+
// `import.meta.glob("./pages/*.ts")` of Vite imports each file that the pattern matches.
|
|
335
|
+
if (member === "glob") {
|
|
336
|
+
const patterns = argument.kind === K.ArrayLiteralExpression ? (argument.elements ?? []) : [argument];
|
|
337
|
+
return patterns
|
|
338
|
+
.filter(isStringLiteral)
|
|
339
|
+
.map((pattern) => ({ specifier: textOf(pattern), specifierNode: pattern, at: pattern, kind: "dynamic-import", whole: true, names: [], computed: "glob" }));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
if (callee.kind !== K.Identifier || textOf(callee) !== "require" || node.arguments?.length !== 1) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
// `const m = require("m")` binds the module; `const { a } = require("m")` takes names.
|
|
348
|
+
const names = [];
|
|
349
|
+
let namespace;
|
|
350
|
+
const target = parent?.kind === K.VariableDeclaration && parent.initializer === node ? parent.name : undefined;
|
|
351
|
+
if (target?.kind === K.Identifier) {
|
|
352
|
+
namespace = { local: textOf(target), node: target, kind: "require" };
|
|
353
|
+
}
|
|
354
|
+
else if (target?.kind === K.ObjectBindingPattern) {
|
|
355
|
+
for (const element of target.elements ?? []) {
|
|
356
|
+
const exported = element.propertyName ?? element.name;
|
|
357
|
+
if (exported?.kind === K.Identifier && element.name !== undefined) {
|
|
358
|
+
names.push({ exported: textOf(exported), kind: "require", node: element.name });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return computedUses(argument, constants, (specifier) => ({
|
|
363
|
+
specifier,
|
|
364
|
+
specifierNode: argument,
|
|
365
|
+
at: node,
|
|
366
|
+
kind: "require",
|
|
367
|
+
whole: names.length === 0,
|
|
368
|
+
names,
|
|
369
|
+
requireNode: callee,
|
|
370
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
/** `new URL("./worker.ts", import.meta.url)` names a file beside this one. */
|
|
374
|
+
function newUrl(node, constants) {
|
|
375
|
+
const [argument, base] = node.arguments ?? [];
|
|
376
|
+
const callee = node.expression;
|
|
377
|
+
if (argument === undefined || base === undefined || callee?.kind !== K.Identifier || textOf(callee) !== "URL" || constants.text(base) !== "import.meta.url") {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
return computedUses(argument, constants, (specifier) => ({ specifier, specifierNode: argument, at: node, kind: "resolve", whole: true, names: [], asset: true }));
|
|
381
|
+
}
|
|
382
|
+
/** `import("m")` in a type, and `import("m").Name`. */
|
|
383
|
+
function importType(node) {
|
|
384
|
+
const specifierNode = node.argument?.literal;
|
|
385
|
+
if (!isStringLiteral(specifierNode)) {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
let first = node.qualifier;
|
|
389
|
+
while (first?.kind === K.QualifiedName) {
|
|
390
|
+
first = first.left;
|
|
391
|
+
}
|
|
392
|
+
const names = first !== undefined ? [{ exported: textOf(first), kind: "import-type", node: first }] : [];
|
|
393
|
+
return {
|
|
394
|
+
specifier: textOf(specifierNode),
|
|
395
|
+
specifierNode,
|
|
396
|
+
at: node,
|
|
397
|
+
kind: "import-type",
|
|
398
|
+
whole: names.length === 0,
|
|
399
|
+
names,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/** Finds `object.member` where `object` is one of the given local names. */
|
|
403
|
+
export function collectMemberUses(sourceFile, locals, prune) {
|
|
404
|
+
if (locals.size === 0) {
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
const uses = memberUses(sourceFile, locals, prune);
|
|
408
|
+
if (checks.walk && prune) {
|
|
409
|
+
same(uses, memberUses(sourceFile, locals, false), (use) => use.member);
|
|
410
|
+
}
|
|
411
|
+
return uses;
|
|
412
|
+
}
|
|
413
|
+
function memberUses(sourceFile, locals, prune) {
|
|
414
|
+
const mentions = new Mentions(sourceFile, locals, prune);
|
|
415
|
+
const uses = [];
|
|
416
|
+
// The `const` variables whose value is a binding, `const f = ns`, by their name. A name
|
|
417
|
+
// that the file declares more than once this way is not followed.
|
|
418
|
+
const aliases = new Map();
|
|
419
|
+
const isConst = (list) => list?.kind === K.VariableDeclarationList && sourceFile.text.startsWith("const", list.getStart(sourceFile));
|
|
420
|
+
const visit = (node, parent) => {
|
|
421
|
+
if (!mentions.in(node)) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const object = node.kind === K.PropertyAccessExpression ? node.expression : node.kind === K.QualifiedName ? node.left : undefined;
|
|
425
|
+
const member = node.kind === K.PropertyAccessExpression ? node.name : node.kind === K.QualifiedName ? node.right : undefined;
|
|
426
|
+
if (object?.kind === K.Identifier && member?.kind === K.Identifier && locals.has(textOf(object))) {
|
|
427
|
+
uses.push({ object, member, objectName: textOf(object), memberName: textOf(member) });
|
|
428
|
+
}
|
|
429
|
+
const value = node.kind === K.VariableDeclaration ? node.initializer : undefined;
|
|
430
|
+
if (value?.kind === K.Identifier && locals.has(textOf(value))) {
|
|
431
|
+
if (node.name?.kind === K.ObjectBindingPattern) {
|
|
432
|
+
// `const { a, b: c } = ns` takes `ns.a` and `ns.b`.
|
|
433
|
+
for (const element of node.name.elements ?? []) {
|
|
434
|
+
const taken = element.propertyName ?? element.name;
|
|
435
|
+
if (taken?.kind === K.Identifier && element.name !== undefined && sourceFile.text.slice(element.getStart(sourceFile), element.end).slice(0, 3) !== "...") {
|
|
436
|
+
uses.push({ object: value, member: taken, objectName: textOf(value), memberName: textOf(taken) });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (node.name?.kind === K.Identifier && isConst(parent)) {
|
|
441
|
+
const name = textOf(node.name);
|
|
442
|
+
aliases.set(name, aliases.has(name) ? undefined : { declaration: node.name, of: value });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
node.forEachChild((child) => visit(child, node));
|
|
446
|
+
};
|
|
447
|
+
sourceFile.forEachChild((child) => visit(child, undefined));
|
|
448
|
+
// The members of the `const` variables, where the file names them.
|
|
449
|
+
const known = new Map([...aliases].flatMap(([name, alias]) => (alias !== undefined && !locals.has(name) ? [[name, alias]] : [])));
|
|
450
|
+
if (known.size > 0) {
|
|
451
|
+
const named = new Mentions(sourceFile, known.keys(), prune);
|
|
452
|
+
const follow = (node) => {
|
|
453
|
+
if (!named.in(node)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const alias = node.kind === K.PropertyAccessExpression && node.expression?.kind === K.Identifier ? known.get(textOf(node.expression)) : undefined;
|
|
457
|
+
if (alias !== undefined && node.expression !== undefined && node.name?.kind === K.Identifier) {
|
|
458
|
+
uses.push({ object: node.expression, member: node.name, objectName: textOf(alias.of), memberName: textOf(node.name), alias });
|
|
459
|
+
}
|
|
460
|
+
node.forEachChild((child) => follow(child));
|
|
461
|
+
};
|
|
462
|
+
sourceFile.forEachChild((child) => follow(child));
|
|
463
|
+
}
|
|
464
|
+
return uses;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The names after the dot of each `object.name` of a file, in the order of the text. What a
|
|
468
|
+
* name is a member of depends on the type of the object, which only the compiler knows.
|
|
469
|
+
* Every node of the file is visited.
|
|
470
|
+
*/
|
|
471
|
+
export function collectAccesses(sourceFile) {
|
|
472
|
+
const names = [];
|
|
473
|
+
const visit = (node) => {
|
|
474
|
+
if (node.kind === K.PropertyAccessExpression && node.name?.kind === K.Identifier) {
|
|
475
|
+
names.push(node.name);
|
|
476
|
+
}
|
|
477
|
+
node.forEachChild((child) => visit(child));
|
|
478
|
+
};
|
|
479
|
+
sourceFile.forEachChild((child) => visit(child));
|
|
480
|
+
return names;
|
|
481
|
+
}
|
|
482
|
+
/** The names that stand for the global object. */
|
|
483
|
+
const GLOBAL_OBJECTS = new Set(["globalThis", "window", "self", "global"]);
|
|
484
|
+
/**
|
|
485
|
+
* The names in value positions that may be globals, in the order of the text. Types,
|
|
486
|
+
* the names after a dot, JSX tags and the callee of `require(..)` are left out.
|
|
487
|
+
*/
|
|
488
|
+
export function collectGlobalUses(sourceFile) {
|
|
489
|
+
// A name that an enclosing scope declares, or that the language declares, is no global of
|
|
490
|
+
// the host: the compiler need not be asked. A scope that is not modelled here only makes
|
|
491
|
+
// the compiler be asked more.
|
|
492
|
+
const scopes = [];
|
|
493
|
+
const declared = (name) => LANGUAGE_NAMES.has(name) || scopes.some((scope) => scope.has(name));
|
|
494
|
+
// In each scope: the `const` variables whose value is a name that may be a global,
|
|
495
|
+
// `const p = process`, with that name.
|
|
496
|
+
const aliases = [];
|
|
497
|
+
const aliasOf = (name) => {
|
|
498
|
+
for (let depth = scopes.length - 1; depth >= 0; depth--) {
|
|
499
|
+
if (scopes[depth]?.has(name) === true) {
|
|
500
|
+
return aliases[depth]?.get(name);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return undefined;
|
|
504
|
+
};
|
|
505
|
+
const found = [];
|
|
506
|
+
const member = (access, object) => access?.kind === K.PropertyAccessExpression && access.expression === object && access.name?.kind === K.Identifier ? access.name : undefined;
|
|
507
|
+
const visit = (node, parent, grand) => {
|
|
508
|
+
if (SKIPPED_NODES.has(node.kind) || isTypeHeritage(node, parent)) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const value = node.kind === K.VariableDeclaration ? node.initializer : undefined;
|
|
512
|
+
if (value?.kind === K.Identifier && node.name?.kind === K.Identifier && !declared(textOf(value))) {
|
|
513
|
+
const constant = parent?.kind === K.VariableDeclarationList && sourceFile.text.startsWith("const", parent.getStart(sourceFile));
|
|
514
|
+
if (constant) {
|
|
515
|
+
aliases[aliases.length - 1]?.set(textOf(node.name), value);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const through = node.kind === K.Identifier && parent !== undefined ? aliasOf(textOf(node)) : undefined;
|
|
519
|
+
const taken = through !== undefined ? member(parent, node) : undefined;
|
|
520
|
+
if (through !== undefined && taken !== undefined) {
|
|
521
|
+
// `p.exit`, where `const p = process`, is a use of `process.exit`.
|
|
522
|
+
found.push({ at: node, node: through, name: textOf(through), member: textOf(taken) });
|
|
523
|
+
}
|
|
524
|
+
const pattern = parent?.kind === K.VariableDeclaration && parent.initializer === node && parent.name?.kind === K.ObjectBindingPattern ? parent.name : undefined;
|
|
525
|
+
const elements = (pattern?.elements ?? []).flatMap((element) => {
|
|
526
|
+
const name = element.propertyName ?? element.name;
|
|
527
|
+
const rest = sourceFile.text.startsWith("...", element.getStart(sourceFile));
|
|
528
|
+
return name?.kind === K.Identifier && !rest ? [name] : [];
|
|
529
|
+
});
|
|
530
|
+
if (node.kind === K.Identifier && parent !== undefined && !declared(textOf(node)) && elements.length > 0 && !GLOBAL_OBJECTS.has(textOf(node))) {
|
|
531
|
+
// `const { env } = process` takes `process.env`.
|
|
532
|
+
for (const element of elements) {
|
|
533
|
+
found.push({ at: element, node, name: textOf(node), member: textOf(element) });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else if (node.kind === K.Identifier && parent !== undefined && !declared(textOf(node)) && isValueName(node, parent)) {
|
|
537
|
+
const after = member(parent, node);
|
|
538
|
+
if (GLOBAL_OBJECTS.has(textOf(node)) && after !== undefined) {
|
|
539
|
+
const next = member(grand, parent);
|
|
540
|
+
found.push({ at: node, node: after, name: textOf(after), through: node, ...(next !== undefined && { member: textOf(next) }) });
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const shorthand = parent.kind === K.ShorthandPropertyAssignment ? { shorthand: parent } : {};
|
|
544
|
+
found.push({ at: node, node, name: textOf(node), ...(after !== undefined && { member: textOf(after) }), ...shorthand });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const scope = scopeNames(node);
|
|
548
|
+
if (scope !== undefined) {
|
|
549
|
+
scopes.push(scope);
|
|
550
|
+
aliases.push(new Map());
|
|
551
|
+
}
|
|
552
|
+
node.forEachChild((child) => visit(child, node, parent));
|
|
553
|
+
if (scope !== undefined) {
|
|
554
|
+
scopes.pop();
|
|
555
|
+
aliases.pop();
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
scopes.push(statementNames(sourceFile));
|
|
559
|
+
aliases.push(new Map());
|
|
560
|
+
sourceFile.forEachChild((child) => visit(child, sourceFile, undefined));
|
|
561
|
+
return found;
|
|
562
|
+
}
|
|
563
|
+
/** The kinds of functions, whose parameters are a scope. */
|
|
564
|
+
const FUNCTIONS = new Set([
|
|
565
|
+
K.FunctionDeclaration,
|
|
566
|
+
K.FunctionExpression,
|
|
567
|
+
K.ArrowFunction,
|
|
568
|
+
K.MethodDeclaration,
|
|
569
|
+
K.Constructor,
|
|
570
|
+
K.GetAccessor,
|
|
571
|
+
K.SetAccessor,
|
|
572
|
+
]);
|
|
573
|
+
/** The kinds of nodes whose statements or head declare names for the code inside them. */
|
|
574
|
+
const STATEMENT_SCOPES = new Set([K.Block, K.ForStatement, K.ForOfStatement, K.ForInStatement, K.CatchClause]);
|
|
575
|
+
/**
|
|
576
|
+
* The names that a node declares for the code inside it, if it is a scope: a block with its
|
|
577
|
+
* declarations, a function with its parameters, a loop or a `catch` with its variable. A
|
|
578
|
+
* `var` counts in its block only, which is narrower than the language has it.
|
|
579
|
+
*/
|
|
580
|
+
function scopeNames(node) {
|
|
581
|
+
if (STATEMENT_SCOPES.has(node.kind)) {
|
|
582
|
+
return statementNames(node);
|
|
583
|
+
}
|
|
584
|
+
if (!FUNCTIONS.has(node.kind)) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
const names = new Set();
|
|
588
|
+
if (node.kind === K.FunctionExpression && node.name !== undefined) {
|
|
589
|
+
names.add(textOf(node.name));
|
|
590
|
+
}
|
|
591
|
+
node.forEachChild((child) => {
|
|
592
|
+
const parameter = child;
|
|
593
|
+
if (parameter.kind === K.Parameter && parameter.name !== undefined) {
|
|
594
|
+
bindingNames(parameter.name, names);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
return names;
|
|
598
|
+
}
|
|
599
|
+
/** Adds the names that a binding declares: `a`, or those of `{ a, b: [c] }`. */
|
|
600
|
+
function bindingNames(node, names) {
|
|
601
|
+
if (node.kind === K.Identifier) {
|
|
602
|
+
names.add(textOf(node));
|
|
603
|
+
}
|
|
604
|
+
else if (node.kind === K.ObjectBindingPattern || node.kind === K.ArrayBindingPattern) {
|
|
605
|
+
node.forEachChild((element) => {
|
|
606
|
+
const name = element.name;
|
|
607
|
+
if (name !== undefined) {
|
|
608
|
+
bindingNames(name, names);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Some names of the ECMAScript library. Its names are never globals of the host, so these
|
|
615
|
+
* need not be asked. The list is short on purpose: a name that is missing is asked.
|
|
616
|
+
*/
|
|
617
|
+
const LANGUAGE_NAMES = new Set([
|
|
618
|
+
"undefined",
|
|
619
|
+
"NaN",
|
|
620
|
+
"Infinity",
|
|
621
|
+
"Array",
|
|
622
|
+
"Boolean",
|
|
623
|
+
"Date",
|
|
624
|
+
"Error",
|
|
625
|
+
"JSON",
|
|
626
|
+
"Map",
|
|
627
|
+
"Math",
|
|
628
|
+
"Number",
|
|
629
|
+
"Object",
|
|
630
|
+
"Promise",
|
|
631
|
+
"Reflect",
|
|
632
|
+
"RegExp",
|
|
633
|
+
"Set",
|
|
634
|
+
"String",
|
|
635
|
+
"Symbol",
|
|
636
|
+
"TypeError",
|
|
637
|
+
"WeakMap",
|
|
638
|
+
"WeakSet",
|
|
639
|
+
"parseFloat",
|
|
640
|
+
"parseInt",
|
|
641
|
+
]);
|
|
642
|
+
/**
|
|
643
|
+
* The names that the statements of a file or a block, the head of a loop or a `catch`
|
|
644
|
+
* declare: imports, variables, functions, classes and enums.
|
|
645
|
+
*/
|
|
646
|
+
function statementNames(container) {
|
|
647
|
+
const names = new Set();
|
|
648
|
+
const bindings = (node) => bindingNames(node, names);
|
|
649
|
+
const visit = (node) => {
|
|
650
|
+
switch (node.kind) {
|
|
651
|
+
case K.VariableStatement:
|
|
652
|
+
case K.VariableDeclarationList:
|
|
653
|
+
node.forEachChild((child) => visit(child));
|
|
654
|
+
break;
|
|
655
|
+
case K.VariableDeclaration:
|
|
656
|
+
case K.FunctionDeclaration:
|
|
657
|
+
case K.ClassDeclaration:
|
|
658
|
+
case K.EnumDeclaration:
|
|
659
|
+
case K.ImportEqualsDeclaration:
|
|
660
|
+
if (node.name !== undefined) {
|
|
661
|
+
bindings(node.name);
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
case K.ImportDeclaration:
|
|
665
|
+
node.forEachChild((child) => visit(child));
|
|
666
|
+
break;
|
|
667
|
+
case K.ImportClause:
|
|
668
|
+
if (node.name !== undefined) {
|
|
669
|
+
names.add(textOf(node.name));
|
|
670
|
+
}
|
|
671
|
+
node.forEachChild((child) => visit(child));
|
|
672
|
+
break;
|
|
673
|
+
case K.NamedImports:
|
|
674
|
+
node.forEachChild((child) => visit(child));
|
|
675
|
+
break;
|
|
676
|
+
case K.ImportSpecifier:
|
|
677
|
+
case K.NamespaceImport:
|
|
678
|
+
names.add(textOf(node.name));
|
|
679
|
+
break;
|
|
680
|
+
default:
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
container.forEachChild((child) => visit(child));
|
|
685
|
+
return names;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* The kinds of nodes whose names are no uses of values: types, and the names of imports and
|
|
689
|
+
* exports, which are those of other modules.
|
|
690
|
+
*/
|
|
691
|
+
const SKIPPED_NODES = new Set([
|
|
692
|
+
K.TypeReference,
|
|
693
|
+
K.TypeQuery,
|
|
694
|
+
K.QualifiedName,
|
|
695
|
+
K.InterfaceDeclaration,
|
|
696
|
+
K.TypeAliasDeclaration,
|
|
697
|
+
K.ImportDeclaration,
|
|
698
|
+
K.ImportEqualsDeclaration,
|
|
699
|
+
K.ExportDeclaration,
|
|
700
|
+
]);
|
|
701
|
+
/** The `implements` clause of a class, which names types. */
|
|
702
|
+
function isTypeHeritage(node, parent) {
|
|
703
|
+
return node.kind === K.HeritageClause && (node.token === K.ImplementsKeyword || parent?.kind === K.InterfaceDeclaration);
|
|
704
|
+
}
|
|
705
|
+
/** Whether an identifier is a name that a value can stand behind, not a property or a tag. */
|
|
706
|
+
function isValueName(node, parent) {
|
|
707
|
+
switch (parent.kind) {
|
|
708
|
+
case K.PropertyAccessExpression:
|
|
709
|
+
return parent.name !== node;
|
|
710
|
+
case K.JsxOpeningElement:
|
|
711
|
+
case K.JsxSelfClosingElement:
|
|
712
|
+
case K.JsxClosingElement:
|
|
713
|
+
return parent.tagName !== node;
|
|
714
|
+
case K.PropertyAssignment:
|
|
715
|
+
case K.PropertyDeclaration:
|
|
716
|
+
case K.PropertySignature:
|
|
717
|
+
case K.MethodDeclaration:
|
|
718
|
+
case K.GetAccessor:
|
|
719
|
+
case K.SetAccessor:
|
|
720
|
+
case K.EnumMember:
|
|
721
|
+
return parent.name !== node;
|
|
722
|
+
case K.BindingElement:
|
|
723
|
+
return parent.propertyName !== node && parent.name !== node;
|
|
724
|
+
case K.JsxAttribute:
|
|
725
|
+
case K.MetaProperty:
|
|
726
|
+
return false;
|
|
727
|
+
case K.CallExpression:
|
|
728
|
+
return !(parent.expression === node && textOf(node) === "require");
|
|
729
|
+
default:
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* The markers that a file can contain: a use of the global `console`, a `debugger`
|
|
735
|
+
* statement, and `unresolved-import`, an import of a relative path, a `paths` alias or a
|
|
736
|
+
* `#name` of `imports` that leads to no file.
|
|
737
|
+
*/
|
|
738
|
+
export const MARKERS = ["console", "debugger", "unresolved-import", "computed-import"];
|
|
739
|
+
/** The `debugger` statements of a file, and its `console.name`, in the order of the text. */
|
|
740
|
+
export function collectMarkers(sourceFile, prune) {
|
|
741
|
+
const found = markers(sourceFile, prune);
|
|
742
|
+
if (checks.walk && prune) {
|
|
743
|
+
same(found, markers(sourceFile, false), (use) => use.at);
|
|
744
|
+
}
|
|
745
|
+
return found;
|
|
746
|
+
}
|
|
747
|
+
const MARKER_WORDS = ["console", "debugger"];
|
|
748
|
+
function markers(sourceFile, prune) {
|
|
749
|
+
const mentions = new Mentions(sourceFile, MARKER_WORDS, prune);
|
|
750
|
+
const found = [];
|
|
751
|
+
const visit = (node) => {
|
|
752
|
+
if (!mentions.in(node)) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (node.kind === K.DebuggerStatement) {
|
|
756
|
+
found.push({ marker: "debugger", at: node });
|
|
757
|
+
}
|
|
758
|
+
else if (node.kind === K.PropertyAccessExpression && node.expression?.kind === K.Identifier && textOf(node.expression) === "console") {
|
|
759
|
+
found.push({ marker: "console", at: node.expression, object: node.expression });
|
|
760
|
+
}
|
|
761
|
+
node.forEachChild((child) => visit(child));
|
|
762
|
+
};
|
|
763
|
+
sourceFile.forEachChild((child) => visit(child));
|
|
764
|
+
return found;
|
|
765
|
+
}
|