entkapp 4.5.1 → 5.1.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/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 +481 -252
- package/src/ast/AdvancedAnalysis.js +6 -5
- package/src/ast/BarrelParser.js +39 -30
- package/src/ast/DeadCodeDetector.js +30 -18
- package/src/ast/MagicDetector.js +1 -1
- package/src/ast/OxcAnalyzer.js +335 -273
- package/src/index.js +279 -365
- 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 +44 -44
- package/src/resolution/ConfigLoader.js +2 -85
- package/src/resolution/DepencyResolver.js +28 -121
- package/src/resolution/EntryPointDetector.js +134 -0
- package/src/resolution/PathMapper.js +31 -107
- package/src/resolution/TSConfigLoader.js +76 -0
- package/src/resolution/WorkSpaceGraph.js +6 -472
- 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,251 @@ 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
|
+
|
|
238
|
+
case ts.SyntaxKind.CallExpression:
|
|
239
|
+
// FIX: Führe zuerst die generische Aufrufanalyse aus, die vorher blockiert war
|
|
240
|
+
this.handleCallExpression(node, fileNode, sourceFile);
|
|
241
|
+
|
|
242
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
243
|
+
const arg = node.arguments[0];
|
|
244
|
+
if (arg) {
|
|
245
|
+
if (ts.isStringLiteral(arg)) {
|
|
246
|
+
const specifier = arg.text;
|
|
247
|
+
fileNode.dynamicImports.add(specifier);
|
|
248
|
+
fileNode.explicitImports.add(specifier);
|
|
249
|
+
} else {
|
|
250
|
+
// UPGRADE: Handle Dynamic Imports with variables/expressions
|
|
251
|
+
fileNode.hasCalculatedDynamicImports = true;
|
|
252
|
+
const exprText = arg.getText(sourceFile);
|
|
253
|
+
if (!fileNode.calculatedDynamicImports || Array.isArray(fileNode.calculatedDynamicImports)) {
|
|
254
|
+
fileNode.calculatedDynamicImports = new Set();
|
|
255
|
+
}
|
|
256
|
+
fileNode.calculatedDynamicImports.add(exprText);
|
|
257
|
+
fileNode.dynamicImports.add('__DYNAMIC_PATTERN__');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
261
|
+
// UPGRADE: CommonJS require() detection
|
|
262
|
+
const arg = node.arguments[0];
|
|
263
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
264
|
+
const specifier = arg.text;
|
|
265
|
+
fileNode.explicitImports.add(specifier);
|
|
266
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
267
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
268
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
269
|
+
}
|
|
270
|
+
} else if (arg && ts.isTemplateExpression(arg)) {
|
|
271
|
+
// Dynamic require with template literal
|
|
272
|
+
fileNode.hasCalculatedDynamicImports = true;
|
|
273
|
+
const exprText = arg.getText(sourceFile);
|
|
274
|
+
|
|
275
|
+
// FIX: Typ-Integrität als Set erzwingen (kein Array [])
|
|
276
|
+
if (!fileNode.calculatedDynamicImports || Array.isArray(fileNode.calculatedDynamicImports)) {
|
|
277
|
+
fileNode.calculatedDynamicImports = new Set();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// FIX: .add() statt .push() verwenden, um Absturz in Pass 2 zu verhindern
|
|
281
|
+
fileNode.calculatedDynamicImports.add({
|
|
282
|
+
kind: 'DynamicRequire',
|
|
283
|
+
expression: exprText,
|
|
284
|
+
start: arg.getStart(sourceFile)
|
|
285
|
+
});
|
|
286
|
+
fileNode.dynamicImports.add('__DYNAMIC_PATTERN__');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
|
|
291
|
+
case ts.SyntaxKind.PropertyAccessExpression:
|
|
292
|
+
{
|
|
293
|
+
const chain = node.getText(sourceFile);
|
|
294
|
+
fileNode.propertyAccessChains.add(chain);
|
|
295
|
+
if (node.name && ts.isIdentifier(node.name)) {
|
|
296
|
+
fileNode.instantiatedIdentifiers.add(node.name.text);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case ts.SyntaxKind.JsxElement:
|
|
302
|
+
case ts.SyntaxKind.JsxSelfClosingElement:
|
|
303
|
+
this.handleJsxElement(node, fileNode, sourceFile);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
130
306
|
}
|
|
131
307
|
|
|
132
308
|
ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
|
|
133
309
|
|
|
134
310
|
if (isScopeNode) {
|
|
135
311
|
this.popScope();
|
|
312
|
+
if (this.pass === 2) this.scopeCounter = previousCounter + 1;
|
|
136
313
|
}
|
|
137
314
|
}
|
|
138
315
|
|
|
316
|
+
isLocalShadowing(name) {
|
|
317
|
+
let scope = this.currentScope;
|
|
318
|
+
// We check all scopes up to the root.
|
|
319
|
+
// If the name is found in a scope that is NOT the root scope, it's definitely shadowing.
|
|
320
|
+
// If it's found in the root scope, it's only shadowing if it's NOT an export.
|
|
321
|
+
while (scope) {
|
|
322
|
+
if (scope.symbols.has(name)) {
|
|
323
|
+
// If we are in the root scope and found the symbol, it's the global declaration.
|
|
324
|
+
// We only consider it shadowing if we are currently in a deeper scope.
|
|
325
|
+
if (scope.parent === null) {
|
|
326
|
+
return this.currentScope !== scope;
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
scope = scope.parent;
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
isDeclarationName(node) {
|
|
336
|
+
const parent = node.parent;
|
|
337
|
+
if (!parent) return false;
|
|
338
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node) return true;
|
|
339
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === node) return true;
|
|
340
|
+
if (ts.isClassDeclaration(parent) && parent.name === node) return true;
|
|
341
|
+
if (ts.isInterfaceDeclaration(parent) && parent.name === node) return true;
|
|
342
|
+
if (ts.isEnumDeclaration(parent) && parent.name === node) return true;
|
|
343
|
+
if (ts.isTypeAliasDeclaration(parent) && parent.name === node) return true;
|
|
344
|
+
if (ts.isImportSpecifier(parent) && parent.name === node) return true;
|
|
345
|
+
if (ts.isExportSpecifier(parent) && parent.name === node) return true;
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
139
349
|
handleImportDeclaration(node, fileNode, sourceFile) {
|
|
140
350
|
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
141
351
|
const specifier = node.moduleSpecifier.text;
|
|
142
352
|
fileNode.explicitImports.add(specifier);
|
|
143
353
|
|
|
144
|
-
// Track external package usage for dependency analysis
|
|
145
354
|
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
146
355
|
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
147
356
|
}
|
|
148
357
|
|
|
149
|
-
// 🚨 TARGET BUG 1 & 2: Audit package requirements for unlisted or root dependencies
|
|
150
358
|
if (this.context.workspaceGraph && typeof this.context.workspaceGraph.auditImportSpecifier === 'function') {
|
|
151
359
|
this.context.workspaceGraph.auditImportSpecifier(specifier, sourceFile.fileName);
|
|
152
360
|
}
|
|
153
361
|
|
|
362
|
+
const targetFile = this.resolveAbsoluteTargetFile(specifier, sourceFile.fileName);
|
|
363
|
+
|
|
154
364
|
if (node.importClause) {
|
|
155
|
-
// Initialize global tracking structures on the engine context if missing
|
|
156
365
|
if (!this.context.importUsageRegistry) this.context.importUsageRegistry = new Set();
|
|
157
366
|
|
|
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
367
|
if (node.importClause.name) {
|
|
185
368
|
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
|
-
}
|
|
369
|
+
if (targetFile) this.context.importUsageRegistry.add(`${targetFile}:default`);
|
|
192
370
|
}
|
|
193
371
|
|
|
194
372
|
if (node.importClause.namedBindings) {
|
|
195
373
|
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
196
374
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
197
|
-
|
|
198
|
-
const targetFile = resolveAbsoluteTargetFile(specifier);
|
|
199
|
-
if (targetFile) {
|
|
200
|
-
this.context.importUsageRegistry.add(`${targetFile}:*`);
|
|
201
|
-
}
|
|
375
|
+
if (targetFile) this.context.importUsageRegistry.add(`${targetFile}:*`);
|
|
202
376
|
} else if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
203
377
|
node.importClause.namedBindings.elements.forEach(element => {
|
|
204
378
|
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
205
379
|
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
|
-
}
|
|
380
|
+
if (targetFile) this.context.importUsageRegistry.add(`${targetFile}:${importedName}`);
|
|
212
381
|
});
|
|
213
382
|
}
|
|
214
383
|
}
|
|
384
|
+
} else {
|
|
385
|
+
// Side-Effect Import (import '...')
|
|
386
|
+
// FIX: Mark the target file as used even if no symbols are imported
|
|
387
|
+
if (targetFile) {
|
|
388
|
+
if (!this.context.importUsageRegistry) this.context.importUsageRegistry = new Set();
|
|
389
|
+
this.context.importUsageRegistry.add(`${targetFile}:*`);
|
|
390
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
391
|
+
}
|
|
215
392
|
}
|
|
216
393
|
}
|
|
217
394
|
}
|
|
218
395
|
|
|
396
|
+
resolveAbsoluteTargetFile(spec, sourceFilePath) {
|
|
397
|
+
if (this.context.workspaceGraph && typeof this.context.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
|
|
398
|
+
if (this.context.workspaceGraph.isLocalWorkspaceSpecifier(spec)) {
|
|
399
|
+
const match = this.context.workspaceGraph.getWorkspacePackageMatch(spec);
|
|
400
|
+
if (match && match.entryPoints && match.entryPoints.length > 0) return match.entryPoints[0];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (spec.startsWith('.')) {
|
|
404
|
+
let resolved = path.resolve(path.dirname(sourceFilePath), spec);
|
|
405
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
406
|
+
if (!path.extname(resolved)) {
|
|
407
|
+
for (const ext of extensions) {
|
|
408
|
+
if (fs.existsSync(resolved + ext)) return resolved + ext;
|
|
409
|
+
}
|
|
410
|
+
// Check index files
|
|
411
|
+
for (const ext of extensions) {
|
|
412
|
+
if (fs.existsSync(path.join(resolved, 'index' + ext))) return path.join(resolved, 'index' + ext);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return resolved;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
219
420
|
handleExportDeclaration(node, fileNode, sourceFile) {
|
|
220
421
|
const specifier = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
|
|
221
422
|
|
|
222
423
|
if (specifier) {
|
|
223
|
-
// Re-export from source: export * from './module' or export { x } from './module'
|
|
224
424
|
fileNode.explicitImports.add(specifier);
|
|
225
|
-
|
|
226
|
-
// Track external package usage from re-exports
|
|
227
425
|
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
228
426
|
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
229
427
|
}
|
|
230
428
|
|
|
231
429
|
if (!node.exportClause) {
|
|
232
|
-
// export * from './module'
|
|
233
430
|
fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
|
|
234
431
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
235
432
|
} else if (ts.isNamespaceExport(node.exportClause)) {
|
|
236
|
-
// export * as name from './module'
|
|
237
433
|
const name = node.exportClause.name.text;
|
|
238
434
|
fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
239
435
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
240
436
|
} else if (ts.isNamedExports(node.exportClause)) {
|
|
241
|
-
// export { x, y as z } from './module'
|
|
242
437
|
node.exportClause.elements.forEach(element => {
|
|
243
438
|
const originalName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
244
439
|
const exportedName = element.name.text;
|
|
@@ -247,7 +442,6 @@ export class ASTAnalyzer {
|
|
|
247
442
|
});
|
|
248
443
|
}
|
|
249
444
|
} else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
250
|
-
// Local named exports: export { x, y as z }
|
|
251
445
|
node.exportClause.elements.forEach(element => {
|
|
252
446
|
const localName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
253
447
|
const exportedName = element.name.text;
|
|
@@ -256,20 +450,40 @@ export class ASTAnalyzer {
|
|
|
256
450
|
}
|
|
257
451
|
}
|
|
258
452
|
|
|
453
|
+
/**
|
|
454
|
+
* UPGRADE 6: Constant Folding for Computed Keys.
|
|
455
|
+
* Resolves the value of constants used as export keys.
|
|
456
|
+
*/
|
|
457
|
+
resolveConstantValue(node, sourceFile) {
|
|
458
|
+
if (ts.isIdentifier(node)) {
|
|
459
|
+
// Look for the symbol in the current scope chain
|
|
460
|
+
let scope = this.currentScope;
|
|
461
|
+
while (scope) {
|
|
462
|
+
if (scope.symbols.has(node.text)) {
|
|
463
|
+
const sym = scope.symbols.get(node.text);
|
|
464
|
+
if (sym.node && ts.isVariableDeclaration(sym.node) && sym.node.initializer) {
|
|
465
|
+
if (ts.isStringLiteral(sym.node.initializer)) {
|
|
466
|
+
return sym.node.initializer.text;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
scope = scope.parent;
|
|
471
|
+
}
|
|
472
|
+
} else if (ts.isStringLiteral(node)) {
|
|
473
|
+
return node.text;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
259
478
|
handleNamedDeclaration(node, fileNode, sourceFile) {
|
|
260
479
|
if (this.hasExportModifier(node)) {
|
|
261
480
|
const isDefault = node.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
262
481
|
const name = isDefault ? 'default' : (node.name?.text || 'anonymous');
|
|
263
482
|
|
|
264
|
-
// 🚨 TARGET BUG 3 (Part B): Extract exported symbol tokens into your global context map
|
|
265
483
|
if (!this.context.exportRegistry) this.context.exportRegistry = new Map();
|
|
266
484
|
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
|
-
}
|
|
485
|
+
if (!this.context.exportRegistry.has(currentFile)) this.context.exportRegistry.set(currentFile, new Set());
|
|
486
|
+
if (name !== 'anonymous') this.context.exportRegistry.get(currentFile).add(name);
|
|
273
487
|
|
|
274
488
|
const exportInfo = {
|
|
275
489
|
type: ts.SyntaxKind[node.kind].toLowerCase().replace('declaration', ''),
|
|
@@ -279,6 +493,34 @@ export class ASTAnalyzer {
|
|
|
279
493
|
|
|
280
494
|
fileNode.internalExports.set(name, exportInfo);
|
|
281
495
|
|
|
496
|
+
// Extract members from object literals in variable exports
|
|
497
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
|
|
498
|
+
exportInfo.members = node.initializer.properties
|
|
499
|
+
.map(p => {
|
|
500
|
+
let mName = null;
|
|
501
|
+
if (p.name && ts.isIdentifier(p.name)) {
|
|
502
|
+
mName = p.name.text;
|
|
503
|
+
} else if (p.name && ts.isComputedPropertyName(p.name)) {
|
|
504
|
+
// UPGRADE 6: Constant Folding for Computed Keys
|
|
505
|
+
mName = this.resolveConstantValue(p.name.expression, sourceFile);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!mName) return null;
|
|
509
|
+
|
|
510
|
+
const mLoc = sourceFile.getLineAndCharacterOfPosition(p.getStart(sourceFile));
|
|
511
|
+
fileNode.symbolSourceLocations.set(`${name}.${mName}`, { line: mLoc.line + 1, column: mLoc.character + 1 });
|
|
512
|
+
fileNode.instantiatedIdentifiers.add(mName);
|
|
513
|
+
return {
|
|
514
|
+
name: mName,
|
|
515
|
+
type: 'property',
|
|
516
|
+
isPublic: true, // Object properties are usually public
|
|
517
|
+
start: p.getStart(sourceFile),
|
|
518
|
+
end: p.getEnd()
|
|
519
|
+
};
|
|
520
|
+
})
|
|
521
|
+
.filter(Boolean);
|
|
522
|
+
}
|
|
523
|
+
|
|
282
524
|
if (ts.isEnumDeclaration(node)) {
|
|
283
525
|
exportInfo.members = node.members.map(m => ({
|
|
284
526
|
name: m.name.getText(sourceFile),
|
|
@@ -286,39 +528,23 @@ export class ASTAnalyzer {
|
|
|
286
528
|
start: m.getStart(sourceFile),
|
|
287
529
|
end: m.getEnd()
|
|
288
530
|
}));
|
|
289
|
-
} else
|
|
531
|
+
} else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
290
532
|
exportInfo.members = node.members
|
|
291
533
|
.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
|
-
}
|
|
534
|
+
.map(m => {
|
|
535
|
+
const mName = m.name.getText(sourceFile);
|
|
536
|
+
const mLoc = sourceFile.getLineAndCharacterOfPosition(m.getStart(sourceFile));
|
|
537
|
+
fileNode.symbolSourceLocations.set(`${name}.${mName}`, { line: mLoc.line + 1, column: mLoc.character + 1 });
|
|
538
|
+
fileNode.instantiatedIdentifiers.add(mName);
|
|
539
|
+
const isPrivate = m.modifiers?.some(mod => mod.kind === ts.SyntaxKind.PrivateKeyword);
|
|
540
|
+
return {
|
|
541
|
+
name: mName,
|
|
542
|
+
type: ts.SyntaxKind[m.kind].toLowerCase(),
|
|
543
|
+
isPublic: !isPrivate,
|
|
544
|
+
start: m.getStart(sourceFile),
|
|
545
|
+
end: m.getEnd()
|
|
546
|
+
};
|
|
319
547
|
});
|
|
320
|
-
}
|
|
321
|
-
exportInfo.members = members;
|
|
322
548
|
}
|
|
323
549
|
|
|
324
550
|
const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
@@ -333,38 +559,12 @@ export class ASTAnalyzer {
|
|
|
333
559
|
if (this.hasExportModifier(node)) {
|
|
334
560
|
node.declarationList.declarations.forEach(decl => {
|
|
335
561
|
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);
|
|
562
|
+
this.handleNamedDeclaration(decl, fileNode, sourceFile);
|
|
345
563
|
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
346
564
|
decl.name.elements.forEach(element => {
|
|
347
565
|
if(element.name && ts.isIdentifier(element.name)) {
|
|
348
566
|
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
|
-
});
|
|
567
|
+
fileNode.internalExports.set(name, { type: 'variable', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
368
568
|
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
369
569
|
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
370
570
|
this.addDeclaredSymbol(name, element, sourceFile);
|
|
@@ -372,74 +572,70 @@ export class ASTAnalyzer {
|
|
|
372
572
|
});
|
|
373
573
|
}
|
|
374
574
|
});
|
|
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
575
|
}
|
|
395
576
|
}
|
|
396
577
|
|
|
397
578
|
handleCallExpression(node, fileNode, sourceFile) {
|
|
398
|
-
|
|
579
|
+
if (this.context.verbose) console.log(`[AST-DEBUG] Checking CallExpression in ${fileNode.filePath}`);
|
|
580
|
+
// Dynamic import(): import('./module')
|
|
399
581
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
400
582
|
const arg = node.arguments[0];
|
|
401
583
|
if (arg) {
|
|
402
584
|
if (ts.isStringLiteral(arg)) {
|
|
403
|
-
|
|
404
|
-
fileNode.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
585
|
+
const specifier = arg.text;
|
|
586
|
+
fileNode.explicitImports.add(specifier);
|
|
587
|
+
fileNode.dynamicImports.add(specifier);
|
|
588
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
589
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
590
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
408
591
|
}
|
|
409
592
|
} else {
|
|
410
|
-
//
|
|
411
|
-
if (fileNode.calculatedDynamicImports) {
|
|
412
|
-
fileNode.calculatedDynamicImports
|
|
593
|
+
// Non-literal dynamic import
|
|
594
|
+
if (!Array.isArray(fileNode.calculatedDynamicImports)) {
|
|
595
|
+
fileNode.calculatedDynamicImports = [];
|
|
413
596
|
}
|
|
597
|
+
fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
|
|
598
|
+
|
|
599
|
+
// UPGRADE 2: Try to perform Glob-Analysis on Template Strings
|
|
600
|
+
this.handlePartialEvaluation(arg, fileNode, sourceFile);
|
|
414
601
|
}
|
|
415
602
|
}
|
|
416
603
|
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
604
|
+
if (this.context.verbose) console.log(`[AST-DEBUG] Found require() call in ${fileNode.filePath}`);
|
|
417
605
|
const arg = node.arguments[0];
|
|
418
606
|
if (arg && ts.isStringLiteral(arg)) {
|
|
607
|
+
if (this.context.verbose) console.log(`[AST-DEBUG] Added import: ${arg.text}`);
|
|
419
608
|
fileNode.explicitImports.add(arg.text);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* UPGRADE 2: Partial Evaluation for dynamic imports.
|
|
615
|
+
* Detects pattern like import(`./dynamic/${name}.js`) and marks the directory as potentially used.
|
|
616
|
+
*/
|
|
617
|
+
handlePartialEvaluation(node, fileNode, sourceFile) {
|
|
618
|
+
if (ts.isTemplateExpression(node)) {
|
|
619
|
+
const head = node.head.text;
|
|
620
|
+
if (head.startsWith('./') || head.startsWith('../')) {
|
|
621
|
+
// FIX: Typ-Integrität als Set erzwingen, kein Array!
|
|
622
|
+
if (!fileNode.globImports || Array.isArray(fileNode.globImports)) {
|
|
623
|
+
fileNode.globImports = new Set();
|
|
624
|
+
}
|
|
625
|
+
const dirPath = path.dirname(head);
|
|
626
|
+
if (dirPath && dirPath !== '.') {
|
|
627
|
+
fileNode.globImports.add(dirPath); // Jetzt klappt .add() garantiert!
|
|
423
628
|
}
|
|
424
629
|
}
|
|
425
630
|
}
|
|
426
631
|
}
|
|
427
632
|
|
|
428
633
|
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
|
-
});
|
|
634
|
+
const tagName = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName;
|
|
635
|
+
if (ts.isIdentifier(tagName)) {
|
|
636
|
+
fileNode.jsxComponents.add(tagName.text);
|
|
637
|
+
fileNode.instantiatedIdentifiers.add(tagName.text);
|
|
638
|
+
}
|
|
443
639
|
}
|
|
444
640
|
|
|
445
641
|
handleDecorator(node, fileNode, sourceFile) {
|
|
@@ -449,19 +645,15 @@ export class ASTAnalyzer {
|
|
|
449
645
|
if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
|
|
450
646
|
return 'unknown';
|
|
451
647
|
};
|
|
452
|
-
|
|
453
648
|
const decoratorName = getDecoratorName(node.expression);
|
|
454
649
|
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
|
-
}
|
|
650
|
+
fileNode.instantiatedIdentifiers.add(decoratorName);
|
|
462
651
|
}
|
|
463
652
|
|
|
464
653
|
hasExportModifier(node) {
|
|
654
|
+
if (ts.isVariableDeclaration(node)) {
|
|
655
|
+
return this.hasExportModifier(node.parent.parent);
|
|
656
|
+
}
|
|
465
657
|
return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
466
658
|
}
|
|
467
659
|
|
|
@@ -477,17 +669,54 @@ export class ASTAnalyzer {
|
|
|
477
669
|
}
|
|
478
670
|
}
|
|
479
671
|
|
|
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
672
|
_extractPackageName(specifier) {
|
|
485
673
|
if (specifier.startsWith('@')) {
|
|
486
|
-
// Scoped package: @scope/name or @scope/name/subpath
|
|
487
674
|
const parts = specifier.split('/');
|
|
488
675
|
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
489
676
|
}
|
|
490
|
-
// Regular package: name or name/subpath
|
|
491
677
|
return specifier.split('/')[0];
|
|
492
678
|
}
|
|
679
|
+
|
|
680
|
+
// UPGRADE: CommonJS module.exports / exports detection
|
|
681
|
+
handleAssignmentExpressionPass1(node, fileNode, sourceFile) {
|
|
682
|
+
if (ts.isExpressionStatement(node)) {
|
|
683
|
+
const expr = node.expression;
|
|
684
|
+
if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
685
|
+
const left = expr.left;
|
|
686
|
+
const right = expr.right;
|
|
687
|
+
|
|
688
|
+
if (ts.isPropertyAccessExpression(left)) {
|
|
689
|
+
const obj = left.expression;
|
|
690
|
+
const prop = left.name;
|
|
691
|
+
|
|
692
|
+
// module.exports = ...
|
|
693
|
+
if (ts.isIdentifier(obj) && obj.text === 'module' &&
|
|
694
|
+
ts.isIdentifier(prop) && prop.text === 'exports') {
|
|
695
|
+
fileNode.internalExports.set('default', { type: 'default', start: left.getStart(sourceFile), end: left.getEnd() });
|
|
696
|
+
|
|
697
|
+
// Handle module.exports = { a, b }
|
|
698
|
+
if (ts.isObjectLiteralExpression(right)) {
|
|
699
|
+
right.properties.forEach(p => {
|
|
700
|
+
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name)) {
|
|
701
|
+
fileNode.internalExports.set(p.name.text, { type: 'variable', start: p.getStart(sourceFile), end: p.getEnd() });
|
|
702
|
+
} else if (ts.isShorthandPropertyAssignment(p)) {
|
|
703
|
+
fileNode.internalExports.set(p.name.text, { type: 'variable', start: p.getStart(sourceFile), end: p.getEnd() });
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// exports.name = ...
|
|
709
|
+
else if (ts.isIdentifier(obj) && obj.text === 'exports') {
|
|
710
|
+
fileNode.internalExports.set(prop.text, { type: 'variable', start: left.getStart(sourceFile), end: left.getEnd() });
|
|
711
|
+
}
|
|
712
|
+
// module.exports.name = ...
|
|
713
|
+
else if (ts.isPropertyAccessExpression(obj) &&
|
|
714
|
+
ts.isIdentifier(obj.expression) && obj.expression.text === 'module' &&
|
|
715
|
+
ts.isIdentifier(obj.name) && obj.name.text === 'exports') {
|
|
716
|
+
fileNode.internalExports.set(prop.text, { type: 'variable', start: left.getStart(sourceFile), end: left.getEnd() });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
493
722
|
}
|