entkapp 4.5.1 → 5.0.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/bin/cli.js +4 -4
- package/docs.zip +0 -0
- package/entkapp/config.json +0 -1
- package/package.json +5 -6
- package/src/EngineContext.js +276 -78
- package/src/analyzers/OxcAnalyzer.js +8 -380
- package/src/api/HeadlessAPI.js +1 -1
- package/src/api/PluginSDK.js +23 -187
- package/src/ast/ASTAnalyzer.js +467 -253
- package/src/ast/AdvancedAnalysis.js +6 -5
- package/src/ast/BarrelParser.js +23 -16
- package/src/ast/DeadCodeDetector.js +30 -18
- package/src/ast/MagicDetector.js +1 -1
- package/src/ast/OxcAnalyzer.js +329 -273
- package/src/index.js +272 -361
- package/src/performance/GraphCache.js +21 -2
- package/src/performance/WorkerPool.js +11 -1
- package/src/performance/WorkerTaskRunner.js +72 -25
- package/src/plugins/PluginRegistry.js +5 -16
- package/src/plugins/ecosystems/GenericPlugins.js +61 -0
- package/src/refractor/TransactionManager.js +3 -136
- package/src/refractor/TypeIntegrity.js +2 -73
- package/src/resolution/CircularDetector.js +27 -66
- package/src/resolution/ConfigLoader.js +2 -85
- package/src/resolution/DepencyResolver.js +20 -124
- package/src/resolution/EntryPointDetector.js +134 -0
- package/src/resolution/PathMapper.js +3 -123
- package/src/resolution/TSConfigLoader.js +76 -0
- package/src/resolution/WorkSpaceGraph.js +4 -473
- package/src/resolution/WorkspaceDiagnostic.js +3 -57
- package/src/plugins/KnipAdapter.js +0 -106
package/src/ast/ASTAnalyzer.js
CHANGED
|
@@ -8,10 +8,6 @@ export class ASTAnalyzer {
|
|
|
8
8
|
this.scopeStack = [];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Returns the TypeScript ScriptKind for a given file path.
|
|
13
|
-
* Exposed as a public method so WorkerTaskRunner can call it directly.
|
|
14
|
-
*/
|
|
15
11
|
getScriptKind(filePath) {
|
|
16
12
|
if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) return ts.ScriptKind.TSX;
|
|
17
13
|
if (filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return ts.ScriptKind.JS;
|
|
@@ -31,35 +27,159 @@ export class ASTAnalyzer {
|
|
|
31
27
|
this.getScriptKind(filePath)
|
|
32
28
|
);
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
this.scopeStack.push(this.currentScope);
|
|
36
|
-
|
|
30
|
+
fileNode.ast = sourceFile;
|
|
37
31
|
this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
|
|
38
|
-
fileNode.ast = sourceFile; // Store the AST for advanced analysis
|
|
39
|
-
this.walkAST(sourceFile, fileNode, sourceFile);
|
|
40
32
|
|
|
41
|
-
|
|
33
|
+
// --- TWO-PASS ANALYSIS STRATEGY ---
|
|
34
|
+
|
|
35
|
+
// Pass 1: Declaration Indexing
|
|
36
|
+
this.currentScope = { symbols: new Map(), parent: null, children: [] };
|
|
37
|
+
this.scopeStack = [this.currentScope];
|
|
38
|
+
this.scopeCounter = 0;
|
|
39
|
+
this.pass = 1;
|
|
40
|
+
try {
|
|
41
|
+
this.walkAST(sourceFile, fileNode, sourceFile);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (this.context.verbose) console.error(`[AST] Pass 1 failed for ${filePath}: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Pass 2: Reference Tracking
|
|
47
|
+
if (this.scopeStack.length > 0) {
|
|
48
|
+
this.currentScope = this.scopeStack[0];
|
|
49
|
+
this.scopeCounter = 0;
|
|
50
|
+
this.pass = 2;
|
|
51
|
+
try {
|
|
52
|
+
this.walkAST(sourceFile, fileNode, sourceFile);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (this.context.verbose) console.error(`[AST] Pass 2 failed for ${filePath}: ${e.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Pass 3: Side-Effect Detection (Diamond Edition)
|
|
59
|
+
this.detectSideEffects(sourceFile, fileNode);
|
|
60
|
+
|
|
61
|
+
// Pass 4: Barrel Detection
|
|
62
|
+
this.detectBarrelStatus(sourceFile, fileNode);
|
|
63
|
+
|
|
64
|
+
// UPGRADE: Final check for "joke" imports or unused barrel re-exports
|
|
65
|
+
if (fileNode.isBarrel && fileNode.instantiatedIdentifiers.size === 0) {
|
|
66
|
+
fileNode.isPureBarrel = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.scopeStack = [];
|
|
42
70
|
this.currentScope = null;
|
|
43
71
|
}
|
|
44
72
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
73
|
+
detectBarrelStatus(sourceFile, fileNode) {
|
|
74
|
+
let totalExportDeclarations = 0;
|
|
75
|
+
let totalTopLevelStatements = sourceFile.statements.length;
|
|
76
|
+
let usesInternalImport = false;
|
|
77
|
+
|
|
78
|
+
if (totalTopLevelStatements === 0) return;
|
|
79
|
+
|
|
80
|
+
// UPGRADE: Barrel Files that re-exports everything may get caught as "OH YOU ENTRY POINT"
|
|
81
|
+
// but it never used the functions it imported.
|
|
82
|
+
// In the entry point, it has to use at least 1 import that it imported,
|
|
83
|
+
// or else its barrel or files that arent importing it as a joke.
|
|
84
|
+
|
|
85
|
+
sourceFile.statements.forEach(node => {
|
|
86
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
|
87
|
+
totalExportDeclarations++;
|
|
88
|
+
} else if (ts.isImportDeclaration(node)) {
|
|
89
|
+
// Imports are common in barrels, we don't count them against the ratio
|
|
90
|
+
totalTopLevelStatements--;
|
|
91
|
+
} else if (ts.isExportAssignment(node)) {
|
|
92
|
+
totalExportDeclarations++;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Check if any imported symbol is actually used in the file (Pass 2 already does this via instantiatedIdentifiers)
|
|
97
|
+
// However, for Barrel detection, we want to know if it's ONLY re-exporting.
|
|
98
|
+
|
|
99
|
+
const barrelRatio = totalExportDeclarations / Math.max(1, totalTopLevelStatements);
|
|
100
|
+
|
|
101
|
+
// If it's mostly exports and imports, it's likely a barrel.
|
|
102
|
+
if (barrelRatio > 0.8) {
|
|
103
|
+
fileNode.isBarrel = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
detectSideEffects(sourceFile, fileNode) {
|
|
108
|
+
let hasSideEffects = false;
|
|
109
|
+
|
|
110
|
+
const checkNode = (node) => {
|
|
111
|
+
if (hasSideEffects) return;
|
|
112
|
+
|
|
113
|
+
// 1. Top-level Call Expressions (excluding declarations)
|
|
114
|
+
if (ts.isExpressionStatement(node)) {
|
|
115
|
+
const expr = node.expression;
|
|
116
|
+
if (ts.isCallExpression(expr) || ts.isAwaitExpression(expr)) {
|
|
117
|
+
// Check for specific bootstrap patterns
|
|
118
|
+
const callText = expr.getText(sourceFile);
|
|
119
|
+
const bootstrapTriggers = [
|
|
120
|
+
'app.listen', 'http.createServer', 'ReactDOM.render',
|
|
121
|
+
'process.on', 'process.exit', 'console.log',
|
|
122
|
+
'setInterval', 'setTimeout', 'new Vue', 'new App'
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
if (bootstrapTriggers.some(trigger => callText.includes(trigger))) {
|
|
126
|
+
hasSideEffects = true;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Any top-level call that isn't just a simple utility might be an entry
|
|
131
|
+
// We exclude common non-executing calls if necessary, but generally,
|
|
132
|
+
// a top-level call in a non-export-only file is a strong signal.
|
|
133
|
+
hasSideEffects = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. Node.js direct execution check: if (require.main === module)
|
|
138
|
+
if (ts.isIfStatement(node)) {
|
|
139
|
+
const cond = node.expression.getText(sourceFile);
|
|
140
|
+
if (cond.includes('require.main === module')) {
|
|
141
|
+
hasSideEffects = true;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 3. IIFE (Immediately Invoked Function Expression)
|
|
147
|
+
if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression)) {
|
|
148
|
+
const func = node.expression.expression;
|
|
149
|
+
if (ts.isParenthesizedExpression(func) || ts.isFunctionExpression(func) || ts.isArrowFunction(func)) {
|
|
150
|
+
hasSideEffects = true;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
ts.forEachChild(sourceFile, checkNode);
|
|
157
|
+
fileNode.hasSideEffects = hasSideEffects;
|
|
50
158
|
}
|
|
51
159
|
|
|
52
160
|
pushScope() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
161
|
+
if (this.pass === 1) {
|
|
162
|
+
const newScope = { symbols: new Map(), parent: this.currentScope, children: [] };
|
|
163
|
+
if (this.currentScope) {
|
|
164
|
+
this.currentScope.children.push(newScope);
|
|
165
|
+
}
|
|
166
|
+
this.scopeStack.push(newScope);
|
|
167
|
+
this.currentScope = newScope;
|
|
168
|
+
} else {
|
|
169
|
+
// In Pass 2, we follow the pre-built children
|
|
170
|
+
if (this.currentScope && this.currentScope.children) {
|
|
171
|
+
const nextScope = this.currentScope.children[this.scopeCounter++];
|
|
172
|
+
if (nextScope) {
|
|
173
|
+
this.scopeStack.push(nextScope);
|
|
174
|
+
this.currentScope = nextScope;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
56
178
|
}
|
|
57
179
|
|
|
58
180
|
popScope() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.currentScope = this.scopeStack[this.scopeStack.length - 1];
|
|
62
|
-
}
|
|
181
|
+
this.scopeStack.pop();
|
|
182
|
+
this.currentScope = this.scopeStack[this.scopeStack.length - 1];
|
|
63
183
|
}
|
|
64
184
|
|
|
65
185
|
addDeclaredSymbol(name, node, sourceFile) {
|
|
@@ -69,176 +189,240 @@ export class ASTAnalyzer {
|
|
|
69
189
|
}
|
|
70
190
|
}
|
|
71
191
|
|
|
72
|
-
resolveSymbol(name) {
|
|
73
|
-
for (let i = this.scopeStack.length - 1; i >= 0; i--) {
|
|
74
|
-
const scope = this.scopeStack[i];
|
|
75
|
-
if (scope.symbols.has(name)) {
|
|
76
|
-
return scope.symbols.get(name);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
192
|
walkAST(node, fileNode, sourceFile) {
|
|
83
|
-
|
|
84
|
-
|
|
193
|
+
const isScopeNode = ts.isBlock(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node) || ts.isArrowFunction(node);
|
|
194
|
+
|
|
195
|
+
let previousCounter = 0;
|
|
85
196
|
if (isScopeNode) {
|
|
197
|
+
if (this.pass === 2) previousCounter = this.scopeCounter;
|
|
198
|
+
this.scopeCounter = 0;
|
|
86
199
|
this.pushScope();
|
|
87
200
|
}
|
|
88
201
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
if (this.pass === 1) {
|
|
203
|
+
// --- PASS 1: DECLARATION INDEXING ---
|
|
204
|
+
switch (node.kind) {
|
|
205
|
+
case ts.SyntaxKind.ImportDeclaration:
|
|
206
|
+
this.handleImportDeclaration(node, fileNode, sourceFile);
|
|
207
|
+
break;
|
|
208
|
+
case ts.SyntaxKind.ExportAssignment:
|
|
209
|
+
fileNode.internalExports.set('default', { type: 'default', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
210
|
+
break;
|
|
211
|
+
case ts.SyntaxKind.VariableStatement:
|
|
212
|
+
this.handleVariableStatement(node, fileNode, sourceFile);
|
|
213
|
+
break;
|
|
214
|
+
case ts.SyntaxKind.FunctionDeclaration:
|
|
215
|
+
case ts.SyntaxKind.ClassDeclaration:
|
|
216
|
+
case ts.SyntaxKind.InterfaceDeclaration:
|
|
217
|
+
case ts.SyntaxKind.TypeAliasDeclaration:
|
|
218
|
+
case ts.SyntaxKind.EnumDeclaration:
|
|
219
|
+
case ts.SyntaxKind.ModuleDeclaration:
|
|
220
|
+
this.handleNamedDeclaration(node, fileNode, sourceFile);
|
|
221
|
+
break;
|
|
222
|
+
case ts.SyntaxKind.ExpressionStatement:
|
|
223
|
+
this.handleAssignmentExpressionPass1(node, fileNode, sourceFile);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// --- PASS 2: REFERENCE TRACKING ---
|
|
228
|
+
switch (node.kind) {
|
|
229
|
+
case ts.SyntaxKind.Identifier:
|
|
230
|
+
if (!this.isDeclarationName(node)) {
|
|
231
|
+
// Scope-Aware Check: Only track if NOT shadowed by a local variable
|
|
232
|
+
if (!this.isLocalShadowing(node.text)) {
|
|
233
|
+
fileNode.instantiatedIdentifiers.add(node.text);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case ts.SyntaxKind.CallExpression:
|
|
238
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
239
|
+
const arg = node.arguments[0];
|
|
240
|
+
if (arg) {
|
|
241
|
+
if (ts.isStringLiteral(arg)) {
|
|
242
|
+
const specifier = arg.text;
|
|
243
|
+
fileNode.dynamicImports.add(specifier);
|
|
244
|
+
fileNode.explicitImports.add(specifier);
|
|
245
|
+
} else {
|
|
246
|
+
// UPGRADE: Handle Dynamic Imports with variables/expressions
|
|
247
|
+
// We mark the file as having "calculated" dynamic imports to be conservative
|
|
248
|
+
fileNode.hasCalculatedDynamicImports = true;
|
|
249
|
+
const exprText = arg.getText(sourceFile);
|
|
250
|
+
if (!fileNode.calculatedDynamicImports) fileNode.calculatedDynamicImports = new Set();
|
|
251
|
+
fileNode.calculatedDynamicImports.add(exprText);
|
|
252
|
+
|
|
253
|
+
// Conservative approach: If we see `import(path)`, any file in the same or sub directory could be a target.
|
|
254
|
+
// This is a simplified version of what knip/entkapp does for plugins.
|
|
255
|
+
fileNode.dynamicImports.add('__DYNAMIC_PATTERN__');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
259
|
+
// UPGRADE: CommonJS require() detection
|
|
260
|
+
const arg = node.arguments[0];
|
|
261
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
262
|
+
const specifier = arg.text;
|
|
263
|
+
fileNode.explicitImports.add(specifier);
|
|
264
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
265
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
266
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
267
|
+
}
|
|
268
|
+
} else if (arg && ts.isTemplateExpression(arg)) {
|
|
269
|
+
// Dynamic require with template literal
|
|
270
|
+
fileNode.hasCalculatedDynamicImports = true;
|
|
271
|
+
const exprText = arg.getText(sourceFile);
|
|
272
|
+
if (!fileNode.calculatedDynamicImports) fileNode.calculatedDynamicImports = [];
|
|
273
|
+
fileNode.calculatedDynamicImports.push({ kind: 'DynamicRequire', expression: exprText, start: arg.getStart(sourceFile) });
|
|
274
|
+
fileNode.dynamicImports.add('__DYNAMIC_PATTERN__');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
case ts.SyntaxKind.PropertyAccessExpression:
|
|
279
|
+
{
|
|
280
|
+
const chain = node.getText(sourceFile);
|
|
281
|
+
fileNode.propertyAccessChains.add(chain);
|
|
282
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
283
|
+
fileNode.instantiatedIdentifiers.add(node.name.text);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case ts.SyntaxKind.CallExpression:
|
|
288
|
+
this.handleCallExpression(node, fileNode, sourceFile);
|
|
289
|
+
break;
|
|
290
|
+
case ts.SyntaxKind.JsxElement:
|
|
291
|
+
case ts.SyntaxKind.JsxSelfClosingElement:
|
|
292
|
+
this.handleJsxElement(node, fileNode, sourceFile);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
130
295
|
}
|
|
131
296
|
|
|
132
297
|
ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
|
|
133
298
|
|
|
134
299
|
if (isScopeNode) {
|
|
135
300
|
this.popScope();
|
|
301
|
+
if (this.pass === 2) this.scopeCounter = previousCounter + 1;
|
|
136
302
|
}
|
|
137
303
|
}
|
|
138
304
|
|
|
305
|
+
isLocalShadowing(name) {
|
|
306
|
+
let scope = this.currentScope;
|
|
307
|
+
// We check all scopes up to the root.
|
|
308
|
+
// If the name is found in a scope that is NOT the root scope, it's definitely shadowing.
|
|
309
|
+
// If it's found in the root scope, it's only shadowing if it's NOT an export.
|
|
310
|
+
while (scope) {
|
|
311
|
+
if (scope.symbols.has(name)) {
|
|
312
|
+
// If we are in the root scope and found the symbol, it's the global declaration.
|
|
313
|
+
// We only consider it shadowing if we are currently in a deeper scope.
|
|
314
|
+
if (scope.parent === null) {
|
|
315
|
+
return this.currentScope !== scope;
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
scope = scope.parent;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
isDeclarationName(node) {
|
|
325
|
+
const parent = node.parent;
|
|
326
|
+
if (!parent) return false;
|
|
327
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node) return true;
|
|
328
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === node) return true;
|
|
329
|
+
if (ts.isClassDeclaration(parent) && parent.name === node) return true;
|
|
330
|
+
if (ts.isInterfaceDeclaration(parent) && parent.name === node) return true;
|
|
331
|
+
if (ts.isEnumDeclaration(parent) && parent.name === node) return true;
|
|
332
|
+
if (ts.isTypeAliasDeclaration(parent) && parent.name === node) return true;
|
|
333
|
+
if (ts.isImportSpecifier(parent) && parent.name === node) return true;
|
|
334
|
+
if (ts.isExportSpecifier(parent) && parent.name === node) return true;
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
139
338
|
handleImportDeclaration(node, fileNode, sourceFile) {
|
|
140
339
|
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
141
340
|
const specifier = node.moduleSpecifier.text;
|
|
142
341
|
fileNode.explicitImports.add(specifier);
|
|
143
342
|
|
|
144
|
-
// Track external package usage for dependency analysis
|
|
145
343
|
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
146
344
|
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
147
345
|
}
|
|
148
346
|
|
|
149
|
-
// 🚨 TARGET BUG 1 & 2: Audit package requirements for unlisted or root dependencies
|
|
150
347
|
if (this.context.workspaceGraph && typeof this.context.workspaceGraph.auditImportSpecifier === 'function') {
|
|
151
348
|
this.context.workspaceGraph.auditImportSpecifier(specifier, sourceFile.fileName);
|
|
152
349
|
}
|
|
153
350
|
|
|
351
|
+
const targetFile = this.resolveAbsoluteTargetFile(specifier, sourceFile.fileName);
|
|
352
|
+
|
|
154
353
|
if (node.importClause) {
|
|
155
|
-
// Initialize global tracking structures on the engine context if missing
|
|
156
354
|
if (!this.context.importUsageRegistry) this.context.importUsageRegistry = new Set();
|
|
157
355
|
|
|
158
|
-
// Cross-file import resolver function
|
|
159
|
-
const resolveAbsoluteTargetFile = (spec) => {
|
|
160
|
-
// If importing a local workspace monorepo module (e.g. '@monorepo/shared')
|
|
161
|
-
if (this.context.workspaceGraph && typeof this.context.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
|
|
162
|
-
if (this.context.workspaceGraph.isLocalWorkspaceSpecifier(spec)) {
|
|
163
|
-
const match = this.context.workspaceGraph.getWorkspacePackageMatch(spec);
|
|
164
|
-
// Grab the first entry point file calculated from its local tsconfig/package.json
|
|
165
|
-
if (match && match.entryPoints && match.entryPoints.length > 0) {
|
|
166
|
-
return match.entryPoints[0];
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// Fallback to relative file layout calculations
|
|
171
|
-
if (spec.startsWith('.')) {
|
|
172
|
-
let resolved = path.resolve(path.dirname(sourceFile.fileName), spec);
|
|
173
|
-
// Append standard extensions if missing from import string
|
|
174
|
-
if (!path.extname(resolved)) {
|
|
175
|
-
if (fs.existsSync(resolved + '.ts')) resolved += '.ts';
|
|
176
|
-
else if (fs.existsSync(resolved + '.tsx')) resolved += '.tsx';
|
|
177
|
-
else if (fs.existsSync(resolved + '.js')) resolved += '.js';
|
|
178
|
-
}
|
|
179
|
-
return resolved;
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
356
|
if (node.importClause.name) {
|
|
185
357
|
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
186
|
-
|
|
187
|
-
// Trace default consumption targets
|
|
188
|
-
const targetFile = resolveAbsoluteTargetFile(specifier);
|
|
189
|
-
if (targetFile) {
|
|
190
|
-
this.context.importUsageRegistry.add(`${targetFile}:default`);
|
|
191
|
-
}
|
|
358
|
+
if (targetFile) this.context.importUsageRegistry.add(`${targetFile}:default`);
|
|
192
359
|
}
|
|
193
360
|
|
|
194
361
|
if (node.importClause.namedBindings) {
|
|
195
362
|
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
196
363
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
197
|
-
|
|
198
|
-
const targetFile = resolveAbsoluteTargetFile(specifier);
|
|
199
|
-
if (targetFile) {
|
|
200
|
-
this.context.importUsageRegistry.add(`${targetFile}:*`);
|
|
201
|
-
}
|
|
364
|
+
if (targetFile) this.context.importUsageRegistry.add(`${targetFile}:*`);
|
|
202
365
|
} else if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
203
366
|
node.importClause.namedBindings.elements.forEach(element => {
|
|
204
367
|
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
205
368
|
fileNode.importedSymbols.add(`${specifier}:${importedName}`);
|
|
206
|
-
|
|
207
|
-
// 🚨 TARGET BUG 3: Map the unique file:symbol consumption token
|
|
208
|
-
const targetFile = resolveAbsoluteTargetFile(specifier);
|
|
209
|
-
if (targetFile) {
|
|
210
|
-
this.context.importUsageRegistry.add(`${targetFile}:${importedName}`);
|
|
211
|
-
}
|
|
369
|
+
if (targetFile) this.context.importUsageRegistry.add(`${targetFile}:${importedName}`);
|
|
212
370
|
});
|
|
213
371
|
}
|
|
214
372
|
}
|
|
373
|
+
} else {
|
|
374
|
+
// Side-Effect Import (import '...')
|
|
375
|
+
// FIX: Mark the target file as used even if no symbols are imported
|
|
376
|
+
if (targetFile) {
|
|
377
|
+
if (!this.context.importUsageRegistry) this.context.importUsageRegistry = new Set();
|
|
378
|
+
this.context.importUsageRegistry.add(`${targetFile}:*`);
|
|
379
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
380
|
+
}
|
|
215
381
|
}
|
|
216
382
|
}
|
|
217
383
|
}
|
|
218
384
|
|
|
385
|
+
resolveAbsoluteTargetFile(spec, sourceFilePath) {
|
|
386
|
+
if (this.context.workspaceGraph && typeof this.context.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
|
|
387
|
+
if (this.context.workspaceGraph.isLocalWorkspaceSpecifier(spec)) {
|
|
388
|
+
const match = this.context.workspaceGraph.getWorkspacePackageMatch(spec);
|
|
389
|
+
if (match && match.entryPoints && match.entryPoints.length > 0) return match.entryPoints[0];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (spec.startsWith('.')) {
|
|
393
|
+
let resolved = path.resolve(path.dirname(sourceFilePath), spec);
|
|
394
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
395
|
+
if (!path.extname(resolved)) {
|
|
396
|
+
for (const ext of extensions) {
|
|
397
|
+
if (fs.existsSync(resolved + ext)) return resolved + ext;
|
|
398
|
+
}
|
|
399
|
+
// Check index files
|
|
400
|
+
for (const ext of extensions) {
|
|
401
|
+
if (fs.existsSync(path.join(resolved, 'index' + ext))) return path.join(resolved, 'index' + ext);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return resolved;
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
219
409
|
handleExportDeclaration(node, fileNode, sourceFile) {
|
|
220
410
|
const specifier = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
|
|
221
411
|
|
|
222
412
|
if (specifier) {
|
|
223
|
-
// Re-export from source: export * from './module' or export { x } from './module'
|
|
224
413
|
fileNode.explicitImports.add(specifier);
|
|
225
|
-
|
|
226
|
-
// Track external package usage from re-exports
|
|
227
414
|
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
228
415
|
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
229
416
|
}
|
|
230
417
|
|
|
231
418
|
if (!node.exportClause) {
|
|
232
|
-
// export * from './module'
|
|
233
419
|
fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
|
|
234
420
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
235
421
|
} else if (ts.isNamespaceExport(node.exportClause)) {
|
|
236
|
-
// export * as name from './module'
|
|
237
422
|
const name = node.exportClause.name.text;
|
|
238
423
|
fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
239
424
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
240
425
|
} else if (ts.isNamedExports(node.exportClause)) {
|
|
241
|
-
// export { x, y as z } from './module'
|
|
242
426
|
node.exportClause.elements.forEach(element => {
|
|
243
427
|
const originalName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
244
428
|
const exportedName = element.name.text;
|
|
@@ -247,7 +431,6 @@ export class ASTAnalyzer {
|
|
|
247
431
|
});
|
|
248
432
|
}
|
|
249
433
|
} else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
250
|
-
// Local named exports: export { x, y as z }
|
|
251
434
|
node.exportClause.elements.forEach(element => {
|
|
252
435
|
const localName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
253
436
|
const exportedName = element.name.text;
|
|
@@ -256,20 +439,40 @@ export class ASTAnalyzer {
|
|
|
256
439
|
}
|
|
257
440
|
}
|
|
258
441
|
|
|
442
|
+
/**
|
|
443
|
+
* UPGRADE 6: Constant Folding for Computed Keys.
|
|
444
|
+
* Resolves the value of constants used as export keys.
|
|
445
|
+
*/
|
|
446
|
+
resolveConstantValue(node, sourceFile) {
|
|
447
|
+
if (ts.isIdentifier(node)) {
|
|
448
|
+
// Look for the symbol in the current scope chain
|
|
449
|
+
let scope = this.currentScope;
|
|
450
|
+
while (scope) {
|
|
451
|
+
if (scope.symbols.has(node.text)) {
|
|
452
|
+
const sym = scope.symbols.get(node.text);
|
|
453
|
+
if (sym.node && ts.isVariableDeclaration(sym.node) && sym.node.initializer) {
|
|
454
|
+
if (ts.isStringLiteral(sym.node.initializer)) {
|
|
455
|
+
return sym.node.initializer.text;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
scope = scope.parent;
|
|
460
|
+
}
|
|
461
|
+
} else if (ts.isStringLiteral(node)) {
|
|
462
|
+
return node.text;
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
259
467
|
handleNamedDeclaration(node, fileNode, sourceFile) {
|
|
260
468
|
if (this.hasExportModifier(node)) {
|
|
261
469
|
const isDefault = node.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
262
470
|
const name = isDefault ? 'default' : (node.name?.text || 'anonymous');
|
|
263
471
|
|
|
264
|
-
// 🚨 TARGET BUG 3 (Part B): Extract exported symbol tokens into your global context map
|
|
265
472
|
if (!this.context.exportRegistry) this.context.exportRegistry = new Map();
|
|
266
473
|
const currentFile = sourceFile.fileName;
|
|
267
|
-
if (!this.context.exportRegistry.has(currentFile))
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
if (name !== 'anonymous') {
|
|
271
|
-
this.context.exportRegistry.get(currentFile).add(name);
|
|
272
|
-
}
|
|
474
|
+
if (!this.context.exportRegistry.has(currentFile)) this.context.exportRegistry.set(currentFile, new Set());
|
|
475
|
+
if (name !== 'anonymous') this.context.exportRegistry.get(currentFile).add(name);
|
|
273
476
|
|
|
274
477
|
const exportInfo = {
|
|
275
478
|
type: ts.SyntaxKind[node.kind].toLowerCase().replace('declaration', ''),
|
|
@@ -279,6 +482,34 @@ export class ASTAnalyzer {
|
|
|
279
482
|
|
|
280
483
|
fileNode.internalExports.set(name, exportInfo);
|
|
281
484
|
|
|
485
|
+
// Extract members from object literals in variable exports
|
|
486
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
|
|
487
|
+
exportInfo.members = node.initializer.properties
|
|
488
|
+
.map(p => {
|
|
489
|
+
let mName = null;
|
|
490
|
+
if (p.name && ts.isIdentifier(p.name)) {
|
|
491
|
+
mName = p.name.text;
|
|
492
|
+
} else if (p.name && ts.isComputedPropertyName(p.name)) {
|
|
493
|
+
// UPGRADE 6: Constant Folding for Computed Keys
|
|
494
|
+
mName = this.resolveConstantValue(p.name.expression, sourceFile);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (!mName) return null;
|
|
498
|
+
|
|
499
|
+
const mLoc = sourceFile.getLineAndCharacterOfPosition(p.getStart(sourceFile));
|
|
500
|
+
fileNode.symbolSourceLocations.set(`${name}.${mName}`, { line: mLoc.line + 1, column: mLoc.character + 1 });
|
|
501
|
+
fileNode.instantiatedIdentifiers.add(mName);
|
|
502
|
+
return {
|
|
503
|
+
name: mName,
|
|
504
|
+
type: 'property',
|
|
505
|
+
isPublic: true, // Object properties are usually public
|
|
506
|
+
start: p.getStart(sourceFile),
|
|
507
|
+
end: p.getEnd()
|
|
508
|
+
};
|
|
509
|
+
})
|
|
510
|
+
.filter(Boolean);
|
|
511
|
+
}
|
|
512
|
+
|
|
282
513
|
if (ts.isEnumDeclaration(node)) {
|
|
283
514
|
exportInfo.members = node.members.map(m => ({
|
|
284
515
|
name: m.name.getText(sourceFile),
|
|
@@ -286,39 +517,23 @@ export class ASTAnalyzer {
|
|
|
286
517
|
start: m.getStart(sourceFile),
|
|
287
518
|
end: m.getEnd()
|
|
288
519
|
}));
|
|
289
|
-
} else
|
|
520
|
+
} else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
290
521
|
exportInfo.members = node.members
|
|
291
522
|
.filter(m => m.name)
|
|
292
|
-
.map(m =>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
name: d.name.getText(sourceFile),
|
|
306
|
-
type: 'variable',
|
|
307
|
-
start: d.getStart(sourceFile),
|
|
308
|
-
end: d.getEnd()
|
|
309
|
-
}));
|
|
310
|
-
} else if (stmt.name) {
|
|
311
|
-
members.push({
|
|
312
|
-
name: stmt.name.getText(sourceFile),
|
|
313
|
-
type: ts.SyntaxKind[stmt.kind].toLowerCase().replace('declaration', ''),
|
|
314
|
-
start: stmt.getStart(sourceFile),
|
|
315
|
-
end: stmt.getEnd()
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|
|
523
|
+
.map(m => {
|
|
524
|
+
const mName = m.name.getText(sourceFile);
|
|
525
|
+
const mLoc = sourceFile.getLineAndCharacterOfPosition(m.getStart(sourceFile));
|
|
526
|
+
fileNode.symbolSourceLocations.set(`${name}.${mName}`, { line: mLoc.line + 1, column: mLoc.character + 1 });
|
|
527
|
+
fileNode.instantiatedIdentifiers.add(mName);
|
|
528
|
+
const isPrivate = m.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
|
|
529
|
+
return {
|
|
530
|
+
name: mName,
|
|
531
|
+
type: ts.SyntaxKind[m.kind].toLowerCase(),
|
|
532
|
+
isPublic: !isPrivate,
|
|
533
|
+
start: m.getStart(sourceFile),
|
|
534
|
+
end: m.getEnd()
|
|
535
|
+
};
|
|
319
536
|
});
|
|
320
|
-
}
|
|
321
|
-
exportInfo.members = members;
|
|
322
537
|
}
|
|
323
538
|
|
|
324
539
|
const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
@@ -333,38 +548,12 @@ export class ASTAnalyzer {
|
|
|
333
548
|
if (this.hasExportModifier(node)) {
|
|
334
549
|
node.declarationList.declarations.forEach(decl => {
|
|
335
550
|
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
336
|
-
|
|
337
|
-
fileNode.internalExports.set(name, {
|
|
338
|
-
type: 'variable',
|
|
339
|
-
start: node.getStart(sourceFile),
|
|
340
|
-
end: node.getEnd()
|
|
341
|
-
});
|
|
342
|
-
const loc = sourceFile.getLineAndCharacterOfPosition(decl.getStart(sourceFile));
|
|
343
|
-
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
344
|
-
this.addDeclaredSymbol(name, decl, sourceFile);
|
|
551
|
+
this.handleNamedDeclaration(decl, fileNode, sourceFile);
|
|
345
552
|
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
346
553
|
decl.name.elements.forEach(element => {
|
|
347
554
|
if(element.name && ts.isIdentifier(element.name)) {
|
|
348
555
|
const name = element.name.text;
|
|
349
|
-
fileNode.internalExports.set(name, {
|
|
350
|
-
type: 'variable',
|
|
351
|
-
start: node.getStart(sourceFile),
|
|
352
|
-
end: node.getEnd()
|
|
353
|
-
});
|
|
354
|
-
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
355
|
-
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
356
|
-
this.addDeclaredSymbol(name, element, sourceFile);
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
} else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
|
|
360
|
-
decl.name.elements.forEach(element => {
|
|
361
|
-
if(ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
362
|
-
const name = element.name.text;
|
|
363
|
-
fileNode.internalExports.set(name, {
|
|
364
|
-
type: 'variable',
|
|
365
|
-
start: node.getStart(sourceFile),
|
|
366
|
-
end: node.getEnd()
|
|
367
|
-
});
|
|
556
|
+
fileNode.internalExports.set(name, { type: 'variable', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
368
557
|
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
369
558
|
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
370
559
|
this.addDeclaredSymbol(name, element, sourceFile);
|
|
@@ -372,74 +561,66 @@ export class ASTAnalyzer {
|
|
|
372
561
|
});
|
|
373
562
|
}
|
|
374
563
|
});
|
|
375
|
-
} else {
|
|
376
|
-
// Non-exported variable declarations also need to be added to scope
|
|
377
|
-
node.declarationList.declarations.forEach(decl => {
|
|
378
|
-
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
379
|
-
this.addDeclaredSymbol(decl.name.text, decl, sourceFile);
|
|
380
|
-
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
381
|
-
decl.name.elements.forEach(element => {
|
|
382
|
-
if (element.name && ts.isIdentifier(element.name)) {
|
|
383
|
-
this.addDeclaredSymbol(element.name.text, element, sourceFile);
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
} else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
|
|
387
|
-
decl.name.elements.forEach(element => {
|
|
388
|
-
if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
389
|
-
this.addDeclaredSymbol(element.name.text, element, sourceFile);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
564
|
}
|
|
395
565
|
}
|
|
396
566
|
|
|
397
567
|
handleCallExpression(node, fileNode, sourceFile) {
|
|
398
|
-
|
|
568
|
+
if (this.context.verbose) console.log(`[AST-DEBUG] Checking CallExpression in ${fileNode.filePath}`);
|
|
569
|
+
// Dynamic import(): import('./module')
|
|
399
570
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
400
571
|
const arg = node.arguments[0];
|
|
401
572
|
if (arg) {
|
|
402
573
|
if (ts.isStringLiteral(arg)) {
|
|
403
|
-
|
|
404
|
-
fileNode.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
574
|
+
const specifier = arg.text;
|
|
575
|
+
fileNode.explicitImports.add(specifier);
|
|
576
|
+
fileNode.dynamicImports.add(specifier);
|
|
577
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
578
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
579
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
408
580
|
}
|
|
409
581
|
} else {
|
|
410
|
-
//
|
|
411
|
-
if (fileNode.calculatedDynamicImports)
|
|
412
|
-
|
|
413
|
-
|
|
582
|
+
// Non-literal dynamic import
|
|
583
|
+
if (!fileNode.calculatedDynamicImports) fileNode.calculatedDynamicImports = [];
|
|
584
|
+
fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
|
|
585
|
+
|
|
586
|
+
// UPGRADE 2: Try to perform Glob-Analysis on Template Strings
|
|
587
|
+
this.handlePartialEvaluation(arg, fileNode, sourceFile);
|
|
414
588
|
}
|
|
415
589
|
}
|
|
416
590
|
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
591
|
+
if (this.context.verbose) console.log(`[AST-DEBUG] Found require() call in ${fileNode.filePath}`);
|
|
417
592
|
const arg = node.arguments[0];
|
|
418
593
|
if (arg && ts.isStringLiteral(arg)) {
|
|
594
|
+
if (this.context.verbose) console.log(`[AST-DEBUG] Added import: ${arg.text}`);
|
|
419
595
|
fileNode.explicitImports.add(arg.text);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* UPGRADE 2: Partial Evaluation for dynamic imports.
|
|
602
|
+
* Detects pattern like import(`./dynamic/${name}.js`) and marks the directory as potentially used.
|
|
603
|
+
*/
|
|
604
|
+
handlePartialEvaluation(node, fileNode, sourceFile) {
|
|
605
|
+
if (ts.isTemplateExpression(node)) {
|
|
606
|
+
const head = node.head.text;
|
|
607
|
+
if (head.startsWith('./') || head.startsWith('../')) {
|
|
608
|
+
// Pattern: `./directory/${variable}`
|
|
609
|
+
const dirPath = path.dirname(head);
|
|
610
|
+
if (dirPath && dirPath !== '.') {
|
|
611
|
+
if (!fileNode.globImports) fileNode.globImports = new Set();
|
|
612
|
+
fileNode.globImports.add(dirPath);
|
|
423
613
|
}
|
|
424
614
|
}
|
|
425
615
|
}
|
|
426
616
|
}
|
|
427
617
|
|
|
428
618
|
handleJsxElement(node, fileNode, sourceFile) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const tagName = getElementName(node.openingElement.tagName);
|
|
436
|
-
fileNode.jsxComponents.add(tagName);
|
|
437
|
-
|
|
438
|
-
node.openingElement.attributes.properties.forEach(attr => {
|
|
439
|
-
if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name)) {
|
|
440
|
-
fileNode.jsxProps.add(`${tagName}:${attr.name.text}`);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
619
|
+
const tagName = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName;
|
|
620
|
+
if (ts.isIdentifier(tagName)) {
|
|
621
|
+
fileNode.jsxComponents.add(tagName.text);
|
|
622
|
+
fileNode.instantiatedIdentifiers.add(tagName.text);
|
|
623
|
+
}
|
|
443
624
|
}
|
|
444
625
|
|
|
445
626
|
handleDecorator(node, fileNode, sourceFile) {
|
|
@@ -449,19 +630,15 @@ export class ASTAnalyzer {
|
|
|
449
630
|
if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
|
|
450
631
|
return 'unknown';
|
|
451
632
|
};
|
|
452
|
-
|
|
453
633
|
const decoratorName = getDecoratorName(node.expression);
|
|
454
634
|
fileNode.decorators.add(decoratorName);
|
|
455
|
-
|
|
456
|
-
// Optionally, extract decorator arguments
|
|
457
|
-
if (ts.isCallExpression(node.expression)) {
|
|
458
|
-
node.expression.arguments.forEach(arg => {
|
|
459
|
-
// Further analysis of arguments can be done here if needed
|
|
460
|
-
});
|
|
461
|
-
}
|
|
635
|
+
fileNode.instantiatedIdentifiers.add(decoratorName);
|
|
462
636
|
}
|
|
463
637
|
|
|
464
638
|
hasExportModifier(node) {
|
|
639
|
+
if (ts.isVariableDeclaration(node)) {
|
|
640
|
+
return this.hasExportModifier(node.parent.parent);
|
|
641
|
+
}
|
|
465
642
|
return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
466
643
|
}
|
|
467
644
|
|
|
@@ -477,17 +654,54 @@ export class ASTAnalyzer {
|
|
|
477
654
|
}
|
|
478
655
|
}
|
|
479
656
|
|
|
480
|
-
/**
|
|
481
|
-
* Extracts the root npm package name from an import specifier.
|
|
482
|
-
* Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
|
|
483
|
-
*/
|
|
484
657
|
_extractPackageName(specifier) {
|
|
485
658
|
if (specifier.startsWith('@')) {
|
|
486
|
-
// Scoped package: @scope/name or @scope/name/subpath
|
|
487
659
|
const parts = specifier.split('/');
|
|
488
660
|
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
489
661
|
}
|
|
490
|
-
// Regular package: name or name/subpath
|
|
491
662
|
return specifier.split('/')[0];
|
|
492
663
|
}
|
|
664
|
+
|
|
665
|
+
// UPGRADE: CommonJS module.exports / exports detection
|
|
666
|
+
handleAssignmentExpressionPass1(node, fileNode, sourceFile) {
|
|
667
|
+
if (ts.isExpressionStatement(node)) {
|
|
668
|
+
const expr = node.expression;
|
|
669
|
+
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
670
|
+
const left = expr.left;
|
|
671
|
+
const right = expr.right;
|
|
672
|
+
|
|
673
|
+
if (ts.isPropertyAccessExpression(left)) {
|
|
674
|
+
const obj = left.expression;
|
|
675
|
+
const prop = left.name;
|
|
676
|
+
|
|
677
|
+
// module.exports = ...
|
|
678
|
+
if (ts.isIdentifier(obj) && obj.text === 'module' &&
|
|
679
|
+
ts.isIdentifier(prop) && prop.text === 'exports') {
|
|
680
|
+
fileNode.internalExports.set('default', { type: 'default', start: left.getStart(sourceFile), end: left.getEnd() });
|
|
681
|
+
|
|
682
|
+
// Handle module.exports = { a, b }
|
|
683
|
+
if (ts.isObjectLiteralExpression(right)) {
|
|
684
|
+
right.properties.forEach(p => {
|
|
685
|
+
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) {
|
|
686
|
+
fileNode.internalExports.set(p.name.text, { type: 'variable', start: p.getStart(sourceFile), end: p.getEnd() });
|
|
687
|
+
} else if (ts.isShorthandPropertyAssignment(p)) {
|
|
688
|
+
fileNode.internalExports.set(p.name.text, { type: 'variable', start: p.getStart(sourceFile), end: p.getEnd() });
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// exports.name = ...
|
|
694
|
+
else if (ts.isIdentifier(obj) && obj.text === 'exports') {
|
|
695
|
+
fileNode.internalExports.set(prop.text, { type: 'variable', start: left.getStart(sourceFile), end: left.getEnd() });
|
|
696
|
+
}
|
|
697
|
+
// module.exports.name = ...
|
|
698
|
+
else if (ts.isPropertyAccessExpression(obj) &&
|
|
699
|
+
ts.isIdentifier(obj.expression) && obj.expression.text === 'module' &&
|
|
700
|
+
ts.isIdentifier(obj.name) && obj.name.text === 'exports') {
|
|
701
|
+
fileNode.internalExports.set(prop.text, { type: 'variable', start: left.getStart(sourceFile), end: left.getEnd() });
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
493
707
|
}
|