pkg-scaffold 3.3.3 → 3.3.4
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/README.md +63 -22
- package/bin/cli.js +5 -5
- package/package.json +2 -2
- package/pnpm-workspace.yaml +2 -0
- package/src/EngineContext.js +14 -0
- package/src/ast/ASTAnalyzer.js +243 -116
- package/src/ast/BarrelParser.js +29 -17
- package/src/ast/DeadCodeDetector.js +73 -0
- package/src/ast/MagicDetector.js +3 -1
- package/src/ast/OxcAnalyzer.js +193 -81
- package/src/index.js +10 -11
- package/src/plugins/PluginRegistry.js +1 -0
- package/src/plugins/ecosystems/MorePlugins.js +184 -0
- package/src/plugins/ecosystems/PluginLoader.js +20 -0
- package/src/resolution/CircularDetector.js +3 -34
- package/src/resolution/ConfigLoader.js +34 -6
- package/src/resolution/WorkSpaceGraph.js +15 -1
package/src/ast/ASTAnalyzer.js
CHANGED
|
@@ -1,138 +1,123 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Enterprise AST Syntax Walker & Feature Extractor
|
|
7
|
-
* Upgraded to use full TypeScript Compiler API (ts.createProgram + TypeChecker)
|
|
8
|
-
* for type-aware cross-file analysis.
|
|
9
|
-
*/
|
|
10
|
-
import { OxcAnalyzer } from './OxcAnalyzer.js';
|
|
11
2
|
|
|
12
3
|
export class ASTAnalyzer {
|
|
13
4
|
constructor(context) {
|
|
14
5
|
this.context = context;
|
|
15
|
-
this.
|
|
16
|
-
this.checker = null;
|
|
17
|
-
this.oxc = new OxcAnalyzer(context);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Initializes the TypeScript program for the entire project.
|
|
22
|
-
* This is crucial for cross-file type resolution.
|
|
23
|
-
*/
|
|
24
|
-
initProgram(filePaths, options = {}) {
|
|
25
|
-
const defaultOptions = {
|
|
26
|
-
target: ts.ScriptTarget.Latest,
|
|
27
|
-
module: ts.ModuleKind.CommonJS,
|
|
28
|
-
allowJs: true,
|
|
29
|
-
checkJs: true,
|
|
30
|
-
esModuleInterop: true,
|
|
31
|
-
skipLibCheck: true
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
this.program = ts.createProgram(filePaths, { ...defaultOptions, ...options });
|
|
35
|
-
this.checker = this.program.getTypeChecker();
|
|
6
|
+
this.scopeStack = [];
|
|
36
7
|
}
|
|
37
8
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
async processFile(filePath, fileNode) {
|
|
42
|
-
// Fast Path: Use OXC for rapid scanning if type checking is not strictly required for this file
|
|
43
|
-
if (this.context.fastMode && this.oxc.isAvailable) {
|
|
44
|
-
const success = await this.oxc.processFile(filePath, fileNode);
|
|
45
|
-
if (success) return true;
|
|
9
|
+
parseFile(filePath, content, fileNode) {
|
|
10
|
+
if (this.context.verbose) {
|
|
11
|
+
console.log(`[AST] Parsing: ${filePath}`);
|
|
46
12
|
}
|
|
47
13
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
14
|
+
const sourceFile = ts.createSourceFile(
|
|
15
|
+
filePath,
|
|
16
|
+
content,
|
|
17
|
+
ts.ScriptTarget.Latest,
|
|
18
|
+
true,
|
|
19
|
+
filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
|
20
|
+
);
|
|
51
21
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (this.context.verbose) {
|
|
55
|
-
console.error(`[AST Error] Source file not found in program: ${filePath}`);
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
22
|
+
this.currentScope = { symbols: new Map(), parent: null };
|
|
23
|
+
this.scopeStack.push(this.currentScope);
|
|
59
24
|
|
|
60
25
|
this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
26
|
+
this.walkAST(sourceFile, fileNode, sourceFile);
|
|
27
|
+
|
|
28
|
+
this.scopeStack.pop(); // Pop the global scope
|
|
29
|
+
this.currentScope = null;
|
|
64
30
|
}
|
|
65
31
|
|
|
66
|
-
|
|
67
|
-
|
|
32
|
+
pushScope() {
|
|
33
|
+
const newScope = { symbols: new Map(), parent: this.currentScope };
|
|
34
|
+
this.scopeStack.push(newScope);
|
|
35
|
+
this.currentScope = newScope;
|
|
36
|
+
}
|
|
68
37
|
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const declFile = declarations[0].getSourceFile().fileName;
|
|
76
|
-
const symbolName = symbol.getName();
|
|
77
|
-
|
|
78
|
-
// Track sub-symbol usage (Property Access)
|
|
79
|
-
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
80
|
-
const parentType = this.checker.getTypeAtLocation(node.parent.expression);
|
|
81
|
-
const parentSymbol = parentType.getSymbol() || parentType.aliasSymbol;
|
|
82
|
-
if (parentSymbol) {
|
|
83
|
-
const parentDecl = parentSymbol.getDeclarations()?.[0];
|
|
84
|
-
if (parentDecl) {
|
|
85
|
-
const parentFile = parentDecl.getSourceFile().fileName;
|
|
86
|
-
fileNode.memberUsage = fileNode.memberUsage || new Set();
|
|
87
|
-
fileNode.memberUsage.add(`${parentFile}:${parentSymbol.getName()}.${symbolName}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
38
|
+
popScope() {
|
|
39
|
+
if (this.scopeStack.length > 1) {
|
|
40
|
+
this.scopeStack.pop();
|
|
41
|
+
this.currentScope = this.scopeStack[this.scopeStack.length - 1];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
91
44
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
45
|
+
addDeclaredSymbol(name, node, sourceFile) {
|
|
46
|
+
if (this.currentScope) {
|
|
47
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
48
|
+
this.currentScope.symbols.set(name, { node, line: loc.line + 1, column: loc.character + 1 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resolveSymbol(name) {
|
|
53
|
+
for (let i = this.scopeStack.length - 1; i >= 0; i--) {
|
|
54
|
+
const scope = this.scopeStack[i];
|
|
55
|
+
if (scope.symbols.has(name)) {
|
|
56
|
+
return scope.symbols.get(name);
|
|
97
57
|
}
|
|
98
58
|
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
walkAST(node, fileNode, sourceFile) {
|
|
63
|
+
// Handle scope entry for blocks, functions, classes, etc.
|
|
64
|
+
const isScopeNode = ts.isBlock(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node);
|
|
65
|
+
if (isScopeNode) {
|
|
66
|
+
this.pushScope();
|
|
67
|
+
}
|
|
99
68
|
|
|
100
69
|
switch (node.kind) {
|
|
101
|
-
case ts.SyntaxKind.ImportDeclaration:
|
|
102
|
-
this.handleImportDeclaration(node, fileNode);
|
|
70
|
+
case ts.SyntaxKind.ImportDeclaration:
|
|
71
|
+
this.handleImportDeclaration(node, fileNode, sourceFile);
|
|
103
72
|
break;
|
|
104
|
-
|
|
105
|
-
case ts.SyntaxKind.ExportDeclaration: {
|
|
73
|
+
case ts.SyntaxKind.ExportDeclaration:
|
|
106
74
|
this.handleExportDeclaration(node, fileNode, sourceFile);
|
|
107
75
|
break;
|
|
108
|
-
|
|
76
|
+
case ts.SyntaxKind.ExportAssignment:
|
|
77
|
+
fileNode.internalExports.set('default', { type: 'default', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
78
|
+
break;
|
|
79
|
+
case ts.SyntaxKind.VariableStatement:
|
|
80
|
+
this.handleVariableStatement(node, fileNode, sourceFile);
|
|
81
|
+
break;
|
|
109
82
|
case ts.SyntaxKind.FunctionDeclaration:
|
|
110
83
|
case ts.SyntaxKind.ClassDeclaration:
|
|
111
84
|
case ts.SyntaxKind.InterfaceDeclaration:
|
|
112
85
|
case ts.SyntaxKind.TypeAliasDeclaration:
|
|
113
86
|
case ts.SyntaxKind.EnumDeclaration:
|
|
114
|
-
case ts.SyntaxKind.ModuleDeclaration:
|
|
87
|
+
case ts.SyntaxKind.ModuleDeclaration:
|
|
115
88
|
this.handleNamedDeclaration(node, fileNode, sourceFile);
|
|
116
89
|
break;
|
|
117
|
-
|
|
118
|
-
case ts.SyntaxKind.VariableStatement: {
|
|
119
|
-
this.handleVariableStatement(node, fileNode, sourceFile);
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
case ts.SyntaxKind.CallExpression: {
|
|
90
|
+
case ts.SyntaxKind.CallExpression:
|
|
123
91
|
this.handleCallExpression(node, fileNode, sourceFile);
|
|
124
92
|
break;
|
|
125
|
-
|
|
93
|
+
case ts.SyntaxKind.JsxElement:
|
|
94
|
+
case ts.SyntaxKind.JsxSelfClosingElement:
|
|
95
|
+
this.handleJsxElement(node, fileNode, sourceFile);
|
|
96
|
+
break;
|
|
97
|
+
case ts.SyntaxKind.Decorator:
|
|
98
|
+
this.handleDecorator(node, fileNode, sourceFile);
|
|
99
|
+
break;
|
|
100
|
+
case ts.SyntaxKind.Identifier:
|
|
101
|
+
// Track usage of identifiers
|
|
102
|
+
const symbol = this.resolveSymbol(node.text);
|
|
103
|
+
if (symbol) {
|
|
104
|
+
// fileNode.usedSymbols.add(`${symbol.node.parent.kind}:${node.text}`); // More granular tracking needed
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
126
107
|
}
|
|
127
108
|
|
|
128
|
-
ts.forEachChild(node, child => this.
|
|
109
|
+
ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
|
|
110
|
+
|
|
111
|
+
if (isScopeNode) {
|
|
112
|
+
this.popScope();
|
|
113
|
+
}
|
|
129
114
|
}
|
|
130
115
|
|
|
131
|
-
handleImportDeclaration(node, fileNode) {
|
|
116
|
+
handleImportDeclaration(node, fileNode, sourceFile) {
|
|
132
117
|
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
133
118
|
const specifier = node.moduleSpecifier.text;
|
|
134
119
|
fileNode.explicitImports.add(specifier);
|
|
135
|
-
|
|
120
|
+
|
|
136
121
|
if (node.importClause) {
|
|
137
122
|
if (node.importClause.namedBindings) {
|
|
138
123
|
if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
@@ -140,36 +125,80 @@ export class ASTAnalyzer {
|
|
|
140
125
|
const importedName = element.name.text;
|
|
141
126
|
const propertyName = element.propertyName ? element.propertyName.text : importedName;
|
|
142
127
|
fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
|
|
128
|
+
this.addDeclaredSymbol(element.name.text, element, sourceFile); // Add local import name to scope
|
|
143
129
|
});
|
|
144
130
|
} else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
145
131
|
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
132
|
+
this.addDeclaredSymbol(node.importClause.namedBindings.name.text, node.importClause.namedBindings, sourceFile); // Add namespace import to scope
|
|
146
133
|
}
|
|
147
134
|
}
|
|
148
135
|
if (node.importClause.name) {
|
|
149
136
|
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
137
|
+
this.addDeclaredSymbol(node.importClause.name.text, node.importClause.name, sourceFile); // Add default import to scope
|
|
150
138
|
}
|
|
151
139
|
}
|
|
152
140
|
}
|
|
153
141
|
}
|
|
154
142
|
|
|
155
143
|
handleExportDeclaration(node, fileNode, sourceFile) {
|
|
156
|
-
// Handle re-exports: export { x } from './y'
|
|
157
144
|
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
158
145
|
const specifier = node.moduleSpecifier.text;
|
|
159
146
|
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
160
147
|
node.exportClause.elements.forEach(element => {
|
|
161
148
|
const name = element.name.text;
|
|
149
|
+
const propertyName = element.propertyName ? element.propertyName.text : name;
|
|
162
150
|
fileNode.internalExports.set(name, {
|
|
163
151
|
type: 're-export',
|
|
164
152
|
source: specifier,
|
|
153
|
+
originalName: propertyName,
|
|
165
154
|
start: node.getStart(sourceFile),
|
|
166
155
|
end: node.getEnd()
|
|
167
156
|
});
|
|
168
157
|
});
|
|
158
|
+
} else if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
|
|
159
|
+
// export * as name from 'module'
|
|
160
|
+
const name = node.exportClause.name.text;
|
|
161
|
+
fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
169
162
|
} else {
|
|
170
|
-
// export * from '
|
|
163
|
+
// export * from 'module'
|
|
171
164
|
fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
|
|
172
165
|
}
|
|
166
|
+
} else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
167
|
+
node.exportClause.elements.forEach(element => {
|
|
168
|
+
const name = element.name.text;
|
|
169
|
+
const propertyName = element.propertyName ? element.propertyName.text : name;
|
|
170
|
+
fileNode.internalExports.set(name, {
|
|
171
|
+
type: 'export',
|
|
172
|
+
originalName: propertyName,
|
|
173
|
+
start: node.getStart(sourceFile),
|
|
174
|
+
end: node.getEnd()
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
} else if (node.declaration) {
|
|
178
|
+
// Direct export of a declaration (e.g., export const x = 1;)
|
|
179
|
+
if (ts.isVariableStatement(node.declaration)) {
|
|
180
|
+
node.declaration.declarationList.declarations.forEach(decl => {
|
|
181
|
+
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
182
|
+
const name = decl.name.text;
|
|
183
|
+
fileNode.internalExports.set(name, {
|
|
184
|
+
type: 'variable',
|
|
185
|
+
start: decl.getStart(sourceFile),
|
|
186
|
+
end: decl.getEnd()
|
|
187
|
+
});
|
|
188
|
+
this.addDeclaredSymbol(name, decl, sourceFile);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
} else if (ts.isFunctionDeclaration(node.declaration) || ts.isClassDeclaration(node.declaration)) {
|
|
192
|
+
const name = node.declaration.name?.text;
|
|
193
|
+
if (name) {
|
|
194
|
+
fileNode.internalExports.set(name, {
|
|
195
|
+
type: ts.SyntaxKind[node.declaration.kind].toLowerCase().replace('declaration', ''),
|
|
196
|
+
start: node.declaration.getStart(sourceFile),
|
|
197
|
+
end: node.declaration.getEnd()
|
|
198
|
+
});
|
|
199
|
+
this.addDeclaredSymbol(name, node.declaration, sourceFile);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
173
202
|
}
|
|
174
203
|
}
|
|
175
204
|
|
|
@@ -183,35 +212,56 @@ export class ASTAnalyzer {
|
|
|
183
212
|
start: node.getStart(sourceFile),
|
|
184
213
|
end: node.getEnd()
|
|
185
214
|
};
|
|
186
|
-
|
|
187
|
-
fileNode.internalExports.set(name, exportInfo);
|
|
188
215
|
|
|
189
|
-
|
|
216
|
+
fileNode.internalExports.set(name, exportInfo);
|
|
217
|
+
|
|
190
218
|
if (ts.isEnumDeclaration(node)) {
|
|
191
|
-
exportInfo.members = node.members.map(m =>
|
|
219
|
+
exportInfo.members = node.members.map(m => ({
|
|
220
|
+
name: m.name.getText(sourceFile),
|
|
221
|
+
type: 'enumMember',
|
|
222
|
+
start: m.getStart(sourceFile),
|
|
223
|
+
end: m.getEnd()
|
|
224
|
+
}));
|
|
192
225
|
} else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
193
226
|
exportInfo.members = node.members
|
|
194
227
|
.filter(m => m.name)
|
|
195
|
-
.map(m =>
|
|
228
|
+
.map(m => ({
|
|
229
|
+
name: m.name.getText(sourceFile),
|
|
230
|
+
type: ts.SyntaxKind[m.kind].toLowerCase(),
|
|
231
|
+
start: m.getStart(sourceFile),
|
|
232
|
+
end: m.getEnd()
|
|
233
|
+
}));
|
|
196
234
|
} else if (ts.isModuleDeclaration(node)) {
|
|
197
|
-
// Handle Namespaces
|
|
198
235
|
const members = [];
|
|
199
236
|
if (node.body && ts.isModuleBlock(node.body)) {
|
|
200
237
|
node.body.statements.forEach(stmt => {
|
|
201
238
|
if (this.hasExportModifier(stmt) && (ts.isVariableStatement(stmt) || ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt))) {
|
|
202
239
|
if (ts.isVariableStatement(stmt)) {
|
|
203
|
-
stmt.declarationList.declarations.forEach(d => members.push(
|
|
240
|
+
stmt.declarationList.declarations.forEach(d => members.push({
|
|
241
|
+
name: d.name.getText(sourceFile),
|
|
242
|
+
type: 'variable',
|
|
243
|
+
start: d.getStart(sourceFile),
|
|
244
|
+
end: d.getEnd()
|
|
245
|
+
}));
|
|
204
246
|
} else if (stmt.name) {
|
|
205
|
-
members.push(
|
|
247
|
+
members.push({
|
|
248
|
+
name: stmt.name.getText(sourceFile),
|
|
249
|
+
type: ts.SyntaxKind[stmt.kind].toLowerCase().replace('declaration', ''),
|
|
250
|
+
start: stmt.getStart(sourceFile),
|
|
251
|
+
end: stmt.getEnd()
|
|
252
|
+
});
|
|
206
253
|
}
|
|
207
254
|
}
|
|
208
255
|
});
|
|
209
256
|
}
|
|
210
257
|
exportInfo.members = members;
|
|
211
258
|
}
|
|
212
|
-
|
|
259
|
+
|
|
213
260
|
const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
214
261
|
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
262
|
+
this.addDeclaredSymbol(name, node, sourceFile);
|
|
263
|
+
} else if (node.name && ts.isIdentifier(node.name)) {
|
|
264
|
+
this.addDeclaredSymbol(node.name.text, node, sourceFile);
|
|
215
265
|
}
|
|
216
266
|
}
|
|
217
267
|
|
|
@@ -225,32 +275,109 @@ export class ASTAnalyzer {
|
|
|
225
275
|
start: decl.getStart(sourceFile),
|
|
226
276
|
end: decl.getEnd()
|
|
227
277
|
});
|
|
278
|
+
this.addDeclaredSymbol(name, decl, sourceFile);
|
|
279
|
+
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
280
|
+
decl.name.elements.forEach(element => {
|
|
281
|
+
if(element.name && ts.isIdentifier(element.name)) {
|
|
282
|
+
const name = element.name.text;
|
|
283
|
+
fileNode.internalExports.set(name, {
|
|
284
|
+
type: 'variable',
|
|
285
|
+
start: element.getStart(sourceFile),
|
|
286
|
+
end: element.getEnd()
|
|
287
|
+
});
|
|
288
|
+
this.addDeclaredSymbol(name, element, sourceFile);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
} else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
|
|
292
|
+
decl.name.elements.forEach(element => {
|
|
293
|
+
if(ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
294
|
+
const name = element.name.text;
|
|
295
|
+
fileNode.internalExports.set(name, {
|
|
296
|
+
type: 'variable',
|
|
297
|
+
start: element.getStart(sourceFile),
|
|
298
|
+
end: element.getEnd()
|
|
299
|
+
});
|
|
300
|
+
this.addDeclaredSymbol(name, element, sourceFile);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
} else {
|
|
306
|
+
// Non-exported variable declarations also need to be added to scope
|
|
307
|
+
node.declarationList.declarations.forEach(decl => {
|
|
308
|
+
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
309
|
+
this.addDeclaredSymbol(decl.name.text, decl, sourceFile);
|
|
310
|
+
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
311
|
+
decl.name.elements.forEach(element => {
|
|
312
|
+
if (element.name && ts.isIdentifier(element.name)) {
|
|
313
|
+
this.addDeclaredSymbol(element.name.text, element, sourceFile);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
} else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
|
|
317
|
+
decl.name.elements.forEach(element => {
|
|
318
|
+
if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
319
|
+
this.addDeclaredSymbol(element.name.text, element, sourceFile);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
228
322
|
}
|
|
229
323
|
});
|
|
230
324
|
}
|
|
231
325
|
}
|
|
232
326
|
|
|
233
327
|
handleCallExpression(node, fileNode, sourceFile) {
|
|
234
|
-
// Trace dynamic imports
|
|
235
328
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
236
329
|
const arg = node.arguments[0];
|
|
237
330
|
if (arg && ts.isStringLiteral(arg)) {
|
|
238
331
|
fileNode.explicitImports.add(arg.text);
|
|
239
332
|
fileNode.dynamicImports.add(arg.text);
|
|
240
333
|
}
|
|
334
|
+
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
335
|
+
const arg = node.arguments[0];
|
|
336
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
337
|
+
fileNode.explicitImports.add(arg.text);
|
|
338
|
+
}
|
|
241
339
|
}
|
|
242
340
|
}
|
|
243
341
|
|
|
244
|
-
|
|
245
|
-
|
|
342
|
+
handleJsxElement(node, fileNode, sourceFile) {
|
|
343
|
+
const getElementName = (name) => {
|
|
344
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
345
|
+
if (ts.isPropertyAccessExpression(name)) return name.name.text;
|
|
346
|
+
return 'unknown';
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const tagName = getElementName(node.openingElement.tagName);
|
|
350
|
+
fileNode.jsxComponents.add(tagName);
|
|
351
|
+
|
|
352
|
+
node.openingElement.attributes.properties.forEach(attr => {
|
|
353
|
+
if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name)) {
|
|
354
|
+
fileNode.jsxProps.add(`${tagName}:${attr.name.text}`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
246
357
|
}
|
|
247
358
|
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
359
|
+
handleDecorator(node, fileNode, sourceFile) {
|
|
360
|
+
const getDecoratorName = (expr) => {
|
|
361
|
+
if (ts.isIdentifier(expr)) return expr.text;
|
|
362
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) return expr.expression.text;
|
|
363
|
+
if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
|
|
364
|
+
return 'unknown';
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const decoratorName = getDecoratorName(node.expression);
|
|
368
|
+
fileNode.decorators.add(decoratorName);
|
|
369
|
+
|
|
370
|
+
// Optionally, extract decorator arguments
|
|
371
|
+
if (ts.isCallExpression(node.expression)) {
|
|
372
|
+
node.expression.arguments.forEach(arg => {
|
|
373
|
+
// Further analysis of arguments can be done here if needed
|
|
374
|
+
// e.g., if (ts.isStringLiteral(arg)) fileNode.decoratorArgs.add(`${decoratorName}:${arg.text}`);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
hasExportModifier(node) {
|
|
380
|
+
return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
254
381
|
}
|
|
255
382
|
|
|
256
383
|
extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
|
package/src/ast/BarrelParser.js
CHANGED
|
@@ -99,6 +99,18 @@ export class BarrelParser {
|
|
|
99
99
|
node.declarationList.declarations.forEach(decl => {
|
|
100
100
|
if (ts.isIdentifier(decl.name)) {
|
|
101
101
|
spec.declaredLocalExports.add(decl.name.text);
|
|
102
|
+
} else if (ts.isObjectBindingPattern(decl.name)) {
|
|
103
|
+
decl.name.elements.forEach(element => {
|
|
104
|
+
if (element.name && ts.isIdentifier(element.name)) {
|
|
105
|
+
spec.declaredLocalExports.add(element.name.text);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} else if (ts.isArrayBindingPattern(decl.name)) {
|
|
109
|
+
decl.name.elements.forEach(element => {
|
|
110
|
+
if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
111
|
+
spec.declaredLocalExports.add(element.name.text);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
102
114
|
}
|
|
103
115
|
});
|
|
104
116
|
}
|
|
@@ -148,36 +160,36 @@ export class BarrelParser {
|
|
|
148
160
|
if (spec.forwardedNamedExports.has(targetSymbolName)) {
|
|
149
161
|
const routingRule = spec.forwardedNamedExports.get(targetSymbolName);
|
|
150
162
|
const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, routingRule.targetModule);
|
|
151
|
-
|
|
163
|
+
if (fullyResolvedPath) {
|
|
164
|
+
return this.determineSymbolDeclarationOrigin(fullyResolvedPath, routingRule.sourceSymbol, activeProjectGraph, protectionStack);
|
|
165
|
+
}
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
// Rule C: Evaluate structural namespace alias groupings
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
// Rule C: Evaluate structural namespace alias groupings (export * as name from 'module')
|
|
169
|
+
for (const [namespaceAlias, relativeModule] of spec.namespacedWildcardExports.entries()) {
|
|
170
|
+
// If the targetSymbolName is prefixed with the namespaceAlias, then it's a member of this namespace re-export
|
|
171
|
+
if (targetSymbolName.startsWith(`${namespaceAlias}.`)) {
|
|
172
|
+
const originalSymbol = targetSymbolName.substring(namespaceAlias.length + 1);
|
|
173
|
+
const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativeModule);
|
|
174
|
+
if (fullyResolvedPath) {
|
|
175
|
+
return this.determineSymbolDeclarationOrigin(fullyResolvedPath, originalSymbol, activeProjectGraph, protectionStack);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
159
178
|
}
|
|
160
179
|
|
|
161
|
-
// Rule D: Sweep through anonymous star re-exports vectors
|
|
180
|
+
// Rule D: Sweep through anonymous star re-exports vectors (export * from 'module')
|
|
162
181
|
for (const relativePath of spec.wildcardExports) {
|
|
163
182
|
const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativePath);
|
|
164
183
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (targetSubSpec.declaredLocalExports.has(targetSymbolName) ||
|
|
169
|
-
targetSubSpec.forwardedNamedExports.has(targetSymbolName)) {
|
|
170
|
-
return this.determineSymbolDeclarationOrigin(fullyResolvedPath, targetSymbolName, activeProjectGraph, protectionStack);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Dynamic recursive trace for multi-tier nested barrel file layouts
|
|
174
|
-
if (targetSubSpec.isBarrelInstance) {
|
|
184
|
+
if (fullyResolvedPath) {
|
|
185
|
+
// Look inside the graph metadata map configuration to verify child target availability
|
|
175
186
|
const continuousResolutionTrace = await this.determineSymbolDeclarationOrigin(
|
|
176
187
|
fullyResolvedPath,
|
|
177
188
|
targetSymbolName,
|
|
178
189
|
activeProjectGraph,
|
|
179
190
|
protectionStack
|
|
180
191
|
);
|
|
192
|
+
// If a resolution was found in a child barrel, return it
|
|
181
193
|
if (continuousResolutionTrace && continuousResolutionTrace.originFile !== fullyResolvedPath) {
|
|
182
194
|
return continuousResolutionTrace;
|
|
183
195
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export class DeadCodeDetector {
|
|
2
|
+
constructor(context) {
|
|
3
|
+
this.context = context;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
detectDeadCode(graph) {
|
|
7
|
+
const deadFiles = [];
|
|
8
|
+
const deadExports = [];
|
|
9
|
+
|
|
10
|
+
// Find all entry points
|
|
11
|
+
const entryPoints = new Set();
|
|
12
|
+
for (const [filePath, node] of graph.entries()) {
|
|
13
|
+
if (node.isEntry || node.isNextJsRoute || node.isSvelteComponent || node.isAstroPage) {
|
|
14
|
+
entryPoints.add(filePath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Traverse from entry points to find used files
|
|
19
|
+
const usedFiles = new Set();
|
|
20
|
+
const queue = Array.from(entryPoints);
|
|
21
|
+
|
|
22
|
+
while (queue.length > 0) {
|
|
23
|
+
const current = queue.shift();
|
|
24
|
+
if (!usedFiles.has(current)) {
|
|
25
|
+
usedFiles.add(current);
|
|
26
|
+
const node = graph.get(current);
|
|
27
|
+
if (node && node.outgoingEdges) {
|
|
28
|
+
for (const edge of node.outgoingEdges) {
|
|
29
|
+
queue.push(edge);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Identify dead files
|
|
36
|
+
for (const filePath of graph.keys()) {
|
|
37
|
+
if (!usedFiles.has(filePath)) {
|
|
38
|
+
deadFiles.push(filePath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Identify dead exports in used files
|
|
43
|
+
for (const filePath of usedFiles) {
|
|
44
|
+
const node = graph.get(filePath);
|
|
45
|
+
if (!node) continue;
|
|
46
|
+
|
|
47
|
+
// If it's an entry point, we consider its exports used (unless strictly configured otherwise)
|
|
48
|
+
if (node.isEntry) continue;
|
|
49
|
+
|
|
50
|
+
for (const [exportName, exportInfo] of node.internalExports.entries()) {
|
|
51
|
+
if (exportName === '*' || exportName === 'default') continue; // Skip wildcards for now
|
|
52
|
+
|
|
53
|
+
let isUsed = false;
|
|
54
|
+
// Check incoming edges if they import this specific symbol
|
|
55
|
+
for (const [otherPath, otherNode] of graph.entries()) {
|
|
56
|
+
if (otherNode.outgoingEdges.has(filePath)) {
|
|
57
|
+
if (otherNode.importedSymbols.has(`${filePath}:${exportName}`) ||
|
|
58
|
+
otherNode.importedSymbols.has(`${filePath}:*`)) {
|
|
59
|
+
isUsed = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isUsed) {
|
|
66
|
+
deadExports.push({ file: filePath, symbol: exportName, line: exportInfo.start });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { deadFiles, deadExports };
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/ast/MagicDetector.js
CHANGED
|
@@ -68,7 +68,9 @@ export class MagicDetector {
|
|
|
68
68
|
'jest.config.', 'vitest.config.', 'playwright.config.', 'cypress.config.',
|
|
69
69
|
'webpack.config.', 'vite.config.', 'rollup.config.', 'tailwind.config.',
|
|
70
70
|
'.eslintrc.', 'prettier.config.', '.postcssrc.', 'postcss.config.',
|
|
71
|
-
'bin/cli.js', 'index.js', 'WorkerTaskRunner.js'
|
|
71
|
+
'bin/cli.js', 'index.js', 'WorkerTaskRunner.js',
|
|
72
|
+
'src/server.ts', 'src/main.ts', 'src/app.ts', 'src/index.tsx', 'src/index.ts',
|
|
73
|
+
'server.ts', 'main.ts', 'app.ts'
|
|
72
74
|
];
|
|
73
75
|
|
|
74
76
|
return testEnvironments.some(matchPattern => normalizedPath.includes(matchPattern));
|