ushman-equiv 0.4.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/AGENTS.md +81 -0
- package/LICENSE.md +21 -0
- package/README.md +201 -0
- package/bin/ushman-equiv +19 -0
- package/dist/analysis-context.d.ts +102 -0
- package/dist/analysis-context.d.ts.map +1 -0
- package/dist/analysis-context.js +708 -0
- package/dist/ast-guards.d.ts +24 -0
- package/dist/ast-guards.d.ts.map +1 -0
- package/dist/ast-guards.js +83 -0
- package/dist/candidate-boot.d.ts +30 -0
- package/dist/candidate-boot.d.ts.map +1 -0
- package/dist/candidate-boot.js +262 -0
- package/dist/canonicalize.d.ts +19 -0
- package/dist/canonicalize.d.ts.map +1 -0
- package/dist/canonicalize.js +525 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +312 -0
- package/dist/equiv-execution-context.d.ts +25 -0
- package/dist/equiv-execution-context.d.ts.map +1 -0
- package/dist/equiv-execution-context.js +82 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/run.d.ts +8 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +129 -0
- package/dist/shared.d.ts +9 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +47 -0
- package/dist/tier-i-import-graph.d.ts +7 -0
- package/dist/tier-i-import-graph.d.ts.map +1 -0
- package/dist/tier-i-import-graph.js +34 -0
- package/dist/tier-l-child-runtime.d.ts +2 -0
- package/dist/tier-l-child-runtime.d.ts.map +1 -0
- package/dist/tier-l-child-runtime.js +62 -0
- package/dist/tier-l-module-load.d.ts +6 -0
- package/dist/tier-l-module-load.d.ts.map +1 -0
- package/dist/tier-l-module-load.js +139 -0
- package/dist/tier-l-stub-source.d.ts +11 -0
- package/dist/tier-l-stub-source.d.ts.map +1 -0
- package/dist/tier-l-stub-source.js +246 -0
- package/dist/tier-r-replay.d.ts +6 -0
- package/dist/tier-r-replay.d.ts.map +1 -0
- package/dist/tier-r-replay.js +382 -0
- package/dist/tier-s-symbol-diff.d.ts +19 -0
- package/dist/tier-s-symbol-diff.d.ts.map +1 -0
- package/dist/tier-s-symbol-diff.js +156 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/workspace.d.ts +63 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +459 -0
- package/package.json +64 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import traverseImport from '@babel/traverse';
|
|
3
|
+
import { isCallExpression, isClassDeclaration, isExportAllDeclaration, isExportDefaultDeclaration, isExportNamedDeclaration, isExportSpecifier, isFunctionDeclaration, isIdentifier, isImportDeclaration, isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier, isRestElement, isStringLiteral, } from '@babel/types';
|
|
4
|
+
import { asNode, isArrayPatternNode, isAssignmentPatternNode, isClassDeclarationNode, isFunctionDeclarationNode, isIdentifierNode, isObjectPatternNode, isObjectPropertyNode, isVariableDeclarationNode, isVariableDeclaratorNode, } from "./ast-guards.js";
|
|
5
|
+
import { compareStrings } from "./shared.js";
|
|
6
|
+
import { assertV4Workspace, chunk, collectSourceFiles, createFilterMatcher, DEFAULT_MAX_CONCURRENCY, discoverEntrypoint, displayPath, importMapMatchesSpecifier, isBareSpecifier, loadImportMap, normalizeImportSpecifier, parseSourceFile, resolveRelativeSpecifier, } from "./workspace.js";
|
|
7
|
+
const resolveTraverse = () => {
|
|
8
|
+
const traverseNamespace = traverseImport;
|
|
9
|
+
if (typeof traverseImport === 'function') {
|
|
10
|
+
return traverseImport;
|
|
11
|
+
}
|
|
12
|
+
if (traverseNamespace && typeof traverseNamespace === 'object' && typeof traverseNamespace.default === 'function') {
|
|
13
|
+
return traverseNamespace.default;
|
|
14
|
+
}
|
|
15
|
+
throw new TypeError('Unable to resolve @babel/traverse default export.');
|
|
16
|
+
};
|
|
17
|
+
const traverse = resolveTraverse();
|
|
18
|
+
const analysisTestHooksStorage = new AsyncLocalStorage();
|
|
19
|
+
export const withAnalysisContextTestHooks = async ({ hooks, run, }) => await analysisTestHooksStorage.run(hooks, run);
|
|
20
|
+
const getBareUsage = (usages, specifier) => {
|
|
21
|
+
const current = usages.get(specifier) ??
|
|
22
|
+
{
|
|
23
|
+
defaultImported: false,
|
|
24
|
+
namedImports: new Set(),
|
|
25
|
+
namespaceImported: false,
|
|
26
|
+
};
|
|
27
|
+
usages.set(specifier, current);
|
|
28
|
+
return current;
|
|
29
|
+
};
|
|
30
|
+
const addNamedImport = (usage, importedName) => {
|
|
31
|
+
if (importedName !== 'default') {
|
|
32
|
+
usage.namedImports.add(importedName);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const addPendingImport = (pendingImports, importerPath, specifier) => {
|
|
36
|
+
pendingImports.push({
|
|
37
|
+
importerPath,
|
|
38
|
+
specifier,
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const readModuleExportName = (value) => {
|
|
42
|
+
const node = asNode(value);
|
|
43
|
+
if (!node) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (isIdentifierNode(node)) {
|
|
47
|
+
return node.name;
|
|
48
|
+
}
|
|
49
|
+
if (isStringLiteral(node)) {
|
|
50
|
+
return node.value;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
const recordNamespaceMemberUsage = (bareUsages, namespaceAliases, node) => {
|
|
55
|
+
if (!isIdentifier(node.object)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const specifier = namespaceAliases.get(node.object.name);
|
|
59
|
+
if (!specifier) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const usage = getBareUsage(bareUsages, specifier);
|
|
63
|
+
if (!node.computed && isIdentifier(node.property)) {
|
|
64
|
+
usage.namedImports.add(node.property.name);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (isStringLiteral(node.property)) {
|
|
68
|
+
usage.namedImports.add(node.property.value);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const collectNamespaceObjectPatternKeys = (pattern) => {
|
|
72
|
+
const node = asNode(pattern);
|
|
73
|
+
if (!isObjectPatternNode(node)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const names = new Set();
|
|
77
|
+
for (const property of node.properties) {
|
|
78
|
+
if (!isObjectPropertyNode(property)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const keyName = readModuleExportName(property.key);
|
|
82
|
+
if (keyName) {
|
|
83
|
+
names.add(keyName);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...names];
|
|
87
|
+
};
|
|
88
|
+
const recordNamespaceDestructureUsage = ({ bareUsages, id, init, namespaceAliases, }) => {
|
|
89
|
+
if (!isIdentifierNode(init)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const specifier = namespaceAliases.get(init.name);
|
|
93
|
+
if (!specifier) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const usage = getBareUsage(bareUsages, specifier);
|
|
97
|
+
for (const name of collectNamespaceObjectPatternKeys(id)) {
|
|
98
|
+
usage.namedImports.add(name);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const recordImportDeclaration = ({ bareUsages, namespaceAliases, pendingImports, source, sourceFile, specifiers, }) => {
|
|
102
|
+
addPendingImport(pendingImports, sourceFile, source);
|
|
103
|
+
if (!isBareSpecifier(source)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const usage = getBareUsage(bareUsages, source);
|
|
107
|
+
for (const specifier of specifiers) {
|
|
108
|
+
if (isImportDefaultSpecifier(specifier)) {
|
|
109
|
+
usage.defaultImported = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (isImportNamespaceSpecifier(specifier)) {
|
|
113
|
+
usage.namespaceImported = true;
|
|
114
|
+
namespaceAliases.set(specifier.local.name, source);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (isImportSpecifier(specifier)) {
|
|
118
|
+
addNamedImport(usage, isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const recordExportNamedImport = ({ bareUsages, pendingImports, source, sourceFile, specifiers, }) => {
|
|
123
|
+
addPendingImport(pendingImports, sourceFile, source);
|
|
124
|
+
if (!isBareSpecifier(source)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const usage = getBareUsage(bareUsages, source);
|
|
128
|
+
for (const specifier of specifiers) {
|
|
129
|
+
if (!isExportSpecifier(specifier)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const importedName = readModuleExportName(specifier.local);
|
|
133
|
+
if (!importedName) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
addNamedImport(usage, importedName);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const scanImportUsages = ({ ast, bareUsages, pendingImports, relativePath, sourceFile, warnings, }) => {
|
|
140
|
+
const namespaceAliases = new Map();
|
|
141
|
+
traverse(ast, {
|
|
142
|
+
CallExpression(path) {
|
|
143
|
+
if (!isCallExpression(path.node) || path.node.callee.type !== 'Import') {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const [argument] = path.node.arguments;
|
|
147
|
+
if (isStringLiteral(argument)) {
|
|
148
|
+
addPendingImport(pendingImports, sourceFile, argument.value);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
warnings.push({
|
|
152
|
+
message: 'Skipped non-literal dynamic import.',
|
|
153
|
+
path: relativePath,
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
ExportAllDeclaration(path) {
|
|
157
|
+
const source = path.node.source.value;
|
|
158
|
+
addPendingImport(pendingImports, sourceFile, source);
|
|
159
|
+
},
|
|
160
|
+
ExportNamedDeclaration(path) {
|
|
161
|
+
const source = path.node.source?.value;
|
|
162
|
+
if (source) {
|
|
163
|
+
recordExportNamedImport({
|
|
164
|
+
bareUsages,
|
|
165
|
+
pendingImports,
|
|
166
|
+
source,
|
|
167
|
+
sourceFile,
|
|
168
|
+
specifiers: path.node.specifiers,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
ImportDeclaration(path) {
|
|
173
|
+
recordImportDeclaration({
|
|
174
|
+
bareUsages,
|
|
175
|
+
namespaceAliases,
|
|
176
|
+
pendingImports,
|
|
177
|
+
source: path.node.source.value,
|
|
178
|
+
sourceFile,
|
|
179
|
+
specifiers: path.node.specifiers,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
MemberExpression(path) {
|
|
183
|
+
recordNamespaceMemberUsage(bareUsages, namespaceAliases, path.node);
|
|
184
|
+
},
|
|
185
|
+
OptionalMemberExpression(path) {
|
|
186
|
+
recordNamespaceMemberUsage(bareUsages, namespaceAliases, path.node);
|
|
187
|
+
},
|
|
188
|
+
VariableDeclarator(path) {
|
|
189
|
+
if (!isVariableDeclaratorNode(path.node)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
recordNamespaceDestructureUsage({
|
|
193
|
+
bareUsages,
|
|
194
|
+
id: path.node.id,
|
|
195
|
+
init: path.node.init,
|
|
196
|
+
namespaceAliases,
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
const collectArrayPatternNames = (pattern, names) => {
|
|
202
|
+
const node = asNode(pattern);
|
|
203
|
+
if (!isArrayPatternNode(node)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
for (const element of node.elements) {
|
|
207
|
+
if (!element) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
collectPatternNames(element, names);
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
};
|
|
214
|
+
const collectObjectPatternNames = (pattern, names) => {
|
|
215
|
+
const node = asNode(pattern);
|
|
216
|
+
if (!isObjectPatternNode(node)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
for (const property of node.properties) {
|
|
220
|
+
if (isRestElement(property)) {
|
|
221
|
+
collectPatternNames(property.argument, names);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (isObjectPropertyNode(property)) {
|
|
225
|
+
collectPatternNames(property.value, names);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
};
|
|
230
|
+
const collectPatternNames = (pattern, names) => {
|
|
231
|
+
const node = asNode(pattern);
|
|
232
|
+
if (!node) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isIdentifierNode(node)) {
|
|
236
|
+
names.add(node.name);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (isAssignmentPatternNode(node)) {
|
|
240
|
+
collectPatternNames(node.left, names);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (collectArrayPatternNames(node, names) || collectObjectPatternNames(node, names)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (isRestElement(node)) {
|
|
247
|
+
collectPatternNames(node.argument, names);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const addDeclaration = (declarations, exportedNames, kind, name, markExported = false) => {
|
|
251
|
+
const existingKind = declarations.get(name);
|
|
252
|
+
if (!existingKind || existingKind === 'export') {
|
|
253
|
+
declarations.set(name, kind);
|
|
254
|
+
}
|
|
255
|
+
if (markExported || kind === 'export') {
|
|
256
|
+
exportedNames.add(name);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const visitExportNamedStatement = ({ declarations, exportedNames, statement, }) => {
|
|
260
|
+
if (statement.declaration) {
|
|
261
|
+
for (const name of collectDeclaredNames(statement.declaration)) {
|
|
262
|
+
exportedNames.add(name);
|
|
263
|
+
}
|
|
264
|
+
visitTopLevelStatement({
|
|
265
|
+
declarations,
|
|
266
|
+
exportedNames,
|
|
267
|
+
statement: statement.declaration,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
for (const specifier of statement.specifiers) {
|
|
271
|
+
if (!isExportSpecifier(specifier)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const exportedName = readModuleExportName(specifier.exported);
|
|
275
|
+
if (exportedName) {
|
|
276
|
+
addDeclaration(declarations, exportedNames, 'export', exportedName, true);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const visitExportAllStatement = ({ declarations, exportedNames, statement, }) => {
|
|
281
|
+
if (!isExportAllDeclaration(statement)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
const exportedName = readModuleExportName(statement.exported);
|
|
285
|
+
if (exportedName) {
|
|
286
|
+
addDeclaration(declarations, exportedNames, 'export', exportedName, true);
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
};
|
|
290
|
+
const visitExportDefaultStatement = ({ declarations, exportedNames, statement, }) => {
|
|
291
|
+
if (!isExportDefaultDeclaration(statement)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
addDeclaration(declarations, exportedNames, 'export', 'default', true);
|
|
295
|
+
if (!isFunctionDeclaration(statement.declaration) && !isClassDeclaration(statement.declaration)) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
const declaration = statement.declaration;
|
|
299
|
+
visitTopLevelStatement({
|
|
300
|
+
declarations,
|
|
301
|
+
exportedNames,
|
|
302
|
+
statement: declaration,
|
|
303
|
+
});
|
|
304
|
+
return true;
|
|
305
|
+
};
|
|
306
|
+
const collectDeclaredNames = (statement) => {
|
|
307
|
+
const node = asNode(statement);
|
|
308
|
+
if (!node) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
if (isFunctionDeclarationNode(node) && node.id) {
|
|
312
|
+
return [node.id.name];
|
|
313
|
+
}
|
|
314
|
+
if (isClassDeclarationNode(node) && node.id) {
|
|
315
|
+
return [node.id.name];
|
|
316
|
+
}
|
|
317
|
+
if (!isVariableDeclarationNode(node)) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const names = new Set();
|
|
321
|
+
for (const declaration of node.declarations) {
|
|
322
|
+
collectPatternNames(declaration.id, names);
|
|
323
|
+
}
|
|
324
|
+
return [...names];
|
|
325
|
+
};
|
|
326
|
+
const recordTopLevelDeclaration = ({ declarations, exportedNames, statement, }) => {
|
|
327
|
+
if (isFunctionDeclarationNode(statement) && statement.id) {
|
|
328
|
+
addDeclaration(declarations, exportedNames, 'function', statement.id.name);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (isClassDeclarationNode(statement) && statement.id) {
|
|
332
|
+
addDeclaration(declarations, exportedNames, 'class', statement.id.name);
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (!isVariableDeclarationNode(statement)) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const names = collectDeclaredNames(statement);
|
|
339
|
+
for (const name of names) {
|
|
340
|
+
addDeclaration(declarations, exportedNames, statement.kind, name);
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
};
|
|
344
|
+
const visitTopLevelStatement = ({ declarations, exportedNames, statement, }) => {
|
|
345
|
+
if (isImportDeclaration(statement)) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (recordTopLevelDeclaration({ declarations, exportedNames, statement })) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (isExportNamedDeclaration(statement)) {
|
|
352
|
+
visitExportNamedStatement({
|
|
353
|
+
declarations,
|
|
354
|
+
exportedNames,
|
|
355
|
+
statement,
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (visitExportAllStatement({
|
|
360
|
+
declarations,
|
|
361
|
+
exportedNames,
|
|
362
|
+
statement,
|
|
363
|
+
})) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
visitExportDefaultStatement({
|
|
367
|
+
declarations,
|
|
368
|
+
exportedNames,
|
|
369
|
+
statement,
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
export const scanTopLevelDeclarations = (ast) => {
|
|
373
|
+
const declarations = new Map();
|
|
374
|
+
const exportedNames = new Set();
|
|
375
|
+
for (const statement of ast.program.body) {
|
|
376
|
+
visitTopLevelStatement({
|
|
377
|
+
declarations,
|
|
378
|
+
exportedNames,
|
|
379
|
+
statement,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
declarations,
|
|
384
|
+
exportedNames,
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
const toReadonlyBareUsages = (usages) => new Map([...usages.entries()].map(([specifier, usage]) => [
|
|
388
|
+
specifier,
|
|
389
|
+
{
|
|
390
|
+
defaultImported: usage.defaultImported,
|
|
391
|
+
namedImports: new Set(usage.namedImports),
|
|
392
|
+
namespaceImported: usage.namespaceImported,
|
|
393
|
+
},
|
|
394
|
+
]));
|
|
395
|
+
const mergeBareUsages = (target, source) => {
|
|
396
|
+
for (const [specifier, usage] of source.entries()) {
|
|
397
|
+
const current = getBareUsage(target, specifier);
|
|
398
|
+
current.defaultImported ||= usage.defaultImported;
|
|
399
|
+
current.namespaceImported ||= usage.namespaceImported;
|
|
400
|
+
for (const name of usage.namedImports) {
|
|
401
|
+
current.namedImports.add(name);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const resolveImportReferences = async ({ importMap, pendingImports, workspaceRoot, }) => {
|
|
406
|
+
const resolvedImports = [];
|
|
407
|
+
for (const pendingImportChunk of chunk(pendingImports, DEFAULT_MAX_CONCURRENCY)) {
|
|
408
|
+
const chunkResults = await Promise.all(pendingImportChunk.map(async (pendingImport) => {
|
|
409
|
+
const normalizedSpecifier = normalizeImportSpecifier(pendingImport.specifier);
|
|
410
|
+
if (isBareSpecifier(normalizedSpecifier)) {
|
|
411
|
+
const resolved = importMapMatchesSpecifier(importMap, normalizedSpecifier) !== null;
|
|
412
|
+
return {
|
|
413
|
+
importerPath: pendingImport.importerPath,
|
|
414
|
+
message: resolved ? undefined : `Missing bare specifier in importmap: ${normalizedSpecifier}`,
|
|
415
|
+
resolved,
|
|
416
|
+
specifier: normalizedSpecifier,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const resolvedPath = await resolveRelativeSpecifier({
|
|
420
|
+
importerPath: pendingImport.importerPath,
|
|
421
|
+
specifier: normalizedSpecifier,
|
|
422
|
+
workspaceRoot,
|
|
423
|
+
});
|
|
424
|
+
return {
|
|
425
|
+
importerPath: pendingImport.importerPath,
|
|
426
|
+
message: resolvedPath ? undefined : `Missing relative import: ${normalizedSpecifier}`,
|
|
427
|
+
resolved: resolvedPath !== null,
|
|
428
|
+
resolvedPath: resolvedPath ?? undefined,
|
|
429
|
+
specifier: normalizedSpecifier,
|
|
430
|
+
};
|
|
431
|
+
}));
|
|
432
|
+
resolvedImports.push(...chunkResults);
|
|
433
|
+
}
|
|
434
|
+
return resolvedImports;
|
|
435
|
+
};
|
|
436
|
+
const createParseFinding = (workspaceRoot, sourceFile, error) => ({
|
|
437
|
+
message: error instanceof Error ? error.message : String(error),
|
|
438
|
+
path: displayPath(workspaceRoot, sourceFile),
|
|
439
|
+
});
|
|
440
|
+
const createMissingRootWarnings = (missingRoots) => missingRoots.map((missingRoot) => ({
|
|
441
|
+
message: `Source root does not exist and was skipped: ${missingRoot}`,
|
|
442
|
+
path: missingRoot,
|
|
443
|
+
}));
|
|
444
|
+
const analyzeSourceFile = async ({ ast, sourceFile, workspaceRoot, }) => {
|
|
445
|
+
const warnings = [];
|
|
446
|
+
const pendingImports = [];
|
|
447
|
+
const bareUsages = new Map();
|
|
448
|
+
scanImportUsages({
|
|
449
|
+
ast,
|
|
450
|
+
bareUsages,
|
|
451
|
+
pendingImports,
|
|
452
|
+
relativePath: displayPath(workspaceRoot, sourceFile),
|
|
453
|
+
sourceFile,
|
|
454
|
+
warnings,
|
|
455
|
+
});
|
|
456
|
+
const symbolScan = scanTopLevelDeclarations(ast);
|
|
457
|
+
return {
|
|
458
|
+
bareUsages: toReadonlyBareUsages(bareUsages),
|
|
459
|
+
declarations: symbolScan.declarations,
|
|
460
|
+
exportedNames: symbolScan.exportedNames,
|
|
461
|
+
pendingImports,
|
|
462
|
+
warnings,
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
const makeFilterCacheKey = (filter) => filter ?? '__USHMAN_EQUIV_FILTER_ALL__';
|
|
466
|
+
const recordSymbolIndexEntry = ({ byNameMutable, declarations, exportFiles, fileEntries, sourceFile, symbolEntry, }) => {
|
|
467
|
+
fileEntries.push(symbolEntry);
|
|
468
|
+
const existing = byNameMutable.get(symbolEntry.name) ?? [];
|
|
469
|
+
byNameMutable.set(symbolEntry.name, [...existing, symbolEntry]);
|
|
470
|
+
const existingKind = declarations.get(symbolEntry.name);
|
|
471
|
+
if (!existingKind || existingKind === 'export') {
|
|
472
|
+
declarations.set(symbolEntry.name, symbolEntry.kind);
|
|
473
|
+
}
|
|
474
|
+
if (symbolEntry.exported && !exportFiles.has(symbolEntry.name)) {
|
|
475
|
+
exportFiles.set(symbolEntry.name, sourceFile);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const sortSymbolIndexMaps = ({ byFileMutable, byNameMutable, }) => ({
|
|
479
|
+
byFile: new Map([...byFileMutable.entries()].sort(([left], [right]) => compareStrings(left, right))),
|
|
480
|
+
byName: new Map([...byNameMutable.entries()]
|
|
481
|
+
.sort(([left], [right]) => compareStrings(left, right))
|
|
482
|
+
.map(([name, entries]) => [
|
|
483
|
+
name,
|
|
484
|
+
[...entries].sort((left, right) => compareStrings(left.filePath, right.filePath) || compareStrings(left.name, right.name)),
|
|
485
|
+
])),
|
|
486
|
+
});
|
|
487
|
+
const buildSymbolIndex = async ({ getSourceAnalysis, missingRoots, sourceFiles, }) => {
|
|
488
|
+
const byFileMutable = new Map();
|
|
489
|
+
const byNameMutable = new Map();
|
|
490
|
+
const declarations = new Map();
|
|
491
|
+
const errors = [];
|
|
492
|
+
const exportFiles = new Map();
|
|
493
|
+
const warnings = createMissingRootWarnings(missingRoots);
|
|
494
|
+
for (const sourceFileChunk of chunk(sourceFiles, DEFAULT_MAX_CONCURRENCY)) {
|
|
495
|
+
const sourceFileAnalyses = await Promise.all(sourceFileChunk.map(async (sourceFile) => ({
|
|
496
|
+
analysis: await getSourceAnalysis(sourceFile),
|
|
497
|
+
sourceFile,
|
|
498
|
+
})));
|
|
499
|
+
for (const { analysis, sourceFile } of sourceFileAnalyses) {
|
|
500
|
+
if (analysis.parseFinding) {
|
|
501
|
+
errors.push(analysis.parseFinding);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const fileEntries = [];
|
|
505
|
+
for (const [name, kind] of analysis.declarations.entries()) {
|
|
506
|
+
recordSymbolIndexEntry({
|
|
507
|
+
byNameMutable,
|
|
508
|
+
declarations,
|
|
509
|
+
exportFiles,
|
|
510
|
+
fileEntries,
|
|
511
|
+
sourceFile,
|
|
512
|
+
symbolEntry: {
|
|
513
|
+
exported: analysis.exportedNames.has(name),
|
|
514
|
+
filePath: sourceFile,
|
|
515
|
+
kind,
|
|
516
|
+
name,
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
if (fileEntries.length > 0) {
|
|
521
|
+
byFileMutable.set(sourceFile, fileEntries);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
...sortSymbolIndexMaps({
|
|
527
|
+
byFileMutable,
|
|
528
|
+
byNameMutable,
|
|
529
|
+
}),
|
|
530
|
+
declarations,
|
|
531
|
+
errors,
|
|
532
|
+
exportFiles,
|
|
533
|
+
warnings,
|
|
534
|
+
};
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* Builds the shared per-workspace analysis context used by tier orchestration.
|
|
538
|
+
* The context owns entrypoint discovery, source walking, parse dedupe,
|
|
539
|
+
* import-graph filtering, and symbol-index reuse for a single workspace root.
|
|
540
|
+
*/
|
|
541
|
+
export const createAnalysisContext = async (options) => {
|
|
542
|
+
await assertV4Workspace(options.workspaceRoot);
|
|
543
|
+
const entrypointPath = await discoverEntrypoint({
|
|
544
|
+
entrypoint: options.entrypoint,
|
|
545
|
+
workspaceRoot: options.workspaceRoot,
|
|
546
|
+
});
|
|
547
|
+
const importMap = await loadImportMap(options.workspaceRoot);
|
|
548
|
+
const sourceCollection = await collectSourceFiles({
|
|
549
|
+
includeEntrypoint: entrypointPath,
|
|
550
|
+
srcRoots: options.srcRoots,
|
|
551
|
+
workspaceRoot: options.workspaceRoot,
|
|
552
|
+
});
|
|
553
|
+
const parsedSourceCache = new Map();
|
|
554
|
+
const sourceAnalysisCache = new Map();
|
|
555
|
+
const importGraphCache = new Map();
|
|
556
|
+
let symbolIndexPromise = null;
|
|
557
|
+
const getParsedSourceFile = (filePath) => {
|
|
558
|
+
const cached = parsedSourceCache.get(filePath);
|
|
559
|
+
if (cached) {
|
|
560
|
+
return cached;
|
|
561
|
+
}
|
|
562
|
+
const next = (async () => {
|
|
563
|
+
analysisTestHooksStorage.getStore()?.onParseSourceFile?.(filePath);
|
|
564
|
+
return parseSourceFile(filePath);
|
|
565
|
+
})();
|
|
566
|
+
parsedSourceCache.set(filePath, next);
|
|
567
|
+
void next.finally(() => {
|
|
568
|
+
if (parsedSourceCache.get(filePath) === next) {
|
|
569
|
+
parsedSourceCache.delete(filePath);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
return next;
|
|
573
|
+
};
|
|
574
|
+
const getSourceAnalysis = (filePath) => {
|
|
575
|
+
const cached = sourceAnalysisCache.get(filePath);
|
|
576
|
+
if (cached) {
|
|
577
|
+
return cached;
|
|
578
|
+
}
|
|
579
|
+
const next = (async () => {
|
|
580
|
+
try {
|
|
581
|
+
const { ast } = await getParsedSourceFile(filePath);
|
|
582
|
+
return await analyzeSourceFile({
|
|
583
|
+
ast,
|
|
584
|
+
sourceFile: filePath,
|
|
585
|
+
workspaceRoot: options.workspaceRoot,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
return {
|
|
590
|
+
bareUsages: new Map(),
|
|
591
|
+
declarations: new Map(),
|
|
592
|
+
exportedNames: new Set(),
|
|
593
|
+
parseFinding: createParseFinding(options.workspaceRoot, filePath, error),
|
|
594
|
+
pendingImports: [],
|
|
595
|
+
warnings: [],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
})();
|
|
599
|
+
sourceAnalysisCache.set(filePath, next);
|
|
600
|
+
return next;
|
|
601
|
+
};
|
|
602
|
+
const aggregateImportGraph = async (filter) => {
|
|
603
|
+
const bareUsages = new Map();
|
|
604
|
+
const parseFindings = [];
|
|
605
|
+
const pendingImports = [];
|
|
606
|
+
const warnings = createMissingRootWarnings(sourceCollection.missingRoots);
|
|
607
|
+
const matcher = createFilterMatcher(filter);
|
|
608
|
+
const matchedFiles = sourceCollection.files.filter((sourceFile) => {
|
|
609
|
+
const relativePath = displayPath(options.workspaceRoot, sourceFile);
|
|
610
|
+
return sourceFile === entrypointPath || matcher(relativePath);
|
|
611
|
+
});
|
|
612
|
+
for (const sourceFileChunk of chunk(matchedFiles, DEFAULT_MAX_CONCURRENCY)) {
|
|
613
|
+
const analyses = await Promise.all(sourceFileChunk.map(async (sourceFile) => ({
|
|
614
|
+
analysis: await getSourceAnalysis(sourceFile),
|
|
615
|
+
sourceFile,
|
|
616
|
+
})));
|
|
617
|
+
for (const { analysis } of analyses) {
|
|
618
|
+
if (analysis.parseFinding) {
|
|
619
|
+
parseFindings.push(analysis.parseFinding);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
mergeBareUsages(bareUsages, analysis.bareUsages);
|
|
623
|
+
pendingImports.push(...analysis.pendingImports);
|
|
624
|
+
warnings.push(...analysis.warnings);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
bareUsages: toReadonlyBareUsages(bareUsages),
|
|
629
|
+
parseFindings,
|
|
630
|
+
pendingImports,
|
|
631
|
+
warnings,
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
const getImportGraphAnalysis = (filter) => {
|
|
635
|
+
const cacheKey = makeFilterCacheKey(filter);
|
|
636
|
+
const cached = importGraphCache.get(cacheKey);
|
|
637
|
+
if (cached) {
|
|
638
|
+
return cached;
|
|
639
|
+
}
|
|
640
|
+
const next = (async () => {
|
|
641
|
+
const aggregated = await aggregateImportGraph(filter);
|
|
642
|
+
const imports = await resolveImportReferences({
|
|
643
|
+
importMap,
|
|
644
|
+
pendingImports: aggregated.pendingImports,
|
|
645
|
+
workspaceRoot: options.workspaceRoot,
|
|
646
|
+
});
|
|
647
|
+
return {
|
|
648
|
+
bareUsages: aggregated.bareUsages,
|
|
649
|
+
entrypointPath,
|
|
650
|
+
importMap,
|
|
651
|
+
imports,
|
|
652
|
+
parseFindings: aggregated.parseFindings,
|
|
653
|
+
warnings: aggregated.warnings,
|
|
654
|
+
};
|
|
655
|
+
})();
|
|
656
|
+
importGraphCache.set(cacheKey, next);
|
|
657
|
+
return next;
|
|
658
|
+
};
|
|
659
|
+
const getSymbolIndex = () => {
|
|
660
|
+
if (symbolIndexPromise) {
|
|
661
|
+
return symbolIndexPromise;
|
|
662
|
+
}
|
|
663
|
+
symbolIndexPromise = buildSymbolIndex({
|
|
664
|
+
getSourceAnalysis,
|
|
665
|
+
missingRoots: sourceCollection.missingRoots,
|
|
666
|
+
sourceFiles: sourceCollection.files,
|
|
667
|
+
});
|
|
668
|
+
return symbolIndexPromise;
|
|
669
|
+
};
|
|
670
|
+
return {
|
|
671
|
+
entrypointPath,
|
|
672
|
+
getImportGraphAnalysis,
|
|
673
|
+
getParsedSourceFile,
|
|
674
|
+
getSymbolIndex,
|
|
675
|
+
importMap,
|
|
676
|
+
missingRoots: sourceCollection.missingRoots,
|
|
677
|
+
sourceFiles: sourceCollection.files,
|
|
678
|
+
workspaceRoot: options.workspaceRoot,
|
|
679
|
+
};
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* Projects a filtered symbol view from the cached workspace symbol index.
|
|
683
|
+
*/
|
|
684
|
+
export const selectCandidateSymbols = ({ filter, index, }) => {
|
|
685
|
+
const matcher = createFilterMatcher(filter);
|
|
686
|
+
const exportFiles = new Map();
|
|
687
|
+
for (const [name, filePath] of index.exportFiles.entries()) {
|
|
688
|
+
if (matcher(name)) {
|
|
689
|
+
exportFiles.set(name, filePath);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const symbolNames = [...index.declarations.keys()].filter((name) => matcher(name)).sort(compareStrings);
|
|
693
|
+
const symbols = [...index.declarations.entries()]
|
|
694
|
+
.filter(([name]) => matcher(name))
|
|
695
|
+
.sort(([left], [right]) => compareStrings(left, right))
|
|
696
|
+
.map(([name, kind]) => ({
|
|
697
|
+
kind,
|
|
698
|
+
name,
|
|
699
|
+
}));
|
|
700
|
+
return {
|
|
701
|
+
errors: index.errors,
|
|
702
|
+
exportFiles,
|
|
703
|
+
index,
|
|
704
|
+
symbolNames,
|
|
705
|
+
symbols,
|
|
706
|
+
warnings: index.warnings,
|
|
707
|
+
};
|
|
708
|
+
};
|