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.
@@ -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
- this.currentScope = { symbols: new Map(), parent: null };
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
- this.scopeStack.pop(); // Pop the global scope
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
- * Alias for walkAST used by WorkerTaskRunner (legacy API compatibility).
47
- */
48
- walkNode(node, sourceFile, fileNode) {
49
- return this.walkAST(node, fileNode, sourceFile);
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
- const newScope = { symbols: new Map(), parent: this.currentScope };
54
- this.scopeStack.push(newScope);
55
- this.currentScope = newScope;
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
- if (this.scopeStack.length > 1) {
60
- this.scopeStack.pop();
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
- // Handle scope entry for blocks, functions, classes, etc.
84
- const isScopeNode = ts.isBlock(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node);
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
- switch (node.kind) {
90
- case ts.SyntaxKind.ImportDeclaration:
91
- this.handleImportDeclaration(node, fileNode, sourceFile);
92
- break;
93
- case ts.SyntaxKind.ExportDeclaration:
94
- this.handleExportDeclaration(node, fileNode, sourceFile);
95
- break;
96
- case ts.SyntaxKind.ExportAssignment:
97
- fileNode.internalExports.set('default', { type: 'default', start: node.getStart(sourceFile), end: node.getEnd() });
98
- break;
99
- case ts.SyntaxKind.VariableStatement:
100
- this.handleVariableStatement(node, fileNode, sourceFile);
101
- break;
102
- case ts.SyntaxKind.FunctionDeclaration:
103
- case ts.SyntaxKind.ClassDeclaration:
104
- case ts.SyntaxKind.InterfaceDeclaration:
105
- case ts.SyntaxKind.TypeAliasDeclaration:
106
- case ts.SyntaxKind.EnumDeclaration:
107
- case ts.SyntaxKind.ModuleDeclaration:
108
- this.handleNamedDeclaration(node, fileNode, sourceFile);
109
- break;
110
- case ts.SyntaxKind.Identifier:
111
- fileNode.instantiatedIdentifiers.add(node.text);
112
- break;
113
- case ts.SyntaxKind.StringLiteral:
114
- case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
115
- fileNode.rawStringReferences.add(node.text);
116
- break;
117
- case ts.SyntaxKind.PropertyAccessExpression:
118
- fileNode.propertyAccessChains.add(node.getText(sourceFile));
119
- break;
120
- case ts.SyntaxKind.CallExpression:
121
- this.handleCallExpression(node, fileNode, sourceFile);
122
- break;
123
- case ts.SyntaxKind.JsxElement:
124
- case ts.SyntaxKind.JsxSelfClosingElement:
125
- this.handleJsxElement(node, fileNode, sourceFile);
126
- break;
127
- case ts.SyntaxKind.Decorator:
128
- this.handleDecorator(node, fileNode, sourceFile);
129
- break;
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
- this.context.exportRegistry.set(currentFile, new Set());
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 if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
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
- name: m.name.getText(sourceFile),
294
- type: ts.SyntaxKind[m.kind].toLowerCase(),
295
- start: m.getStart(sourceFile),
296
- end: m.getEnd()
297
- }));
298
- } else if (ts.isModuleDeclaration(node)) {
299
- const members = [];
300
- if (node.body && ts.isModuleBlock(node.body)) {
301
- node.body.statements.forEach(stmt => {
302
- if (this.hasExportModifier(stmt) && (ts.isVariableStatement(stmt) || ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt))) {
303
- if (ts.isVariableStatement(stmt)) {
304
- stmt.declarationList.declarations.forEach(d => members.push({
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
- const name = decl.name.text;
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
- // Dynamic import(): import('./module').then(...)
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
- fileNode.explicitImports.add(arg.text);
404
- fileNode.dynamicImports.add(arg.text);
405
- // Track external package usage from dynamic imports
406
- if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
407
- fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
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
- // Dynamic import with a non-literal expression (e.g., variable or template literal).
411
- if (fileNode.calculatedDynamicImports) {
412
- fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
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
- // Track external package usage from require() calls
421
- if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
422
- fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
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 getElementName = (name) => {
430
- if (ts.isIdentifier(name)) return name.text;
431
- if (ts.isPropertyAccessExpression(name)) return name.name.text;
432
- return 'unknown';
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
  }