entkapp 5.4.0 → 5.6.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/README.md +7 -1
- package/bin/cli.js +117 -58
- package/bin/cli.mjs +175 -0
- package/index.cjs +18 -0
- package/index.mjs +51 -0
- package/package.json +7 -6
- package/src/EngineContext.js +0 -6
- package/src/EngineContext.mjs +428 -0
- package/src/Initializer.mjs +82 -0
- package/src/analyzers/CodeSmellAnalyzer.mjs +106 -0
- package/src/analyzers/OxcAnalyzer.mjs +11 -0
- package/src/api/HeadlessAPI.js +31 -16
- package/src/api/HeadlessAPI.mjs +369 -0
- package/src/api/PluginSDK.mjs +135 -0
- package/src/ast/ASTAnalyzer.js +23 -97
- package/src/ast/ASTAnalyzer.mjs +742 -0
- package/src/ast/AdvancedAnalysis.mjs +586 -0
- package/src/ast/BarrelParser.mjs +230 -0
- package/src/ast/DeadCodeDetector.js +7 -5
- package/src/ast/DeadCodeDetector.mjs +92 -0
- package/src/ast/MagicDetector.js +43 -23
- package/src/ast/MagicDetector.mjs +203 -0
- package/src/ast/OxcAnalyzer.js +27 -190
- package/src/ast/OxcAnalyzer.mjs +188 -0
- package/src/ast/SecretScanner.mjs +374 -0
- package/src/healing/GitSandbox.mjs +82 -0
- package/src/healing/SelfHealer.mjs +48 -0
- package/src/index.js +41 -88
- package/src/index.mjs +1176 -0
- package/src/performance/GraphCache.js +0 -27
- package/src/performance/GraphCache.mjs +108 -0
- package/src/performance/SupplyChainGuard.mjs +92 -0
- package/src/performance/WorkerPool.js +4 -22
- package/src/performance/WorkerPool.mjs +132 -0
- package/src/performance/WorkerTaskRunner.js +0 -26
- package/src/performance/WorkerTaskRunner.mjs +144 -0
- package/src/plugins/BasePlugin.js +27 -16
- package/src/plugins/BasePlugin.mjs +240 -0
- package/src/plugins/PluginRegistry.js +0 -44
- package/src/plugins/PluginRegistry.mjs +203 -0
- package/src/plugins/ecosystems/BackendServices.mjs +197 -0
- package/src/plugins/ecosystems/GenericPlugins.mjs +142 -0
- package/src/plugins/ecosystems/ModernFrameworks.mjs +162 -0
- package/src/plugins/ecosystems/MorePlugins.mjs +562 -0
- package/src/plugins/ecosystems/NewPlugins.mjs +526 -0
- package/src/plugins/ecosystems/NextJsPlugin.mjs +45 -0
- package/src/plugins/ecosystems/PluginLoader.mjs +193 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.mjs +56 -0
- package/src/plugins/ecosystems/UltimateBundle.js +0 -259
- package/src/plugins/ecosystems/UltimateBundle.mjs +1182 -0
- package/src/refractor/ImpactAnalyzer.mjs +92 -0
- package/src/refractor/SourceRewriter.mjs +86 -0
- package/src/refractor/TransactionManager.mjs +5 -0
- package/src/refractor/TypeIntegrity.mjs +4 -0
- package/src/resolution/BuildOrchestrator.mjs +46 -0
- package/src/resolution/CircularDetector.mjs +91 -0
- package/src/resolution/ConfigGenerator.mjs +83 -0
- package/src/resolution/ConfigLoader.js +26 -190
- package/src/resolution/ConfigLoader.mjs +94 -0
- package/src/resolution/DepencyResolver.mjs +66 -0
- package/src/resolution/DependencyFixer.mjs +88 -0
- package/src/resolution/DependencyProfiler.mjs +286 -0
- package/src/resolution/EntryPointDetector.mjs +134 -0
- package/src/resolution/FrameworkConfigParser.mjs +119 -0
- package/src/resolution/GraphAnalyzer.js +1 -65
- package/src/resolution/GraphAnalyzer.mjs +80 -0
- package/src/resolution/MigrationAnalyzer.mjs +60 -0
- package/src/resolution/PathMapper.js +0 -81
- package/src/resolution/PathMapper.mjs +82 -0
- package/src/resolution/TSConfigLoader.js +1 -3
- package/src/resolution/TSConfigLoader.mjs +162 -0
- package/src/resolution/WorkSpaceGraph.js +89 -260
- package/src/resolution/WorkSpaceGraph.mjs +183 -0
- package/src/resolution/WorkspaceDiagnostic.mjs +139 -0
- package/test.js +76 -0
- package/wrangler.toml +6 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ansis from 'ansis';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Core In-Memory Graph Node Representation.
|
|
7
|
+
* Tracks structural metadata, dependencies, and usage metrics for a single file.
|
|
8
|
+
*/
|
|
9
|
+
export class GraphNode {
|
|
10
|
+
constructor(filePath) {
|
|
11
|
+
this.filePath = filePath;
|
|
12
|
+
this.incomingEdges = new Set(); // Files that import THIS file
|
|
13
|
+
this.outgoingEdges = new Set(); // Files that THIS file imports
|
|
14
|
+
|
|
15
|
+
this.explicitImports = new Set(); // Raw import strings
|
|
16
|
+
this.importedSymbols = new Set(); // "file:symbol" or "file:*"
|
|
17
|
+
this.internalExports = new Map(); // symbol -> { type, start, end, ... }
|
|
18
|
+
|
|
19
|
+
this.externalPackageUsage = new Set();
|
|
20
|
+
this.instantiatedIdentifiers = new Set();
|
|
21
|
+
this.propertyAccessChains = new Set();
|
|
22
|
+
this.rawStringReferences = new Set();
|
|
23
|
+
this.jsxComponents = new Set();
|
|
24
|
+
this.jsxProps = new Set();
|
|
25
|
+
this.decorators = new Set();
|
|
26
|
+
this.dynamicImports = new Set();
|
|
27
|
+
this.calculatedDynamicImports = [];
|
|
28
|
+
|
|
29
|
+
this.isEntry = false;
|
|
30
|
+
this.isLibraryEntry = false;
|
|
31
|
+
this.isFrameworkComponent = false;
|
|
32
|
+
this.symbolSourceLocations = new Map(); // symbol -> { line, column }
|
|
33
|
+
this.localSuppressedRules = new Set();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isSymbolReferencedExternally(symbolName, projectGraph, visited = new Set()) {
|
|
37
|
+
const visitKey = `${this.filePath}:${symbolName}`;
|
|
38
|
+
if (visited.has(visitKey)) return false;
|
|
39
|
+
visited.add(visitKey);
|
|
40
|
+
|
|
41
|
+
// Self-reference check
|
|
42
|
+
if (this.instantiatedIdentifiers.has(symbolName)) return true;
|
|
43
|
+
for (const accessChain of this.propertyAccessChains) {
|
|
44
|
+
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const parentPath of this.incomingEdges) {
|
|
48
|
+
const parentNode = projectGraph.get(parentPath);
|
|
49
|
+
if (!parentNode) continue;
|
|
50
|
+
|
|
51
|
+
// Direct Import Tokens
|
|
52
|
+
const absoluteImportKey = `${this.filePath}:${symbolName}`;
|
|
53
|
+
const absoluteStarKey = `${this.filePath}:*`;
|
|
54
|
+
if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) return true;
|
|
55
|
+
|
|
56
|
+
// Advanced Member Usage Detection
|
|
57
|
+
if (parentNode.propertyAccessChains.has(symbolName)) return true;
|
|
58
|
+
|
|
59
|
+
// If it's a member access like 'Logger.error' or 'app.start', we check for the leaf name usage.
|
|
60
|
+
if (symbolName.includes('.')) {
|
|
61
|
+
const parts = symbolName.split('.');
|
|
62
|
+
const leafName = parts[parts.length - 1];
|
|
63
|
+
const parentName = parts[0];
|
|
64
|
+
|
|
65
|
+
if ((parentNode.instantiatedIdentifiers.has(parentName) ||
|
|
66
|
+
parentNode.importedSymbols.has(`${this.filePath}:${parentName}`)) &&
|
|
67
|
+
parentNode.instantiatedIdentifiers.has(leafName)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Dynamic Import Check
|
|
73
|
+
if (parentNode.dynamicImports.has(this.filePath) || parentNode.explicitImports.has(this.filePath)) {
|
|
74
|
+
if (parentNode.dynamicImports.has(this.filePath)) return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Recursive Re-export Resolution
|
|
78
|
+
for (const [exportedName, exportMeta] of parentNode.internalExports.entries()) {
|
|
79
|
+
const isMatch = (exportMeta.type === 're-export' || exportMeta.type === 're-export-namespace' || exportMeta.type === 're-export-all') &&
|
|
80
|
+
(exportMeta.originalName === symbolName || exportMeta.originalName === '*' || exportMeta.type === 're-export-all') &&
|
|
81
|
+
(exportMeta.source === this.filePath || (exportMeta.source && path.resolve(path.dirname(parentPath), exportMeta.source) === this.filePath));
|
|
82
|
+
|
|
83
|
+
if (isMatch) {
|
|
84
|
+
if (parentNode.isSymbolReferencedExternally(exportedName, projectGraph, visited)) return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fuzzy / Dynamic usage (Identifiers, Strings, JSX, Decorators)
|
|
89
|
+
// UPGRADE: Only check for fuzzy matches if we have a reason to believe it might be used.
|
|
90
|
+
// We skip global names like 'toLowerCase' unless they are explicitly imported.
|
|
91
|
+
const isGlobalName = ['toLowerCase', 'toUpperCase', 'toString', 'valueOf', 'hasOwnProperty', 'constructor'].includes(symbolName);
|
|
92
|
+
|
|
93
|
+
if (!isGlobalName) {
|
|
94
|
+
if (parentNode.instantiatedIdentifiers.has(symbolName)) return true;
|
|
95
|
+
if (parentNode.rawStringReferences.has(symbolName)) return true;
|
|
96
|
+
if (parentNode.jsxComponents.has(symbolName)) return true;
|
|
97
|
+
if (parentNode.decorators.has(symbolName)) return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const accessChain of parentNode.propertyAccessChains) {
|
|
101
|
+
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class EngineContext {
|
|
110
|
+
constructor(cwd) {
|
|
111
|
+
this.cwd = cwd || process.cwd();
|
|
112
|
+
this.projectGraph = new Map(); // Path -> GraphNode
|
|
113
|
+
this.usedExternalPackages = new Set();
|
|
114
|
+
this.unimportedUsedPackages = new Set();
|
|
115
|
+
this.importedUnusedPackages = new Set();
|
|
116
|
+
this.unusedBinaries = new Set();
|
|
117
|
+
this.manifestDependencies = new Map();
|
|
118
|
+
|
|
119
|
+
this.isWorkspaceEnabled = false;
|
|
120
|
+
this.monorepoPackageRoots = new Set();
|
|
121
|
+
this.verbose = false;
|
|
122
|
+
this.metrics = { totalFilesScanned: 0, cacheHits: 0, cacheMisses: 0 };
|
|
123
|
+
this.allSecretFindings = [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getOrCreateNode(filePath) {
|
|
127
|
+
if (!this.projectGraph.has(filePath)) {
|
|
128
|
+
this.projectGraph.set(filePath, new GraphNode(filePath));
|
|
129
|
+
// NEW: Automatically track package.json files in manifestDependencies
|
|
130
|
+
if (filePath.endsWith('package.json')) {
|
|
131
|
+
this.manifestDependencies.set(filePath, new Set());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return this.projectGraph.get(filePath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async generateSummaryReport() {
|
|
138
|
+
const report = {
|
|
139
|
+
orphanedFiles: [],
|
|
140
|
+
deadExports: [],
|
|
141
|
+
unusedDependencies: [],
|
|
142
|
+
unimportedUsedPackages: [],
|
|
143
|
+
importedUnusedPackages: [],
|
|
144
|
+
unusedBinaries: []
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// --- DEEP REACHABILITY ANALYSIS (BFS) ---
|
|
148
|
+
const reachableFiles = new Set();
|
|
149
|
+
const queue = [];
|
|
150
|
+
|
|
151
|
+
// 1. Initialize BFS with Entry Points
|
|
152
|
+
for (const [filePath, node] of this.projectGraph.entries()) {
|
|
153
|
+
// FIX: Also check if the file is explicitly mentioned in package.json bin or main
|
|
154
|
+
if (node.isEntry || node.isLibraryEntry || node.isFrameworkComponent) {
|
|
155
|
+
reachableFiles.add(filePath);
|
|
156
|
+
queue.push(filePath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// UPGRADE: Ensure package.json main and bin are always entry points
|
|
161
|
+
// UPGRADE: Comprehensive Entry Point Discovery (Manifests + Tooling)
|
|
162
|
+
for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
|
|
163
|
+
try {
|
|
164
|
+
const data = JSON.parse(fsSync.readFileSync(manifestPath, 'utf8'));
|
|
165
|
+
const pkgDir = path.dirname(manifestPath);
|
|
166
|
+
const entries = [];
|
|
167
|
+
|
|
168
|
+
// 1. Standard Manifest Entries
|
|
169
|
+
if (data.main) entries.push(path.resolve(pkgDir, data.main));
|
|
170
|
+
if (data.module) entries.push(path.resolve(pkgDir, data.module));
|
|
171
|
+
if (data.exports) {
|
|
172
|
+
const unwind = (val) => {
|
|
173
|
+
if (typeof val === 'string') entries.push(path.resolve(pkgDir, val));
|
|
174
|
+
else if (typeof val === 'object' && val !== null) Object.values(val).forEach(unwind);
|
|
175
|
+
};
|
|
176
|
+
unwind(data.exports);
|
|
177
|
+
}
|
|
178
|
+
if (data.bin) {
|
|
179
|
+
if (typeof data.bin === 'string') entries.push(path.resolve(pkgDir, data.bin));
|
|
180
|
+
else Object.values(data.bin).forEach(b => entries.push(path.resolve(pkgDir, b)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 2. Protect Documentation & Configs
|
|
184
|
+
const possibleConfigs = ['vite.config.mjs', 'vite.config.ts', 'vitest.config.ts', 'tsconfig.json'];
|
|
185
|
+
possibleConfigs.forEach(c => entries.push(path.resolve(pkgDir, c)));
|
|
186
|
+
|
|
187
|
+
entries.forEach(e => {
|
|
188
|
+
const normalized = e.replace(/\\/g, '/');
|
|
189
|
+
// Check with common extensions if not found
|
|
190
|
+
const candidates = [normalized, normalized + '.mjs', normalized + '.ts', normalized + '.tsx', normalized + '/index.mjs', normalized + '/index.ts'];
|
|
191
|
+
for (const cand of candidates) {
|
|
192
|
+
if (this.projectGraph.has(cand) && !reachableFiles.has(cand)) {
|
|
193
|
+
reachableFiles.add(cand);
|
|
194
|
+
queue.push(cand);
|
|
195
|
+
this.projectGraph.get(cand).isEntry = true;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// --- PLUGIN-BASED ECOSYSTEM DETECTION ---
|
|
202
|
+
// Plugins will now handle their own reachability and dependency validation.
|
|
203
|
+
if (this.pluginRegistry) {
|
|
204
|
+
const activePlugins = await this.pluginRegistry.getActivePlugins(pkgDir);
|
|
205
|
+
for (const plugin of activePlugins) {
|
|
206
|
+
if (typeof plugin.onDiscovery === 'function') {
|
|
207
|
+
await plugin.onDiscovery({
|
|
208
|
+
pkgDir,
|
|
209
|
+
data,
|
|
210
|
+
reachableFiles,
|
|
211
|
+
queue,
|
|
212
|
+
projectGraph: this.projectGraph,
|
|
213
|
+
context: this
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch (e) {}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 2. BFS Traversal
|
|
222
|
+
while (queue.length > 0) {
|
|
223
|
+
const currentPath = queue.shift();
|
|
224
|
+
const node = this.projectGraph.get(currentPath);
|
|
225
|
+
|
|
226
|
+
if (!node) continue;
|
|
227
|
+
|
|
228
|
+
// Track outgoing edges (static and linked dynamic imports)
|
|
229
|
+
if (node.outgoingEdges) {
|
|
230
|
+
for (const outgoingPath of node.outgoingEdges) {
|
|
231
|
+
const normalizedPath = outgoingPath.replace(/\\/g, '/');
|
|
232
|
+
if (!reachableFiles.has(normalizedPath)) {
|
|
233
|
+
reachableFiles.add(normalizedPath);
|
|
234
|
+
queue.push(normalizedPath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// UPGRADE: Handle non-literal dynamic imports (calculatedDynamicImports)
|
|
240
|
+
if ((node.calculatedDynamicImports && node.calculatedDynamicImports.length > 0) || (node.dynamicImports && node.dynamicImports.has('__DYNAMIC_PATTERN__'))) {
|
|
241
|
+
const dir = path.dirname(currentPath);
|
|
242
|
+
// Conservative: If we have a calculated dynamic import, any file in the same or sub directory could be a target
|
|
243
|
+
for (const [otherPath, _] of this.projectGraph.entries()) {
|
|
244
|
+
// Fix: Check if otherPath is in the same directory or a subdirectory of 'dir'
|
|
245
|
+
if (otherPath !== currentPath && otherPath.startsWith(dir) && !reachableFiles.has(otherPath)) {
|
|
246
|
+
// Additional check: If it's in a 'plugins' or 'modules' folder, it's very likely a dynamic target
|
|
247
|
+
const isLikelyDynamicTarget = otherPath.includes('/plugins/') || otherPath.includes('/modules/') || otherPath.includes('/dynamic/');
|
|
248
|
+
if (isLikelyDynamicTarget) {
|
|
249
|
+
reachableFiles.add(otherPath);
|
|
250
|
+
queue.push(otherPath);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// UPGRADE: Handle Worker/Threads usage (new Worker('path'))
|
|
257
|
+
for (const chain of node.propertyAccessChains) {
|
|
258
|
+
if (chain.includes('new Worker(') || chain.includes('Worker(')) {
|
|
259
|
+
// Check if any file path is mentioned in raw strings
|
|
260
|
+
for (const str of node.rawStringReferences) {
|
|
261
|
+
if (str.startsWith('.') || str.startsWith('/')) {
|
|
262
|
+
try {
|
|
263
|
+
const resolved = path.resolve(path.dirname(currentPath), str);
|
|
264
|
+
const extensions = ['.mjs', '.ts', '.mjs', '.cjs'];
|
|
265
|
+
for (const ext of extensions) {
|
|
266
|
+
const p = resolved.endsWith(ext) ? resolved : resolved + ext;
|
|
267
|
+
const normalized = p.replace(/\\/g, '/');
|
|
268
|
+
if (this.projectGraph.has(normalized) && !reachableFiles.has(normalized)) {
|
|
269
|
+
reachableFiles.add(normalized);
|
|
270
|
+
queue.push(normalized);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (e) {}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Additional check for unlinked dynamic imports (conservative)
|
|
280
|
+
if (node.dynamicImports) {
|
|
281
|
+
for (const dynamicPath of node.dynamicImports) {
|
|
282
|
+
if (dynamicPath.startsWith('__DYNAMIC_PATTERN__:')) {
|
|
283
|
+
const pattern = dynamicPath.split(':')[1];
|
|
284
|
+
const dir = path.dirname(currentPath);
|
|
285
|
+
for (const [otherPath, _] of this.projectGraph.entries()) {
|
|
286
|
+
if (otherPath.startsWith(dir) && !reachableFiles.has(otherPath)) {
|
|
287
|
+
reachableFiles.add(otherPath);
|
|
288
|
+
queue.push(otherPath);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- ANALYSIS PHASE ---
|
|
297
|
+
if (this.verbose) {
|
|
298
|
+
console.log(`[Reachability] Total reachable files: ${reachableFiles.size}`);
|
|
299
|
+
for (const f of reachableFiles) console.log(` • Reachable: ${path.relative(this.cwd, f)}`);
|
|
300
|
+
}
|
|
301
|
+
for (const [filePath, node] of this.projectGraph.entries()) {
|
|
302
|
+
const isSuppressed = node.localSuppressedRules.has('*') || node.localSuppressedRules.has('unused-file');
|
|
303
|
+
const isReachable = reachableFiles.has(filePath);
|
|
304
|
+
|
|
305
|
+
// Check for Orphaned Files
|
|
306
|
+
if (!isReachable && !isSuppressed) {
|
|
307
|
+
const relPath = path.relative(this.cwd, filePath).replace(/\\/g, '/');
|
|
308
|
+
report.orphanedFiles.push(relPath);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check for Dead Exports
|
|
312
|
+
if (isReachable) {
|
|
313
|
+
const isPublicAPI = node.isEntry || node.isLibraryEntry;
|
|
314
|
+
|
|
315
|
+
for (const [symbol, meta] of node.internalExports.entries()) {
|
|
316
|
+
if (symbol === '*' || symbol === 'default' || node.localSuppressedRules.has('unused-export') || node.localSuppressedRules.has(`unused-export:${symbol}`)) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const isSymbolUsed = isPublicAPI || node.isSymbolReferencedExternally(symbol, this.projectGraph);
|
|
321
|
+
|
|
322
|
+
if (!isSymbolUsed) {
|
|
323
|
+
const loc = node.symbolSourceLocations.get(symbol) || { line: 0, column: 0 };
|
|
324
|
+
report.deadExports.push({
|
|
325
|
+
symbol,
|
|
326
|
+
file: path.relative(this.cwd, filePath),
|
|
327
|
+
line: loc.line
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Member Analysis
|
|
332
|
+
if (meta.members && meta.members.length > 0 && isSymbolUsed) {
|
|
333
|
+
for (const member of meta.members) {
|
|
334
|
+
const fullMemberName = `${symbol}.${member.name}`;
|
|
335
|
+
|
|
336
|
+
if (isPublicAPI && member.isPublic !== false) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// UPGRADE: Check for dynamic usage in ANY reachable file, not just direct imports
|
|
341
|
+
let isMemberUsed = false;
|
|
342
|
+
for (const [otherPath, otherNode] of this.projectGraph.entries()) {
|
|
343
|
+
if (reachableFiles.has(otherPath)) {
|
|
344
|
+
if (otherNode.instantiatedIdentifiers.has(member.name) ||
|
|
345
|
+
otherNode.propertyAccessChains.has(fullMemberName) ||
|
|
346
|
+
otherNode.rawStringReferences.has(member.name)) {
|
|
347
|
+
isMemberUsed = true;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!isMemberUsed && !node.isSymbolReferencedExternally(fullMemberName, this.projectGraph)) {
|
|
354
|
+
const loc = node.symbolSourceLocations.get(fullMemberName) || { line: 0, column: 0 };
|
|
355
|
+
report.deadExports.push({
|
|
356
|
+
symbol: fullMemberName,
|
|
357
|
+
file: path.relative(this.cwd, filePath),
|
|
358
|
+
line: loc.line
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// --- DEPENDENCY ANALYSIS ---
|
|
368
|
+
const usedByReachableFiles = new Set();
|
|
369
|
+
reachableFiles.forEach(filePath => {
|
|
370
|
+
const node = this.projectGraph.get(filePath);
|
|
371
|
+
if (node) {
|
|
372
|
+
node.externalPackageUsage.forEach(pkg => usedByReachableFiles.add(pkg));
|
|
373
|
+
|
|
374
|
+
// Conservative check for dynamic imports in reachable files
|
|
375
|
+
if (node.calculatedDynamicImports) {
|
|
376
|
+
node.calculatedDynamicImports.forEach(entry => {
|
|
377
|
+
const expr = typeof entry === 'string' ? entry : (entry.pattern || entry.text || '');
|
|
378
|
+
// If it looks like a package name (no dots/slashes), assume it's used
|
|
379
|
+
if (expr && !expr.includes('.') && !expr.includes('/') && !expr.includes('`') && !expr.includes("'") && !expr.includes('"')) {
|
|
380
|
+
usedByReachableFiles.add(expr);
|
|
381
|
+
}
|
|
382
|
+
// Check raw strings for potential package names
|
|
383
|
+
node.rawStringReferences.forEach(str => {
|
|
384
|
+
if (!str.startsWith('.') && !str.startsWith('/')) {
|
|
385
|
+
const pkg = str.startsWith('@') ? str.split('/').slice(0, 2).join('/') : str.split('/')[0];
|
|
386
|
+
usedByReachableFiles.add(pkg);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
|
|
395
|
+
const allDeps = [...(deps.dependencies || []), ...(deps.devDependencies || [])];
|
|
396
|
+
const hasTypeScriptFiles = Array.from(this.projectGraph.keys()).some(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
397
|
+
|
|
398
|
+
for (const dep of allDeps) {
|
|
399
|
+
if (dep.startsWith('@types/') || dep === 'entkapp' || (dep === 'typescript' && hasTypeScriptFiles)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// UPGRADE: Be more aggressive in detecting unused dependencies.
|
|
404
|
+
// Check both externalPackageUsage and rawStringReferences.
|
|
405
|
+
const isUsed = usedByReachableFiles.has(dep) ||
|
|
406
|
+
Array.from(reachableFiles).some(f => {
|
|
407
|
+
const node = this.projectGraph.get(f);
|
|
408
|
+
return node && (node.externalPackageUsage.has(dep) || node.rawStringReferences.has(dep));
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (!isUsed) {
|
|
412
|
+
report.unusedDependencies.push({
|
|
413
|
+
package: dep,
|
|
414
|
+
type: deps.dependencies.includes(dep) ? 'dependency' : 'devDependency',
|
|
415
|
+
manifest: path.relative(this.cwd, manifestPath)
|
|
416
|
+
});
|
|
417
|
+
this.importedUnusedPackages.add(dep);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
report.unimportedUsedPackages = Array.from(this.unimportedUsedPackages);
|
|
423
|
+
report.importedUnusedPackages = Array.from(this.importedUnusedPackages);
|
|
424
|
+
report.unusedBinaries = Array.from(this.unusedBinaries);
|
|
425
|
+
|
|
426
|
+
return report;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Entkapp Initializer: Analyzes the codebase to generate a package.json
|
|
7
|
+
* with correctly categorized dependencies and devDependencies.
|
|
8
|
+
*/
|
|
9
|
+
export class Initializer {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run() {
|
|
15
|
+
console.log("🚀 Starting entkapp intelligent initialization...");
|
|
16
|
+
const cwd = this.context.cwd || process.cwd();
|
|
17
|
+
|
|
18
|
+
// 1. Scan for all JS/TS files
|
|
19
|
+
const files = await glob('**/*.{js,ts,jsx,tsx,vue,svelte,astro}', {
|
|
20
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**'],
|
|
21
|
+
cwd
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const dependencies = new Set();
|
|
25
|
+
const devDependencies = new Set();
|
|
26
|
+
|
|
27
|
+
// Common dev tool indicators
|
|
28
|
+
const devIndicators = ['test', 'spec', 'config', 'stories', 'mock'];
|
|
29
|
+
|
|
30
|
+
console.log(`🔍 Analyzing ${files.length} files for imports...`);
|
|
31
|
+
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const content = await fs.readFile(path.join(cwd, file), 'utf8');
|
|
34
|
+
const isDevFile = devIndicators.some(ind => file.toLowerCase().includes(ind));
|
|
35
|
+
|
|
36
|
+
// Simple but effective regex for imports
|
|
37
|
+
const importMatches = content.matchAll(/(?:import|from|require)\s*\(?\s*['"]([^' "./][^'"]*)['"]/g);
|
|
38
|
+
|
|
39
|
+
for (const match of importMatches) {
|
|
40
|
+
const pkg = this.extractPackageName(match[1]);
|
|
41
|
+
if (pkg && !this.isBuiltIn(pkg)) {
|
|
42
|
+
if (isDevFile) devDependencies.add(pkg);
|
|
43
|
+
else dependencies.add(pkg);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Generate package.json
|
|
49
|
+
const pkgJson = {
|
|
50
|
+
name: path.basename(cwd),
|
|
51
|
+
version: "1.0.0",
|
|
52
|
+
description: "Initialized with entkapp",
|
|
53
|
+
main: "index.mjs",
|
|
54
|
+
scripts: {
|
|
55
|
+
"entkapp:run": "entkapp -r",
|
|
56
|
+
"entkapp:check": "entkapp -r --verbose"
|
|
57
|
+
},
|
|
58
|
+
dependencies: Object.fromEntries([...dependencies].sort().map(d => [d, "latest"])),
|
|
59
|
+
devDependencies: Object.fromEntries([...devDependencies].sort().map(d => [d, "latest"]))
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await fs.writeFile(path.join(cwd, 'package.json'), JSON.stringify(pkgJson, null, 2));
|
|
63
|
+
console.log("✅ package.json generated with analyzed dependencies.");
|
|
64
|
+
|
|
65
|
+
// 3. Create /entkapp folder
|
|
66
|
+
await fs.mkdir(path.join(cwd, 'entkapp'), { recursive: true });
|
|
67
|
+
console.log("✅ /entkapp configuration folder created.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
extractPackageName(specifier) {
|
|
71
|
+
if (specifier.startsWith('@')) {
|
|
72
|
+
const parts = specifier.split('/');
|
|
73
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
|
|
74
|
+
}
|
|
75
|
+
return specifier.split('/')[0];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isBuiltIn(pkg) {
|
|
79
|
+
const builtins = ['path', 'fs', 'os', 'crypto', 'http', 'https', 'stream', 'util', 'events', 'module', 'process'];
|
|
80
|
+
return builtins.includes(pkg) || pkg.startsWith('node:');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeSmellAnalyzer: Performs deep static analysis to find runtime risks and logic smells.
|
|
3
|
+
*/
|
|
4
|
+
export class CodeSmellAnalyzer {
|
|
5
|
+
constructor(context) {
|
|
6
|
+
this.context = context;
|
|
7
|
+
this.issues = [];
|
|
8
|
+
this.rules = {
|
|
9
|
+
'potential-null-pointer': {
|
|
10
|
+
message: 'Potential Null Pointer Risk: Accessing property on an object that might be undefined.',
|
|
11
|
+
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cant_access_property'
|
|
12
|
+
},
|
|
13
|
+
'infinite-loop-risk': {
|
|
14
|
+
message: 'Potential Infinite Loop: Loop condition does not seem to change within the body.',
|
|
15
|
+
link: 'https://en.wikipedia.org/wiki/Infinite_loop'
|
|
16
|
+
},
|
|
17
|
+
'implicit-type-coercion': {
|
|
18
|
+
message: 'Implicit Type Coercion: Using loose equality (==) can lead to unexpected runtime behavior.',
|
|
19
|
+
link: 'https://dorey.github.io/JavaScript-Equality-Table/'
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
analyze(fileNode) {
|
|
25
|
+
if (!fileNode.ast) return;
|
|
26
|
+
this.walk(fileNode.ast, fileNode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
walk(node, fileNode) {
|
|
30
|
+
if (!node) return;
|
|
31
|
+
|
|
32
|
+
// 1. Check for Loose Equality (==)
|
|
33
|
+
if (node.type === 'BinaryExpression' && (node.operator === '==' || node.operator === '!=')) {
|
|
34
|
+
this.addIssue('implicit-type-coercion', node, fileNode);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Check for Potential Infinite Loops (While loops with static conditions)
|
|
38
|
+
if (node.type === 'WhileStatement' && node.test.type === 'Literal' && node.test.value === true) {
|
|
39
|
+
// Check if there is a break or return inside
|
|
40
|
+
if (!this.hasExitStatement(node.body)) {
|
|
41
|
+
this.addIssue('infinite-loop-risk', node, fileNode);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3. Check for Null Pointer Risks (Accessing properties on potentially uninitialized vars)
|
|
46
|
+
if (node.type === 'MemberExpression' && node.object.type === 'Identifier') {
|
|
47
|
+
const varName = node.object.name;
|
|
48
|
+
if (this.isPotentiallyNull(varName, fileNode)) {
|
|
49
|
+
this.addIssue('potential-null-pointer', node, fileNode);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Recursively walk the AST
|
|
54
|
+
for (const key in node) {
|
|
55
|
+
if (node[key] && typeof node[key] === 'object') {
|
|
56
|
+
if (Array.isArray(node[key])) {
|
|
57
|
+
node[key].forEach(child => this.walk(child, fileNode));
|
|
58
|
+
} else {
|
|
59
|
+
this.walk(node[key], fileNode);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
addIssue(ruleId, node, fileNode) {
|
|
66
|
+
const rule = this.rules[ruleId];
|
|
67
|
+
fileNode.diagnostics = fileNode.diagnostics || [];
|
|
68
|
+
fileNode.diagnostics.push({
|
|
69
|
+
ruleId,
|
|
70
|
+
message: rule.message,
|
|
71
|
+
link: rule.link,
|
|
72
|
+
line: node.start ? this.getLineNumber(node.start, fileNode.content) : 0,
|
|
73
|
+
severity: 'warning'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
hasExitStatement(node) {
|
|
78
|
+
let found = false;
|
|
79
|
+
const check = (n) => {
|
|
80
|
+
if (!n || found) return;
|
|
81
|
+
if (n.type === 'BreakStatement' || n.type === 'ReturnStatement' || n.type === 'ThrowStatement') {
|
|
82
|
+
found = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const key in n) {
|
|
86
|
+
if (n[key] && typeof n[key] === 'object') {
|
|
87
|
+
if (Array.isArray(n[key])) n[key].forEach(check);
|
|
88
|
+
else check(n[key]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
check(node);
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isPotentiallyNull(name, fileNode) {
|
|
97
|
+
// Simple heuristic: if it's a variable declared without init or in a try-catch
|
|
98
|
+
const symbol = fileNode.symbolTable?.get(name);
|
|
99
|
+
return symbol && symbol.type === 'variable' && !symbol.initialized;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getLineNumber(pos, content) {
|
|
103
|
+
if (!content) return 0;
|
|
104
|
+
return content.substring(0, pos).split('\n').length;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OxcAnalyzer as CoreOxcAnalyzer } from '../ast/OxcAnalyzer.mjs';
|
|
2
|
+
|
|
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);
|
|
10
|
+
}
|
|
11
|
+
}
|