entkapp 4.5.0 → 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.
@@ -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,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
- // 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
+ 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
- this.context.exportRegistry.set(currentFile, new Set());
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 if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
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
- 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
- }
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
- 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);
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
- // Dynamic import(): import('./module').then(...)
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
- 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));
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
- // 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) });
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
- // Track external package usage from require() calls
421
- if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
422
- fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
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 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
- });
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
  }