entkapp 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.scaffold-ignore +22 -0
- package/LICENSE +211 -0
- package/NOTICE +13 -0
- package/README.md +33 -0
- package/bin/cli.js +174 -0
- package/entkapp/config.json +42 -0
- package/entkapp/plugins/README.md +19 -0
- package/index.js +2254 -0
- package/logo.png +0 -0
- package/package.json +96 -0
- package/src/EngineContext.js +185 -0
- package/src/api/HeadlessAPI.js +376 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/ast/ASTAnalyzer.js +492 -0
- package/src/ast/BarrelParser.js +221 -0
- package/src/ast/DeadCodeDetector.js +73 -0
- package/src/ast/MagicDetector.js +203 -0
- package/src/ast/OxcAnalyzer.js +337 -0
- package/src/ast/SecretScanner.js +304 -0
- package/src/healing/GitSandbox.js +82 -0
- package/src/healing/SelfHealer.js +52 -0
- package/src/index.js +723 -0
- package/src/performance/GraphCache.js +87 -0
- package/src/performance/SupplyChainGuard.js +92 -0
- package/src/performance/WorkerPool.js +109 -0
- package/src/plugins/BasePlugin.js +71 -0
- package/src/plugins/KnipAdapter.js +106 -0
- package/src/plugins/PluginRegistry.js +197 -0
- package/src/plugins/ecosystems/BackendServices.js +61 -0
- package/src/plugins/ecosystems/GenericPlugins.js +64 -0
- package/src/plugins/ecosystems/ModernFrameworks.js +159 -0
- package/src/plugins/ecosystems/MorePlugins.js +184 -0
- package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
- package/src/plugins/ecosystems/PluginLoader.js +20 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
- package/src/refractor/ImpactAnalyzer.js +92 -0
- package/src/refractor/SourceRewriter.js +86 -0
- package/src/refractor/TransactionManager.js +138 -0
- package/src/refractor/TypeIntegrity.js +75 -0
- package/src/resolution/CircularDetector.js +91 -0
- package/src/resolution/ConfigLoader.js +87 -0
- package/src/resolution/DepencyResolver.js +133 -0
- package/src/resolution/DependencyProfiler.js +268 -0
- package/src/resolution/PathMapper.js +125 -0
- package/src/resolution/WorkSpaceGraph.js +474 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
|
|
5
|
+
export class ASTAnalyzer {
|
|
6
|
+
constructor(context) {
|
|
7
|
+
this.context = context;
|
|
8
|
+
this.scopeStack = [];
|
|
9
|
+
}
|
|
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
|
+
getScriptKind(filePath) {
|
|
16
|
+
if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) return ts.ScriptKind.TSX;
|
|
17
|
+
if (filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return ts.ScriptKind.JS;
|
|
18
|
+
return ts.ScriptKind.TS;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
parseFile(filePath, content, fileNode) {
|
|
22
|
+
if (this.context.verbose) {
|
|
23
|
+
console.log(`[AST] Parsing: ${filePath}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sourceFile = ts.createSourceFile(
|
|
27
|
+
filePath,
|
|
28
|
+
content,
|
|
29
|
+
ts.ScriptTarget.Latest,
|
|
30
|
+
true,
|
|
31
|
+
this.getScriptKind(filePath)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
this.currentScope = { symbols: new Map(), parent: null };
|
|
35
|
+
this.scopeStack.push(this.currentScope);
|
|
36
|
+
|
|
37
|
+
this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
|
|
38
|
+
this.walkAST(sourceFile, fileNode, sourceFile);
|
|
39
|
+
|
|
40
|
+
this.scopeStack.pop(); // Pop the global scope
|
|
41
|
+
this.currentScope = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Alias for walkAST used by WorkerTaskRunner (legacy API compatibility).
|
|
46
|
+
*/
|
|
47
|
+
walkNode(node, sourceFile, fileNode) {
|
|
48
|
+
return this.walkAST(node, fileNode, sourceFile);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pushScope() {
|
|
52
|
+
const newScope = { symbols: new Map(), parent: this.currentScope };
|
|
53
|
+
this.scopeStack.push(newScope);
|
|
54
|
+
this.currentScope = newScope;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
popScope() {
|
|
58
|
+
if (this.scopeStack.length > 1) {
|
|
59
|
+
this.scopeStack.pop();
|
|
60
|
+
this.currentScope = this.scopeStack[this.scopeStack.length - 1];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
addDeclaredSymbol(name, node, sourceFile) {
|
|
65
|
+
if (this.currentScope) {
|
|
66
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
67
|
+
this.currentScope.symbols.set(name, { node, line: loc.line + 1, column: loc.character + 1 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolveSymbol(name) {
|
|
72
|
+
for (let i = this.scopeStack.length - 1; i >= 0; i--) {
|
|
73
|
+
const scope = this.scopeStack[i];
|
|
74
|
+
if (scope.symbols.has(name)) {
|
|
75
|
+
return scope.symbols.get(name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
walkAST(node, fileNode, sourceFile) {
|
|
82
|
+
// Handle scope entry for blocks, functions, classes, etc.
|
|
83
|
+
const isScopeNode = ts.isBlock(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node);
|
|
84
|
+
if (isScopeNode) {
|
|
85
|
+
this.pushScope();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
switch (node.kind) {
|
|
89
|
+
case ts.SyntaxKind.ImportDeclaration:
|
|
90
|
+
this.handleImportDeclaration(node, fileNode, sourceFile);
|
|
91
|
+
break;
|
|
92
|
+
case ts.SyntaxKind.ExportDeclaration:
|
|
93
|
+
this.handleExportDeclaration(node, fileNode, sourceFile);
|
|
94
|
+
break;
|
|
95
|
+
case ts.SyntaxKind.ExportAssignment:
|
|
96
|
+
fileNode.internalExports.set('default', { type: 'default', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
97
|
+
break;
|
|
98
|
+
case ts.SyntaxKind.VariableStatement:
|
|
99
|
+
this.handleVariableStatement(node, fileNode, sourceFile);
|
|
100
|
+
break;
|
|
101
|
+
case ts.SyntaxKind.FunctionDeclaration:
|
|
102
|
+
case ts.SyntaxKind.ClassDeclaration:
|
|
103
|
+
case ts.SyntaxKind.InterfaceDeclaration:
|
|
104
|
+
case ts.SyntaxKind.TypeAliasDeclaration:
|
|
105
|
+
case ts.SyntaxKind.EnumDeclaration:
|
|
106
|
+
case ts.SyntaxKind.ModuleDeclaration:
|
|
107
|
+
this.handleNamedDeclaration(node, fileNode, sourceFile);
|
|
108
|
+
break;
|
|
109
|
+
case ts.SyntaxKind.Identifier:
|
|
110
|
+
fileNode.instantiatedIdentifiers.add(node.text);
|
|
111
|
+
break;
|
|
112
|
+
case ts.SyntaxKind.StringLiteral:
|
|
113
|
+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
114
|
+
fileNode.rawStringReferences.add(node.text);
|
|
115
|
+
break;
|
|
116
|
+
case ts.SyntaxKind.PropertyAccessExpression:
|
|
117
|
+
fileNode.propertyAccessChains.add(node.getText(sourceFile));
|
|
118
|
+
break;
|
|
119
|
+
case ts.SyntaxKind.CallExpression:
|
|
120
|
+
this.handleCallExpression(node, fileNode, sourceFile);
|
|
121
|
+
break;
|
|
122
|
+
case ts.SyntaxKind.JsxElement:
|
|
123
|
+
case ts.SyntaxKind.JsxSelfClosingElement:
|
|
124
|
+
this.handleJsxElement(node, fileNode, sourceFile);
|
|
125
|
+
break;
|
|
126
|
+
case ts.SyntaxKind.Decorator:
|
|
127
|
+
this.handleDecorator(node, fileNode, sourceFile);
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
|
|
132
|
+
|
|
133
|
+
if (isScopeNode) {
|
|
134
|
+
this.popScope();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
handleImportDeclaration(node, fileNode, sourceFile) {
|
|
139
|
+
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
140
|
+
const specifier = node.moduleSpecifier.text;
|
|
141
|
+
fileNode.explicitImports.add(specifier);
|
|
142
|
+
|
|
143
|
+
// Track external package usage for dependency analysis
|
|
144
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
145
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 🚨 TARGET BUG 1 & 2: Audit package requirements for unlisted or root dependencies
|
|
149
|
+
if (this.context.workspaceGraph && typeof this.context.workspaceGraph.auditImportSpecifier === 'function') {
|
|
150
|
+
this.context.workspaceGraph.auditImportSpecifier(specifier, sourceFile.fileName);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (node.importClause) {
|
|
154
|
+
// Initialize global tracking structures on the engine context if missing
|
|
155
|
+
if (!this.context.importUsageRegistry) this.context.importUsageRegistry = new Set();
|
|
156
|
+
|
|
157
|
+
// Cross-file import resolver function
|
|
158
|
+
const resolveAbsoluteTargetFile = (spec) => {
|
|
159
|
+
// If importing a local workspace monorepo module (e.g. '@monorepo/shared')
|
|
160
|
+
if (this.context.workspaceGraph && typeof this.context.workspaceGraph.isLocalWorkspaceSpecifier === 'function') {
|
|
161
|
+
if (this.context.workspaceGraph.isLocalWorkspaceSpecifier(spec)) {
|
|
162
|
+
const match = this.context.workspaceGraph.getWorkspacePackageMatch(spec);
|
|
163
|
+
// Grab the first entry point file calculated from its local tsconfig/package.json
|
|
164
|
+
if (match && match.entryPoints && match.entryPoints.length > 0) {
|
|
165
|
+
return match.entryPoints[0];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Fallback to relative file layout calculations
|
|
170
|
+
if (spec.startsWith('.')) {
|
|
171
|
+
let resolved = path.resolve(path.dirname(sourceFile.fileName), spec);
|
|
172
|
+
// Append standard extensions if missing from import string
|
|
173
|
+
if (!path.extname(resolved)) {
|
|
174
|
+
if (fs.existsSync(resolved + '.ts')) resolved += '.ts';
|
|
175
|
+
else if (fs.existsSync(resolved + '.tsx')) resolved += '.tsx';
|
|
176
|
+
else if (fs.existsSync(resolved + '.js')) resolved += '.js';
|
|
177
|
+
}
|
|
178
|
+
return resolved;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (node.importClause.name) {
|
|
184
|
+
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
185
|
+
|
|
186
|
+
// Trace default consumption targets
|
|
187
|
+
const targetFile = resolveAbsoluteTargetFile(specifier);
|
|
188
|
+
if (targetFile) {
|
|
189
|
+
this.context.importUsageRegistry.add(`${targetFile}:default`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (node.importClause.namedBindings) {
|
|
194
|
+
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
195
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
196
|
+
|
|
197
|
+
const targetFile = resolveAbsoluteTargetFile(specifier);
|
|
198
|
+
if (targetFile) {
|
|
199
|
+
this.context.importUsageRegistry.add(`${targetFile}:*`);
|
|
200
|
+
}
|
|
201
|
+
} else if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
202
|
+
node.importClause.namedBindings.elements.forEach(element => {
|
|
203
|
+
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
204
|
+
fileNode.importedSymbols.add(`${specifier}:${importedName}`);
|
|
205
|
+
|
|
206
|
+
// 🚨 TARGET BUG 3: Map the unique file:symbol consumption token
|
|
207
|
+
const targetFile = resolveAbsoluteTargetFile(specifier);
|
|
208
|
+
if (targetFile) {
|
|
209
|
+
this.context.importUsageRegistry.add(`${targetFile}:${importedName}`);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
handleExportDeclaration(node, fileNode, sourceFile) {
|
|
219
|
+
const specifier = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
|
|
220
|
+
|
|
221
|
+
if (specifier) {
|
|
222
|
+
// Re-export from source: export * from './module' or export { x } from './module'
|
|
223
|
+
fileNode.explicitImports.add(specifier);
|
|
224
|
+
|
|
225
|
+
// Track external package usage from re-exports
|
|
226
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
227
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!node.exportClause) {
|
|
231
|
+
// export * from './module'
|
|
232
|
+
fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
|
|
233
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
234
|
+
} else if (ts.isNamespaceExport(node.exportClause)) {
|
|
235
|
+
// export * as name from './module'
|
|
236
|
+
const name = node.exportClause.name.text;
|
|
237
|
+
fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
238
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
239
|
+
} else if (ts.isNamedExports(node.exportClause)) {
|
|
240
|
+
// export { x, y as z } from './module'
|
|
241
|
+
node.exportClause.elements.forEach(element => {
|
|
242
|
+
const originalName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
243
|
+
const exportedName = element.name.text;
|
|
244
|
+
fileNode.internalExports.set(exportedName, { type: 're-export', source: specifier, originalName, start: element.getStart(sourceFile), end: element.getEnd() });
|
|
245
|
+
fileNode.importedSymbols.add(`${specifier}:${originalName}`);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
} else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
249
|
+
// Local named exports: export { x, y as z }
|
|
250
|
+
node.exportClause.elements.forEach(element => {
|
|
251
|
+
const localName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
252
|
+
const exportedName = element.name.text;
|
|
253
|
+
fileNode.internalExports.set(exportedName, { type: 'export', originalName: localName, start: element.getStart(sourceFile), end: element.getEnd() });
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
handleNamedDeclaration(node, fileNode, sourceFile) {
|
|
259
|
+
if (this.hasExportModifier(node)) {
|
|
260
|
+
const isDefault = node.modifiers?.some(m => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
261
|
+
const name = isDefault ? 'default' : (node.name?.text || 'anonymous');
|
|
262
|
+
|
|
263
|
+
// 🚨 TARGET BUG 3 (Part B): Extract exported symbol tokens into your global context map
|
|
264
|
+
if (!this.context.exportRegistry) this.context.exportRegistry = new Map();
|
|
265
|
+
const currentFile = sourceFile.fileName;
|
|
266
|
+
if (!this.context.exportRegistry.has(currentFile)) {
|
|
267
|
+
this.context.exportRegistry.set(currentFile, new Set());
|
|
268
|
+
}
|
|
269
|
+
if (name !== 'anonymous') {
|
|
270
|
+
this.context.exportRegistry.get(currentFile).add(name);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const exportInfo = {
|
|
274
|
+
type: ts.SyntaxKind[node.kind].toLowerCase().replace('declaration', ''),
|
|
275
|
+
start: node.getStart(sourceFile),
|
|
276
|
+
end: node.getEnd()
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
fileNode.internalExports.set(name, exportInfo);
|
|
280
|
+
|
|
281
|
+
if (ts.isEnumDeclaration(node)) {
|
|
282
|
+
exportInfo.members = node.members.map(m => ({
|
|
283
|
+
name: m.name.getText(sourceFile),
|
|
284
|
+
type: 'enumMember',
|
|
285
|
+
start: m.getStart(sourceFile),
|
|
286
|
+
end: m.getEnd()
|
|
287
|
+
}));
|
|
288
|
+
} else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
289
|
+
exportInfo.members = node.members
|
|
290
|
+
.filter(m => m.name)
|
|
291
|
+
.map(m => ({
|
|
292
|
+
name: m.name.getText(sourceFile),
|
|
293
|
+
type: ts.SyntaxKind[m.kind].toLowerCase(),
|
|
294
|
+
start: m.getStart(sourceFile),
|
|
295
|
+
end: m.getEnd()
|
|
296
|
+
}));
|
|
297
|
+
} else if (ts.isModuleDeclaration(node)) {
|
|
298
|
+
const members = [];
|
|
299
|
+
if (node.body && ts.isModuleBlock(node.body)) {
|
|
300
|
+
node.body.statements.forEach(stmt => {
|
|
301
|
+
if (this.hasExportModifier(stmt) && (ts.isVariableStatement(stmt) || ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt))) {
|
|
302
|
+
if (ts.isVariableStatement(stmt)) {
|
|
303
|
+
stmt.declarationList.declarations.forEach(d => members.push({
|
|
304
|
+
name: d.name.getText(sourceFile),
|
|
305
|
+
type: 'variable',
|
|
306
|
+
start: d.getStart(sourceFile),
|
|
307
|
+
end: d.getEnd()
|
|
308
|
+
}));
|
|
309
|
+
} else if (stmt.name) {
|
|
310
|
+
members.push({
|
|
311
|
+
name: stmt.name.getText(sourceFile),
|
|
312
|
+
type: ts.SyntaxKind[stmt.kind].toLowerCase().replace('declaration', ''),
|
|
313
|
+
start: stmt.getStart(sourceFile),
|
|
314
|
+
end: stmt.getEnd()
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
exportInfo.members = members;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
324
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
325
|
+
this.addDeclaredSymbol(name, node, sourceFile);
|
|
326
|
+
} else if (node.name && ts.isIdentifier(node.name)) {
|
|
327
|
+
this.addDeclaredSymbol(node.name.text, node, sourceFile);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
handleVariableStatement(node, fileNode, sourceFile) {
|
|
332
|
+
if (this.hasExportModifier(node)) {
|
|
333
|
+
node.declarationList.declarations.forEach(decl => {
|
|
334
|
+
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
335
|
+
const name = decl.name.text;
|
|
336
|
+
fileNode.internalExports.set(name, {
|
|
337
|
+
type: 'variable',
|
|
338
|
+
start: node.getStart(sourceFile),
|
|
339
|
+
end: node.getEnd()
|
|
340
|
+
});
|
|
341
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(decl.getStart(sourceFile));
|
|
342
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
343
|
+
this.addDeclaredSymbol(name, decl, sourceFile);
|
|
344
|
+
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
345
|
+
decl.name.elements.forEach(element => {
|
|
346
|
+
if(element.name && ts.isIdentifier(element.name)) {
|
|
347
|
+
const name = element.name.text;
|
|
348
|
+
fileNode.internalExports.set(name, {
|
|
349
|
+
type: 'variable',
|
|
350
|
+
start: node.getStart(sourceFile),
|
|
351
|
+
end: node.getEnd()
|
|
352
|
+
});
|
|
353
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
354
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
355
|
+
this.addDeclaredSymbol(name, element, sourceFile);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
} else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
|
|
359
|
+
decl.name.elements.forEach(element => {
|
|
360
|
+
if(ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
361
|
+
const name = element.name.text;
|
|
362
|
+
fileNode.internalExports.set(name, {
|
|
363
|
+
type: 'variable',
|
|
364
|
+
start: node.getStart(sourceFile),
|
|
365
|
+
end: node.getEnd()
|
|
366
|
+
});
|
|
367
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
368
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
369
|
+
this.addDeclaredSymbol(name, element, sourceFile);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
// Non-exported variable declarations also need to be added to scope
|
|
376
|
+
node.declarationList.declarations.forEach(decl => {
|
|
377
|
+
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
378
|
+
this.addDeclaredSymbol(decl.name.text, decl, sourceFile);
|
|
379
|
+
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
380
|
+
decl.name.elements.forEach(element => {
|
|
381
|
+
if (element.name && ts.isIdentifier(element.name)) {
|
|
382
|
+
this.addDeclaredSymbol(element.name.text, element, sourceFile);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
} else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
|
|
386
|
+
decl.name.elements.forEach(element => {
|
|
387
|
+
if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
|
|
388
|
+
this.addDeclaredSymbol(element.name.text, element, sourceFile);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
handleCallExpression(node, fileNode, sourceFile) {
|
|
397
|
+
// Dynamic import(): import('./module').then(...)
|
|
398
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
399
|
+
const arg = node.arguments[0];
|
|
400
|
+
if (arg) {
|
|
401
|
+
if (ts.isStringLiteral(arg)) {
|
|
402
|
+
fileNode.explicitImports.add(arg.text);
|
|
403
|
+
fileNode.dynamicImports.add(arg.text);
|
|
404
|
+
// Track external package usage from dynamic imports
|
|
405
|
+
if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
|
|
406
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
// Dynamic import with a non-literal expression (e.g., variable or template literal).
|
|
410
|
+
if (fileNode.calculatedDynamicImports) {
|
|
411
|
+
fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
416
|
+
const arg = node.arguments[0];
|
|
417
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
418
|
+
fileNode.explicitImports.add(arg.text);
|
|
419
|
+
// Track external package usage from require() calls
|
|
420
|
+
if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
|
|
421
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
handleJsxElement(node, fileNode, sourceFile) {
|
|
428
|
+
const getElementName = (name) => {
|
|
429
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
430
|
+
if (ts.isPropertyAccessExpression(name)) return name.name.text;
|
|
431
|
+
return 'unknown';
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const tagName = getElementName(node.openingElement.tagName);
|
|
435
|
+
fileNode.jsxComponents.add(tagName);
|
|
436
|
+
|
|
437
|
+
node.openingElement.attributes.properties.forEach(attr => {
|
|
438
|
+
if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name)) {
|
|
439
|
+
fileNode.jsxProps.add(`${tagName}:${attr.name.text}`);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
handleDecorator(node, fileNode, sourceFile) {
|
|
445
|
+
const getDecoratorName = (expr) => {
|
|
446
|
+
if (ts.isIdentifier(expr)) return expr.text;
|
|
447
|
+
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) return expr.expression.text;
|
|
448
|
+
if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
|
|
449
|
+
return 'unknown';
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const decoratorName = getDecoratorName(node.expression);
|
|
453
|
+
fileNode.decorators.add(decoratorName);
|
|
454
|
+
|
|
455
|
+
// Optionally, extract decorator arguments
|
|
456
|
+
if (ts.isCallExpression(node.expression)) {
|
|
457
|
+
node.expression.arguments.forEach(arg => {
|
|
458
|
+
// Further analysis of arguments can be done here if needed
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
hasExportModifier(node) {
|
|
464
|
+
return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
|
|
468
|
+
const fullText = sourceFile.text;
|
|
469
|
+
const commentRegex = /\/\*\*?[\s\S]*?\*\/|\/\/.*/g;
|
|
470
|
+
let match;
|
|
471
|
+
while ((match = commentRegex.exec(fullText)) !== null) {
|
|
472
|
+
const suppressMatches = match[0].match(/@scaffold-suppress\s+([a-zA-Z0-9_\-*:]+)/g);
|
|
473
|
+
if (suppressMatches) {
|
|
474
|
+
suppressMatches.forEach(m => fileNode.localSuppressedRules.add(m.replace('@scaffold-suppress', '').trim()));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Extracts the root npm package name from an import specifier.
|
|
481
|
+
* Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
|
|
482
|
+
*/
|
|
483
|
+
_extractPackageName(specifier) {
|
|
484
|
+
if (specifier.startsWith('@')) {
|
|
485
|
+
// Scoped package: @scope/name or @scope/name/subpath
|
|
486
|
+
const parts = specifier.split('/');
|
|
487
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
488
|
+
}
|
|
489
|
+
// Regular package: name or name/subpath
|
|
490
|
+
return specifier.split('/')[0];
|
|
491
|
+
}
|
|
492
|
+
}
|