entkapp 4.5.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +4 -4
- package/entkapp/config.json +0 -1
- package/package.json +5 -6
- package/src/EngineContext.js +276 -78
- package/src/analyzers/OxcAnalyzer.js +8 -380
- package/src/api/HeadlessAPI.js +1 -1
- package/src/api/PluginSDK.js +23 -187
- package/src/ast/ASTAnalyzer.js +481 -252
- package/src/ast/AdvancedAnalysis.js +6 -5
- package/src/ast/BarrelParser.js +39 -30
- package/src/ast/DeadCodeDetector.js +30 -18
- package/src/ast/MagicDetector.js +1 -1
- package/src/ast/OxcAnalyzer.js +335 -273
- package/src/index.js +279 -365
- package/src/performance/GraphCache.js +21 -2
- package/src/performance/WorkerPool.js +11 -1
- package/src/performance/WorkerTaskRunner.js +72 -25
- package/src/plugins/PluginRegistry.js +5 -16
- package/src/plugins/ecosystems/GenericPlugins.js +61 -0
- package/src/refractor/TransactionManager.js +3 -136
- package/src/refractor/TypeIntegrity.js +2 -73
- package/src/resolution/CircularDetector.js +44 -44
- package/src/resolution/ConfigLoader.js +2 -85
- package/src/resolution/DepencyResolver.js +28 -121
- package/src/resolution/EntryPointDetector.js +134 -0
- package/src/resolution/PathMapper.js +31 -107
- package/src/resolution/TSConfigLoader.js +76 -0
- package/src/resolution/WorkSpaceGraph.js +6 -472
- package/src/resolution/WorkspaceDiagnostic.js +3 -57
- package/src/plugins/KnipAdapter.js +0 -106
|
@@ -1,383 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
constructor(context) {
|
|
3
|
-
this.context = context;
|
|
4
|
-
this.oxc = null;
|
|
5
|
-
this.isAvailable = false;
|
|
6
|
-
}
|
|
1
|
+
import { OxcAnalyzer as CoreOxcAnalyzer } from '../ast/OxcAnalyzer.js';
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} catch (e) {
|
|
16
|
-
try {
|
|
17
|
-
const { createRequire } = await import('module');
|
|
18
|
-
const require = createRequire(import.meta.url);
|
|
19
|
-
this.oxc = require("oxc-parser");
|
|
20
|
-
this.isAvailable = true;
|
|
21
|
-
return true;
|
|
22
|
-
} catch (err) {
|
|
23
|
-
this.isAvailable = false;
|
|
24
|
-
if (this.context.verbose) {
|
|
25
|
-
console.warn(`[OxcAnalyzer] oxc-parser failed to load: ${err.message}`);
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* Proxy for the Core OxcAnalyzer to maintain backward compatibility
|
|
5
|
+
* while centralizing the implementation.
|
|
6
|
+
*/
|
|
7
|
+
export class OxcAnalyzer extends CoreOxcAnalyzer {
|
|
8
|
+
constructor(context) {
|
|
9
|
+
super(context);
|
|
29
10
|
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async parseFile(filePath, content, fileNode) {
|
|
33
|
-
if (!this.isAvailable) {
|
|
34
|
-
const initialized = await this.init();
|
|
35
|
-
if (!initialized) return false;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Fix: Ensure filePath is correctly handled for OXC (Windows path issue)
|
|
40
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
41
|
-
|
|
42
|
-
// Try passing the path directly as the second argument if the object-based options fail
|
|
43
|
-
// Some versions of oxc-parser expect a string as the second argument for the filename
|
|
44
|
-
let result;
|
|
45
|
-
try {
|
|
46
|
-
result = this.oxc.parseSync(content, {
|
|
47
|
-
sourceType: "module",
|
|
48
|
-
sourceFilename: normalizedPath,
|
|
49
|
-
lang: "typescript"
|
|
50
|
-
});
|
|
51
|
-
} catch (e) {
|
|
52
|
-
// Fallback for versions that expect the path as second argument
|
|
53
|
-
result = this.oxc.parseSync(content, normalizedPath);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Fix: Handle cases where OXC returns a JSON string instead of an object
|
|
57
|
-
// Stabilize result through JSON round-trip to fix N-API conversion issues on Windows
|
|
58
|
-
let parsedResult;
|
|
59
|
-
try {
|
|
60
|
-
parsedResult = typeof result === 'string' ? JSON.parse(result) : JSON.parse(JSON.stringify(result));
|
|
61
|
-
} catch (err) {
|
|
62
|
-
if (typeof result === 'object') {
|
|
63
|
-
parsedResult = result; // Last resort
|
|
64
|
-
} else {
|
|
65
|
-
throw new Error("OXC returned an invalid format");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let ast;
|
|
70
|
-
if (parsedResult && typeof parsedResult === 'object') {
|
|
71
|
-
if (parsedResult.program) {
|
|
72
|
-
ast = parsedResult;
|
|
73
|
-
} else if (parsedResult.ast) {
|
|
74
|
-
ast = { program: parsedResult.ast };
|
|
75
|
-
} else {
|
|
76
|
-
ast = { program: parsedResult };
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
throw new Error("OXC returned an invalid AST format");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
fileNode.ast = ast.program; // Store the AST for advanced analysis
|
|
83
|
-
fileNode.jsxComponents = new Set();
|
|
84
|
-
fileNode.jsxProps = new Set();
|
|
85
|
-
fileNode.decorators = new Set();
|
|
86
|
-
|
|
87
|
-
this.walkOxcAst(ast.program, fileNode, content);
|
|
88
|
-
|
|
89
|
-
const lines = content.split('\n');
|
|
90
|
-
const getLineCol = (pos) => {
|
|
91
|
-
let count = 0;
|
|
92
|
-
for (let i = 0; i < lines.length; i++) {
|
|
93
|
-
if (count + lines[i].length + 1 > pos) {
|
|
94
|
-
return { line: i + 1, column: pos - count + 1 };
|
|
95
|
-
}
|
|
96
|
-
count += lines[i].length + 1;
|
|
97
|
-
}
|
|
98
|
-
return { line: 1, column: 1 };
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
for (const [name, meta] of fileNode.internalExports.entries()) {
|
|
102
|
-
if (meta.start !== undefined) {
|
|
103
|
-
fileNode.symbolSourceLocations.set(name, getLineCol(meta.start));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return true;
|
|
108
|
-
} catch (e) {
|
|
109
|
-
if (this.context.verbose) {
|
|
110
|
-
console.warn(`[OXC] Failed to parse ${filePath}. Error: ${e.message}`);
|
|
111
|
-
if (e.stack) console.debug(e.stack);
|
|
112
|
-
console.info(`[OXC] Switching back to TypeScript Compiler API for ${filePath}`);
|
|
113
|
-
}
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
walkOxcAst(node, fileNode, content) {
|
|
119
|
-
if (!node) return;
|
|
120
|
-
|
|
121
|
-
switch (node.type) {
|
|
122
|
-
case "ImportDeclaration":
|
|
123
|
-
this.handleImportDeclaration(node, fileNode);
|
|
124
|
-
break;
|
|
125
|
-
case "ExportNamedDeclaration":
|
|
126
|
-
case "ExportDefaultDeclaration":
|
|
127
|
-
case "ExportAllDeclaration":
|
|
128
|
-
this.handleExportDeclaration(node, fileNode, content);
|
|
129
|
-
break;
|
|
130
|
-
case "CallExpression":
|
|
131
|
-
this.handleCallExpression(node, fileNode);
|
|
132
|
-
break;
|
|
133
|
-
case "JSXElement":
|
|
134
|
-
case "JSXFragment":
|
|
135
|
-
this.handleJsxElement(node, fileNode);
|
|
136
|
-
break;
|
|
137
|
-
case "Decorator":
|
|
138
|
-
this.handleDecorator(node, fileNode);
|
|
139
|
-
break;
|
|
140
|
-
case "StringLiteral":
|
|
141
|
-
fileNode.rawStringReferences.add(node.value);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
for (const key in node) {
|
|
146
|
-
if (node[key] && typeof node[key] === "object") {
|
|
147
|
-
if (Array.isArray(node[key])) {
|
|
148
|
-
node[key].forEach((child) => this.walkOxcAst(child, fileNode, content));
|
|
149
|
-
} else {
|
|
150
|
-
this.walkOxcAst(node[key], fileNode, content);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
handleImportDeclaration(node, fileNode) {
|
|
157
|
-
const specifier = node.source.value;
|
|
158
|
-
fileNode.explicitImports.add(specifier);
|
|
159
|
-
|
|
160
|
-
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
161
|
-
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (node.specifiers) {
|
|
165
|
-
node.specifiers.forEach((spec) => {
|
|
166
|
-
if (spec.type === "ImportSpecifier") {
|
|
167
|
-
const importedName = spec.imported.name || (spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value);
|
|
168
|
-
fileNode.importedSymbols.add(`${specifier}:${importedName}`);
|
|
169
|
-
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
170
|
-
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
171
|
-
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
172
|
-
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
handleExportDeclaration(node, fileNode, content) {
|
|
179
|
-
// 1. Default Exports
|
|
180
|
-
if (node.type === "ExportDefaultDeclaration") {
|
|
181
|
-
fileNode.internalExports.set("default", {
|
|
182
|
-
type: "default",
|
|
183
|
-
start: node.start,
|
|
184
|
-
end: node.end
|
|
185
|
-
});
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// 2. Re-export All: export * from 'mod' or export * as ns from 'mod'
|
|
190
|
-
if (node.type === "ExportAllDeclaration") {
|
|
191
|
-
const sourceSpecifier = node.source.value;
|
|
192
|
-
fileNode.explicitImports.add(sourceSpecifier);
|
|
193
|
-
if (!sourceSpecifier.startsWith('.') && !sourceSpecifier.startsWith('/')) {
|
|
194
|
-
fileNode.externalPackageUsage.add(this._extractPackageName(sourceSpecifier));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (node.exported) {
|
|
198
|
-
// export * as ns from 'mod'
|
|
199
|
-
const name = node.exported.name || (node.exported.type === "Identifier" ? node.exported.name : null);
|
|
200
|
-
if (name) {
|
|
201
|
-
fileNode.internalExports.set(name, {
|
|
202
|
-
type: "re-export-namespace",
|
|
203
|
-
source: sourceSpecifier,
|
|
204
|
-
originalName: "*",
|
|
205
|
-
start: node.start,
|
|
206
|
-
end: node.end
|
|
207
|
-
});
|
|
208
|
-
fileNode.importedSymbols.add(`${sourceSpecifier}:*`);
|
|
209
|
-
}
|
|
210
|
-
} else {
|
|
211
|
-
// export * from 'mod'
|
|
212
|
-
fileNode.internalExports.set("*", {
|
|
213
|
-
type: "re-export-all",
|
|
214
|
-
source: sourceSpecifier
|
|
215
|
-
});
|
|
216
|
-
fileNode.importedSymbols.add(`${sourceSpecifier}:*`);
|
|
217
|
-
}
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 3. Named Exports & Re-exports with specifiers
|
|
222
|
-
if (node.source) {
|
|
223
|
-
// Re-export: export { x } from 'mod'
|
|
224
|
-
const specifier = node.source.value;
|
|
225
|
-
fileNode.explicitImports.add(specifier);
|
|
226
|
-
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
227
|
-
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (node.specifiers) {
|
|
231
|
-
node.specifiers.forEach((spec) => {
|
|
232
|
-
const exportedName = spec.exported.name || (spec.exported.type === "Identifier" ? spec.exported.name : spec.exported.value);
|
|
233
|
-
const localName = spec.local.name || (spec.local.type === "Identifier" ? spec.local.name : spec.local.value);
|
|
234
|
-
fileNode.internalExports.set(exportedName, {
|
|
235
|
-
type: "re-export",
|
|
236
|
-
source: specifier,
|
|
237
|
-
originalName: localName,
|
|
238
|
-
start: spec.start,
|
|
239
|
-
end: spec.end,
|
|
240
|
-
});
|
|
241
|
-
fileNode.importedSymbols.add(`${specifier}:${localName}`);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
} else if (node.declaration) {
|
|
245
|
-
// Direct declaration export: export const x = 1, export function f() {}
|
|
246
|
-
const decl = node.declaration;
|
|
247
|
-
if (decl.type === "VariableDeclaration") {
|
|
248
|
-
decl.declarations.forEach((d) => {
|
|
249
|
-
this._extractNamesFromPattern(d.id, (name) => {
|
|
250
|
-
fileNode.internalExports.set(name, {
|
|
251
|
-
type: "variable",
|
|
252
|
-
start: d.start,
|
|
253
|
-
end: d.end
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
} else if (decl.id && decl.id.name) {
|
|
258
|
-
let type = "unknown";
|
|
259
|
-
if (decl.type === "FunctionDeclaration") type = "function";
|
|
260
|
-
else if (decl.type === "ClassDeclaration") type = "class";
|
|
261
|
-
else if (decl.type === "TSEnumDeclaration") type = "enum";
|
|
262
|
-
else if (decl.type === "TSInterfaceDeclaration") type = "interface";
|
|
263
|
-
else if (decl.type === "TSTypeAliasDeclaration") type = "type";
|
|
264
|
-
else if (decl.type === "TSModuleDeclaration") type = "namespace";
|
|
265
|
-
|
|
266
|
-
fileNode.internalExports.set(decl.id.name, {
|
|
267
|
-
type,
|
|
268
|
-
start: decl.start,
|
|
269
|
-
end: decl.end
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
} else if (node.specifiers) {
|
|
273
|
-
// Export existing locals: export { x, y as z }
|
|
274
|
-
node.specifiers.forEach((spec) => {
|
|
275
|
-
const exportedName = spec.exported.name || (spec.exported.type === "Identifier" ? spec.exported.name : spec.exported.value);
|
|
276
|
-
const localName = spec.local.name || (spec.local.type === "Identifier" ? spec.local.name : spec.local.value);
|
|
277
|
-
fileNode.internalExports.set(exportedName, {
|
|
278
|
-
type: "export",
|
|
279
|
-
originalName: localName,
|
|
280
|
-
start: spec.start,
|
|
281
|
-
end: spec.end,
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
_extractNamesFromPattern(node, callback) {
|
|
288
|
-
if (!node) return;
|
|
289
|
-
if (node.type === "Identifier") {
|
|
290
|
-
callback(node.name);
|
|
291
|
-
} else if (node.type === "ObjectPattern") {
|
|
292
|
-
node.properties.forEach(p => {
|
|
293
|
-
if (p.type === "Property") {
|
|
294
|
-
this._extractNamesFromPattern(p.value, callback);
|
|
295
|
-
} else if (p.type === "RestElement") {
|
|
296
|
-
this._extractNamesFromPattern(p.argument, callback);
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
} else if (node.type === "ArrayPattern") {
|
|
300
|
-
node.elements.forEach(e => {
|
|
301
|
-
if (e) this._extractNamesFromPattern(e, callback);
|
|
302
|
-
});
|
|
303
|
-
} else if (node.type === "AssignmentPattern") {
|
|
304
|
-
this._extractNamesFromPattern(node.left, callback);
|
|
305
|
-
} else if (node.type === "RestElement") {
|
|
306
|
-
this._extractNamesFromPattern(node.argument, callback);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
handleCallExpression(node, fileNode) {
|
|
311
|
-
// Dynamic import(): import('./module')
|
|
312
|
-
if (node.callee.type === "Import" && node.arguments.length > 0) {
|
|
313
|
-
const arg = node.arguments[0];
|
|
314
|
-
if (arg.type === "StringLiteral") {
|
|
315
|
-
const specifier = arg.value;
|
|
316
|
-
fileNode.explicitImports.add(specifier);
|
|
317
|
-
fileNode.dynamicImports.add(specifier);
|
|
318
|
-
fileNode.importedSymbols.add(`${specifier}:*`); // Dynamic import usually consumes the whole namespace
|
|
319
|
-
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
320
|
-
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
323
|
-
if (fileNode.calculatedDynamicImports) {
|
|
324
|
-
fileNode.calculatedDynamicImports.push({ kind: arg.type, start: arg.start });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
} else if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length > 0 && node.arguments[0].type === "StringLiteral") {
|
|
328
|
-
const specifier = node.arguments[0].value;
|
|
329
|
-
fileNode.explicitImports.add(specifier);
|
|
330
|
-
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
331
|
-
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
332
|
-
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
handleJsxElement(node, fileNode) {
|
|
338
|
-
const getElementName = (nameNode) => {
|
|
339
|
-
if (nameNode.type === "JSXIdentifier") return nameNode.name;
|
|
340
|
-
if (nameNode.type === "JSXMemberExpression") return `${getElementName(nameNode.object)}.${nameNode.property.name}`;
|
|
341
|
-
if (nameNode.type === "JSXNamespacedName") return `${nameNode.namespace.name}:${nameNode.name.name}`;
|
|
342
|
-
return "unknown";
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
if (node.openingElement) {
|
|
346
|
-
const tagName = getElementName(node.openingElement.name);
|
|
347
|
-
fileNode.jsxComponents.add(tagName);
|
|
348
|
-
|
|
349
|
-
if (node.openingElement.attributes) {
|
|
350
|
-
node.openingElement.attributes.forEach(attr => {
|
|
351
|
-
if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier") {
|
|
352
|
-
fileNode.jsxProps.add(`${tagName}:${attr.name.name}`);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
handleDecorator(node, fileNode) {
|
|
360
|
-
const getDecoratorName = (expr) => {
|
|
361
|
-
if (expr.type === "Identifier") return expr.name;
|
|
362
|
-
if (expr.type === "CallExpression") return getDecoratorName(expr.callee);
|
|
363
|
-
if (expr.type === "MemberExpression") {
|
|
364
|
-
const prop = expr.property.name || expr.property.value;
|
|
365
|
-
return prop || "unknown";
|
|
366
|
-
}
|
|
367
|
-
return "unknown";
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
const decoratorName = getDecoratorName(node.expression);
|
|
371
|
-
if (decoratorName !== "unknown") {
|
|
372
|
-
fileNode.decorators.add(decoratorName);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
_extractPackageName(specifier) {
|
|
377
|
-
if (specifier.startsWith('@')) {
|
|
378
|
-
const parts = specifier.split('/');
|
|
379
|
-
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
380
|
-
}
|
|
381
|
-
return specifier.split('/')[0];
|
|
382
|
-
}
|
|
383
11
|
}
|
package/src/api/HeadlessAPI.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import EventEmitter from 'events';
|
|
16
16
|
import path from 'path';
|
|
17
|
-
import { RefactoringEngine } from '../index.js';
|
|
17
|
+
// import { RefactoringEngine } from '../index.js';
|
|
18
18
|
|
|
19
19
|
export class HeadlessAPI extends EventEmitter {
|
|
20
20
|
constructor(options = {}) {
|
package/src/api/PluginSDK.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================================
|
|
3
|
-
* Plugin SDK for entkapp v4.
|
|
3
|
+
* Plugin SDK for entkapp v4.7.0 (Enterprise Edition)
|
|
4
4
|
* ============================================================================
|
|
5
5
|
* Provides utilities and helpers for developing custom plugins that extend
|
|
6
6
|
* entkapp's analysis and healing capabilities.
|
|
@@ -21,8 +21,6 @@ export class PluginSDKBase extends BasePlugin {
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Register a hook for a specific lifecycle event
|
|
24
|
-
* @param {string} eventName - Event name (e.g., 'analyze:start', 'refactor:complete')
|
|
25
|
-
* @param {Function} handler - Handler function
|
|
26
24
|
*/
|
|
27
25
|
registerHook(eventName, handler) {
|
|
28
26
|
if (!this.hooks.has(eventName)) {
|
|
@@ -33,8 +31,6 @@ export class PluginSDKBase extends BasePlugin {
|
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* Emit a hook event
|
|
36
|
-
* @param {string} eventName - Event name
|
|
37
|
-
* @param {Object} data - Event data
|
|
38
34
|
*/
|
|
39
35
|
async emitHook(eventName, data) {
|
|
40
36
|
const handlers = this.hooks.get(eventName) || [];
|
|
@@ -45,7 +41,6 @@ export class PluginSDKBase extends BasePlugin {
|
|
|
45
41
|
|
|
46
42
|
/**
|
|
47
43
|
* Register a code transformer
|
|
48
|
-
* @param {Function} transformer - Transformer function
|
|
49
44
|
*/
|
|
50
45
|
registerTransformer(transformer) {
|
|
51
46
|
this.transformers.push(transformer);
|
|
@@ -53,41 +48,17 @@ export class PluginSDKBase extends BasePlugin {
|
|
|
53
48
|
|
|
54
49
|
/**
|
|
55
50
|
* Register a validator
|
|
56
|
-
* @param {Function} validator - Validator function
|
|
57
51
|
*/
|
|
58
52
|
registerValidator(validator) {
|
|
59
53
|
this.validators.push(validator);
|
|
60
54
|
}
|
|
61
55
|
|
|
62
56
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @param {string} code - Source code
|
|
65
|
-
* @param {string} filePath - File path
|
|
66
|
-
* @returns {Promise<string>} Transformed code
|
|
57
|
+
* Enterprise: Register a Call Graph visitor
|
|
67
58
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
result = await transformer(result, filePath);
|
|
72
|
-
}
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Run all validators on a code string
|
|
78
|
-
* @param {string} code - Source code
|
|
79
|
-
* @param {string} filePath - File path
|
|
80
|
-
* @returns {Promise<Array>} Validation errors
|
|
81
|
-
*/
|
|
82
|
-
async runValidators(code, filePath) {
|
|
83
|
-
const errors = [];
|
|
84
|
-
for (const validator of this.validators) {
|
|
85
|
-
const result = await validator(code, filePath);
|
|
86
|
-
if (result && result.length > 0) {
|
|
87
|
-
errors.push(...result);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return errors;
|
|
59
|
+
registerCallGraphVisitor(visitor) {
|
|
60
|
+
this.context.callGraphVisitors = this.context.callGraphVisitors || [];
|
|
61
|
+
this.context.callGraphVisitors.push(visitor);
|
|
91
62
|
}
|
|
92
63
|
}
|
|
93
64
|
|
|
@@ -97,8 +68,6 @@ export class PluginSDKBase extends BasePlugin {
|
|
|
97
68
|
export class PluginSDK {
|
|
98
69
|
/**
|
|
99
70
|
* Create a custom plugin class
|
|
100
|
-
* @param {Object} config - Plugin configuration
|
|
101
|
-
* @returns {Class} Plugin class
|
|
102
71
|
*/
|
|
103
72
|
static createPlugin(config) {
|
|
104
73
|
return class CustomPlugin extends PluginSDKBase {
|
|
@@ -136,163 +105,30 @@ export class PluginSDK {
|
|
|
136
105
|
return await config.analyze(node, filePath, this.context);
|
|
137
106
|
}
|
|
138
107
|
}
|
|
139
|
-
|
|
140
|
-
async transform(code, filePath) {
|
|
141
|
-
if (config.transform) {
|
|
142
|
-
return await config.transform(code, filePath, this.context);
|
|
143
|
-
}
|
|
144
|
-
return code;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async validate(code, filePath) {
|
|
148
|
-
if (config.validate) {
|
|
149
|
-
return await config.validate(code, filePath, this.context);
|
|
150
|
-
}
|
|
151
|
-
return [];
|
|
152
|
-
}
|
|
153
108
|
};
|
|
154
109
|
}
|
|
155
110
|
|
|
156
111
|
/**
|
|
157
|
-
* Create a
|
|
158
|
-
* @param {Object} config - Configuration for CSS-in-JS analysis
|
|
159
|
-
* @returns {Class} Plugin class
|
|
112
|
+
* Create a specialized member usage analyzer
|
|
160
113
|
*/
|
|
161
|
-
static
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
async analyze(node, filePath) {
|
|
178
|
-
// Track CSS-in-JS imports
|
|
179
|
-
for (const lib of cssLibraries) {
|
|
180
|
-
if (node.explicitImports.has(lib)) {
|
|
181
|
-
node.cssInJsLibraries = node.cssInJsLibraries || new Set();
|
|
182
|
-
node.cssInJsLibraries.add(lib);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Detect styled component definitions
|
|
187
|
-
const styledPattern = /(?:styled|css|keyframes)\s*\.\w+|styled\(\w+\)/g;
|
|
188
|
-
for (const match of (node.rawCode || '').matchAll(styledPattern)) {
|
|
189
|
-
node.styledComponentUsages = node.styledComponentUsages || [];
|
|
190
|
-
node.styledComponentUsages.push(match[0]);
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
|
|
194
|
-
async validate(code, filePath) {
|
|
195
|
-
const errors = [];
|
|
196
|
-
// Check for unused CSS-in-JS definitions
|
|
197
|
-
const unusedStylesPattern = /(?:const|let|var)\s+(\w+)\s*=\s*(?:styled|css)\./g;
|
|
198
|
-
const matches = [...code.matchAll(unusedStylesPattern)];
|
|
199
|
-
|
|
200
|
-
for (const match of matches) {
|
|
201
|
-
const styleName = match[1];
|
|
202
|
-
const usagePattern = new RegExp(`\\b${styleName}\\b`);
|
|
203
|
-
if (!usagePattern.test(code.substring(match.index + match[0].length))) {
|
|
204
|
-
errors.push({
|
|
205
|
-
type: 'unused-style',
|
|
206
|
-
name: styleName,
|
|
207
|
-
line: code.substring(0, match.index).split('\n').length,
|
|
208
|
-
message: `Unused CSS-in-JS definition: ${styleName}`
|
|
209
|
-
});
|
|
114
|
+
static createMemberAnalyzer(config = {}) {
|
|
115
|
+
return this.createPlugin({
|
|
116
|
+
name: config.name || 'custom-member-analyzer',
|
|
117
|
+
async analyze(node, filePath, context) {
|
|
118
|
+
// Custom logic to protect or flag specific members
|
|
119
|
+
if (config.protectMembers) {
|
|
120
|
+
for (const [symbol, meta] of node.internalExports.entries()) {
|
|
121
|
+
if (meta.members) {
|
|
122
|
+
meta.members.forEach(m => {
|
|
123
|
+
if (config.protectMembers.includes(m.name)) {
|
|
124
|
+
m.isPublic = true; // Mark as public to protect from deletion
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
210
130
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return errors;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Create an asset tracking plugin
|
|
220
|
-
* @param {Object} config - Configuration for asset tracking
|
|
221
|
-
* @returns {Class} Plugin class
|
|
222
|
-
*/
|
|
223
|
-
static createAssetTrackingPlugin(config = {}) {
|
|
224
|
-
const assetExtensions = config.extensions || [
|
|
225
|
-
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp',
|
|
226
|
-
'.mp4', '.webm', '.mp3', '.wav',
|
|
227
|
-
'.woff', '.woff2', '.ttf', '.eot'
|
|
228
|
-
];
|
|
229
|
-
|
|
230
|
-
return this.createPlugin({
|
|
231
|
-
name: config.name || 'asset-tracker',
|
|
232
|
-
configFiles: [],
|
|
233
|
-
routePatterns: [/\.(tsx?|jsx?)$/],
|
|
234
|
-
|
|
235
|
-
async analyze(node, filePath) {
|
|
236
|
-
node.assetReferences = node.assetReferences || new Set();
|
|
237
|
-
|
|
238
|
-
// Track asset imports
|
|
239
|
-
const assetImportPattern = /import\s+(?:\*\s+as\s+\w+|[\w\s,{}]+)\s+from\s+['"]([^'"]+(?:${assetExtensions.join('|')}))['"]/g;
|
|
240
|
-
for (const match of (node.rawCode || '').matchAll(assetImportPattern)) {
|
|
241
|
-
node.assetReferences.add(match[1]);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Track asset requires
|
|
245
|
-
const assetRequirePattern = /require\s*\(\s*['"]([^'"]+(?:${assetExtensions.join('|')}))['"]\s*\)/g;
|
|
246
|
-
for (const match of (node.rawCode || '').matchAll(assetRequirePattern)) {
|
|
247
|
-
node.assetReferences.add(match[1]);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Track asset URLs in strings
|
|
251
|
-
const assetUrlPattern = /['"]([^'"]*(?:${assetExtensions.join('|')}))['"]/g;
|
|
252
|
-
for (const match of (node.rawCode || '').matchAll(assetUrlPattern)) {
|
|
253
|
-
node.assetReferences.add(match[1]);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Create a monorepo awareness plugin
|
|
261
|
-
* @param {Object} config - Configuration for monorepo support
|
|
262
|
-
* @returns {Class} Plugin class
|
|
263
|
-
*/
|
|
264
|
-
static createMonorepoPlugin(config = {}) {
|
|
265
|
-
return this.createPlugin({
|
|
266
|
-
name: config.name || 'monorepo-aware',
|
|
267
|
-
configFiles: config.configFiles || ['nx.json', 'pnpm-workspace.yaml', 'lerna.json'],
|
|
268
|
-
|
|
269
|
-
async analyze(node, filePath) {
|
|
270
|
-
// Track workspace package references
|
|
271
|
-
node.workspaceReferences = node.workspaceReferences || new Set();
|
|
272
|
-
|
|
273
|
-
// Detect workspace imports (e.g., @workspace/package-name)
|
|
274
|
-
const workspacePattern = /@[\w-]+\/[\w-]+/g;
|
|
275
|
-
for (const match of (node.rawCode || '').matchAll(workspacePattern)) {
|
|
276
|
-
node.workspaceReferences.add(match[0]);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Create a circular dependency detector plugin
|
|
284
|
-
* @param {Object} config - Configuration
|
|
285
|
-
* @returns {Class} Plugin class
|
|
286
|
-
*/
|
|
287
|
-
static createCircularDepPlugin(config = {}) {
|
|
288
|
-
return this.createPlugin({
|
|
289
|
-
name: config.name || 'circular-dep-detector',
|
|
290
|
-
|
|
291
|
-
async analyze(node, filePath) {
|
|
292
|
-
node.potentialCycles = node.potentialCycles || [];
|
|
293
|
-
// Cycle detection will be handled by the main engine
|
|
294
|
-
}
|
|
295
|
-
});
|
|
131
|
+
});
|
|
296
132
|
}
|
|
297
133
|
}
|
|
298
134
|
|