entkapp 4.5.0 ā 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +4 -4
- package/docs.zip +0 -0
- 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 +467 -253
- package/src/ast/AdvancedAnalysis.js +6 -5
- package/src/ast/BarrelParser.js +23 -16
- package/src/ast/DeadCodeDetector.js +30 -18
- package/src/ast/MagicDetector.js +1 -1
- package/src/ast/OxcAnalyzer.js +328 -264
- package/src/index.js +272 -361
- 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 +27 -66
- package/src/resolution/ConfigLoader.js +2 -85
- package/src/resolution/DepencyResolver.js +20 -124
- package/src/resolution/EntryPointDetector.js +134 -0
- package/src/resolution/PathMapper.js +3 -123
- package/src/resolution/TSConfigLoader.js +76 -0
- package/src/resolution/WorkSpaceGraph.js +4 -473
- package/src/resolution/WorkspaceDiagnostic.js +3 -57
- package/src/plugins/KnipAdapter.js +0 -106
package/bin/cli.js
CHANGED
|
@@ -27,8 +27,8 @@ async function bootstrap() {
|
|
|
27
27
|
|
|
28
28
|
program
|
|
29
29
|
.name('entkapp')
|
|
30
|
-
.description(ansis.cyan('Enterprise
|
|
31
|
-
.version(packageJsonContent.version || '
|
|
30
|
+
.description(ansis.cyan('The Ultimate Enterprise Codebase Janitor with OXC integration, type-aware analysis, and automated structural healing.'))
|
|
31
|
+
.version(packageJsonContent.version || '5.0.0');
|
|
32
32
|
|
|
33
33
|
program
|
|
34
34
|
.option('-c, --cwd <path>', 'Specify the execution context root directory', process.cwd())
|
|
@@ -85,7 +85,7 @@ async function bootstrap() {
|
|
|
85
85
|
interface: "CLI",
|
|
86
86
|
useBuiltinPlugins: true,
|
|
87
87
|
useCustomPlugins: true,
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
options: { verbose: false, fastMode: true, selfHealing: true },
|
|
90
90
|
enabledPlugins: ["nextjs", "nuxt", "remix", "sveltekit", "astro"]
|
|
91
91
|
};
|
|
@@ -132,7 +132,7 @@ async function bootstrap() {
|
|
|
132
132
|
}, timeoutMs);
|
|
133
133
|
timeoutTimer.unref(); // Allow process to exit if work finishes
|
|
134
134
|
|
|
135
|
-
console.log(ansis.bold.green(`\nš¦ entkapp v${packageJsonContent.version || '
|
|
135
|
+
console.log(ansis.bold.green(`\nš¦ entkapp v${packageJsonContent.version || '5.0.0'} Engine Activation`));
|
|
136
136
|
console.log(ansis.dim('------------------------------------------------------------'));
|
|
137
137
|
console.log(`${ansis.bold('Target Workspace Root :')} ${ansis.blue(targetCwd)}`);
|
|
138
138
|
console.log(`${ansis.bold('Refactoring Mode :')} ${options.fix ? ansis.yellow('Active Fixing & Self-Healing Enabled') : ansis.gray('Dry-Run Reporting Only')}`);
|
package/docs.zip
ADDED
|
Binary file
|
package/entkapp/config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"name": "entkapp",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.0",
|
|
5
5
|
"description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
@@ -77,16 +77,15 @@
|
|
|
77
77
|
},
|
|
78
78
|
"homepage": "https://dreamlongyt.github.io/entkapp/",
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"ansis": "^3.
|
|
81
|
-
"commander": "^12.
|
|
82
|
-
"enhanced-resolve": "^5.
|
|
80
|
+
"ansis": "^3.17.0",
|
|
81
|
+
"commander": "^12.1.0",
|
|
82
|
+
"enhanced-resolve": "^5.24.0",
|
|
83
83
|
"execa": "^8.0.1",
|
|
84
84
|
"oxc-parser": "^0.135.0",
|
|
85
|
-
"typescript": "^5.
|
|
85
|
+
"typescript": "^5.9.3"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@types/node": "^25.9.3",
|
|
89
|
-
"knip": "^6.17.1",
|
|
90
89
|
"vitepress": "^1.6.4",
|
|
91
90
|
"vue": ">=3.5.0"
|
|
92
91
|
},
|
package/src/EngineContext.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import fsSync from 'fs';
|
|
1
2
|
import path from 'path';
|
|
3
|
+
import ansis from 'ansis';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Core In-Memory Graph Node Representation.
|
|
@@ -36,76 +38,62 @@ export class GraphNode {
|
|
|
36
38
|
if (visited.has(visitKey)) return false;
|
|
37
39
|
visited.add(visitKey);
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// --- NEW: Self-reference check (Fix for SecretSeverity bug) ---
|
|
42
|
-
// If the symbol is used within the same file, it is NOT dead.
|
|
41
|
+
// Self-reference check
|
|
43
42
|
if (this.instantiatedIdentifiers.has(symbolName)) return true;
|
|
44
|
-
|
|
45
|
-
// Check property access in the same file
|
|
46
43
|
for (const accessChain of this.propertyAccessChains) {
|
|
47
|
-
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`))
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
44
|
+
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) return true;
|
|
50
45
|
}
|
|
51
|
-
// --------------------------------------------------------------
|
|
52
46
|
|
|
53
47
|
for (const parentPath of this.incomingEdges) {
|
|
54
48
|
const parentNode = projectGraph.get(parentPath);
|
|
55
49
|
if (!parentNode) continue;
|
|
56
50
|
|
|
57
|
-
//
|
|
51
|
+
// Direct Import Tokens
|
|
58
52
|
const absoluteImportKey = `${this.filePath}:${symbolName}`;
|
|
59
53
|
const absoluteStarKey = `${this.filePath}:*`;
|
|
54
|
+
if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) return true;
|
|
60
55
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const importKey = `${relativePath}:${symbolName}`;
|
|
70
|
-
const importKeyAlt = `${relativePathNoExt}:${symbolName}`;
|
|
71
|
-
const starKey = `${relativePath}:*`;
|
|
72
|
-
const starKeyAlt = `${relativePathNoExt}:*`;
|
|
73
|
-
|
|
74
|
-
if (parentNode.importedSymbols.has(importKey) || parentNode.importedSymbols.has(importKeyAlt) ||
|
|
75
|
-
parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
|
|
76
|
-
return true;
|
|
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;
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
//
|
|
77
|
+
// Recursive Re-export Resolution
|
|
80
78
|
for (const [exportedName, exportMeta] of parentNode.internalExports.entries()) {
|
|
81
|
-
const isMatch = (exportMeta.type === 're-export' || exportMeta.type === 're-export-namespace') &&
|
|
82
|
-
(exportMeta.originalName === symbolName || exportMeta.originalName === '*') &&
|
|
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') &&
|
|
83
81
|
(exportMeta.source === this.filePath || (exportMeta.source && path.resolve(path.dirname(parentPath), exportMeta.source) === this.filePath));
|
|
84
82
|
|
|
85
83
|
if (isMatch) {
|
|
86
|
-
|
|
87
|
-
if (parentNode.isSymbolReferencedExternally(exportedName, projectGraph, visited)) {
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
84
|
+
if (parentNode.isSymbolReferencedExternally(exportedName, projectGraph, visited)) return true;
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
87
|
|
|
88
|
+
// Fuzzy / Dynamic usage (Identifiers, Strings, JSX, Decorators)
|
|
93
89
|
if (parentNode.instantiatedIdentifiers.has(symbolName)) return true;
|
|
94
|
-
|
|
95
|
-
for (const accessChain of parentNode.propertyAccessChains) {
|
|
96
|
-
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
90
|
if (parentNode.rawStringReferences.has(symbolName)) return true;
|
|
102
91
|
if (parentNode.jsxComponents.has(symbolName)) return true;
|
|
103
|
-
|
|
104
|
-
for (const jsxProp of parentNode.jsxProps) {
|
|
105
|
-
if (jsxProp.endsWith(`:${symbolName}`)) return true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
92
|
if (parentNode.decorators.has(symbolName)) return true;
|
|
93
|
+
|
|
94
|
+
for (const accessChain of parentNode.propertyAccessChains) {
|
|
95
|
+
if (accessChain.endsWith(`.${symbolName}`) || accessChain.includes(`.${symbolName}.`)) return true;
|
|
96
|
+
}
|
|
109
97
|
}
|
|
110
98
|
|
|
111
99
|
return false;
|
|
@@ -114,14 +102,13 @@ export class GraphNode {
|
|
|
114
102
|
|
|
115
103
|
export class EngineContext {
|
|
116
104
|
constructor(cwd) {
|
|
117
|
-
// FIX: Ensure cwd is always defined, fallback to process.cwd()
|
|
118
105
|
this.cwd = cwd || process.cwd();
|
|
119
106
|
this.projectGraph = new Map(); // Path -> GraphNode
|
|
120
107
|
this.usedExternalPackages = new Set();
|
|
121
|
-
this.unimportedUsedPackages = new Set();
|
|
122
|
-
this.importedUnusedPackages = new Set();
|
|
123
|
-
this.unusedBinaries = new Set();
|
|
124
|
-
this.manifestDependencies = new Map();
|
|
108
|
+
this.unimportedUsedPackages = new Set();
|
|
109
|
+
this.importedUnusedPackages = new Set();
|
|
110
|
+
this.unusedBinaries = new Set();
|
|
111
|
+
this.manifestDependencies = new Map();
|
|
125
112
|
|
|
126
113
|
this.isWorkspaceEnabled = false;
|
|
127
114
|
this.monorepoPackageRoots = new Set();
|
|
@@ -137,62 +124,273 @@ export class EngineContext {
|
|
|
137
124
|
return this.projectGraph.get(filePath);
|
|
138
125
|
}
|
|
139
126
|
|
|
140
|
-
generateSummaryReport() {
|
|
127
|
+
async generateSummaryReport() {
|
|
141
128
|
const report = {
|
|
142
129
|
orphanedFiles: [],
|
|
143
130
|
deadExports: [],
|
|
144
131
|
unusedDependencies: [],
|
|
145
|
-
unimportedUsedPackages: [],
|
|
146
|
-
importedUnusedPackages: [],
|
|
147
|
-
unusedBinaries: []
|
|
132
|
+
unimportedUsedPackages: [],
|
|
133
|
+
importedUnusedPackages: [],
|
|
134
|
+
unusedBinaries: []
|
|
148
135
|
};
|
|
149
136
|
|
|
150
|
-
//
|
|
137
|
+
// --- DEEP REACHABILITY ANALYSIS (BFS) ---
|
|
138
|
+
const reachableFiles = new Set();
|
|
139
|
+
const queue = [];
|
|
140
|
+
|
|
141
|
+
// 1. Initialize BFS with Entry Points
|
|
142
|
+
for (const [filePath, node] of this.projectGraph.entries()) {
|
|
143
|
+
// FIX: Also check if the file is explicitly mentioned in package.json bin or main
|
|
144
|
+
if (node.isEntry || node.isLibraryEntry || node.isFrameworkComponent) {
|
|
145
|
+
reachableFiles.add(filePath);
|
|
146
|
+
queue.push(filePath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// UPGRADE: Ensure package.json main and bin are always entry points
|
|
151
|
+
// UPGRADE: Comprehensive Entry Point Discovery (Manifests + Tooling)
|
|
152
|
+
for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
|
|
153
|
+
try {
|
|
154
|
+
const data = JSON.parse(fsSync.readFileSync(manifestPath, 'utf8'));
|
|
155
|
+
const pkgDir = path.dirname(manifestPath);
|
|
156
|
+
const entries = [];
|
|
157
|
+
|
|
158
|
+
// 1. Standard Manifest Entries
|
|
159
|
+
if (data.main) entries.push(path.resolve(pkgDir, data.main));
|
|
160
|
+
if (data.module) entries.push(path.resolve(pkgDir, data.module));
|
|
161
|
+
if (data.exports) {
|
|
162
|
+
const unwind = (val) => {
|
|
163
|
+
if (typeof val === 'string') entries.push(path.resolve(pkgDir, val));
|
|
164
|
+
else if (typeof val === 'object' && val !== null) Object.values(val).forEach(unwind);
|
|
165
|
+
};
|
|
166
|
+
unwind(data.exports);
|
|
167
|
+
}
|
|
168
|
+
if (data.bin) {
|
|
169
|
+
if (typeof data.bin === 'string') entries.push(path.resolve(pkgDir, data.bin));
|
|
170
|
+
else Object.values(data.bin).forEach(b => entries.push(path.resolve(pkgDir, b)));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. Protect Documentation & Configs
|
|
174
|
+
const possibleConfigs = ['vite.config.js', 'vite.config.ts', 'vitest.config.ts', 'tsconfig.json'];
|
|
175
|
+
possibleConfigs.forEach(c => entries.push(path.resolve(pkgDir, c)));
|
|
176
|
+
|
|
177
|
+
entries.forEach(e => {
|
|
178
|
+
const normalized = e.replace(/\\/g, '/');
|
|
179
|
+
// Check with common extensions if not found
|
|
180
|
+
const candidates = [normalized, normalized + '.js', normalized + '.ts', normalized + '.tsx', normalized + '/index.js', normalized + '/index.ts'];
|
|
181
|
+
for (const cand of candidates) {
|
|
182
|
+
if (this.projectGraph.has(cand) && !reachableFiles.has(cand)) {
|
|
183
|
+
reachableFiles.add(cand);
|
|
184
|
+
queue.push(cand);
|
|
185
|
+
this.projectGraph.get(cand).isEntry = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// --- PLUGIN-BASED ECOSYSTEM DETECTION ---
|
|
192
|
+
// Plugins will now handle their own erreichbarkeit and dependency validation.
|
|
193
|
+
if (this.pluginRegistry) {
|
|
194
|
+
const activePlugins = await this.pluginRegistry.getActivePlugins(pkgDir);
|
|
195
|
+
for (const plugin of activePlugins) {
|
|
196
|
+
if (typeof plugin.onDiscovery === 'function') {
|
|
197
|
+
await plugin.onDiscovery({
|
|
198
|
+
pkgDir,
|
|
199
|
+
data,
|
|
200
|
+
reachableFiles,
|
|
201
|
+
queue,
|
|
202
|
+
projectGraph: this.projectGraph,
|
|
203
|
+
context: this
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. BFS Traversal
|
|
212
|
+
while (queue.length > 0) {
|
|
213
|
+
const currentPath = queue.shift();
|
|
214
|
+
const node = this.projectGraph.get(currentPath);
|
|
215
|
+
|
|
216
|
+
if (!node) continue;
|
|
217
|
+
|
|
218
|
+
// Track outgoing edges (static and linked dynamic imports)
|
|
219
|
+
if (node.outgoingEdges) {
|
|
220
|
+
for (const outgoingPath of node.outgoingEdges) {
|
|
221
|
+
const normalizedPath = outgoingPath.replace(/\\/g, '/');
|
|
222
|
+
if (!reachableFiles.has(normalizedPath)) {
|
|
223
|
+
reachableFiles.add(normalizedPath);
|
|
224
|
+
queue.push(normalizedPath);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// UPGRADE: Handle non-literal dynamic imports (calculatedDynamicImports)
|
|
230
|
+
if ((node.calculatedDynamicImports && node.calculatedDynamicImports.length > 0) || (node.dynamicImports && node.dynamicImports.has('__DYNAMIC_PATTERN__'))) {
|
|
231
|
+
const dir = path.dirname(currentPath);
|
|
232
|
+
// Conservative: If we have a calculated dynamic import, any file in the same or sub directory could be a target
|
|
233
|
+
for (const [otherPath, _] of this.projectGraph.entries()) {
|
|
234
|
+
// Fix: Check if otherPath is in the same directory or a subdirectory of 'dir'
|
|
235
|
+
if (otherPath !== currentPath && otherPath.startsWith(dir) && !reachableFiles.has(otherPath)) {
|
|
236
|
+
// Additional check: If it's in a 'plugins' or 'modules' folder, it's very likely a dynamic target
|
|
237
|
+
const isLikelyDynamicTarget = otherPath.includes('/plugins/') || otherPath.includes('/modules/') || otherPath.includes('/dynamic/');
|
|
238
|
+
if (isLikelyDynamicTarget) {
|
|
239
|
+
reachableFiles.add(otherPath);
|
|
240
|
+
queue.push(otherPath);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// UPGRADE: Handle Worker/Threads usage (new Worker('path'))
|
|
247
|
+
for (const chain of node.propertyAccessChains) {
|
|
248
|
+
if (chain.includes('new Worker(') || chain.includes('Worker(')) {
|
|
249
|
+
// Check if any file path is mentioned in raw strings
|
|
250
|
+
for (const str of node.rawStringReferences) {
|
|
251
|
+
if (str.startsWith('.') || str.startsWith('/')) {
|
|
252
|
+
try {
|
|
253
|
+
const resolved = path.resolve(path.dirname(currentPath), str);
|
|
254
|
+
const extensions = ['.js', '.ts', '.mjs', '.cjs'];
|
|
255
|
+
for (const ext of extensions) {
|
|
256
|
+
const p = resolved.endsWith(ext) ? resolved : resolved + ext;
|
|
257
|
+
const normalized = p.replace(/\\/g, '/');
|
|
258
|
+
if (this.projectGraph.has(normalized) && !reachableFiles.has(normalized)) {
|
|
259
|
+
reachableFiles.add(normalized);
|
|
260
|
+
queue.push(normalized);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Additional check for unlinked dynamic imports (conservative)
|
|
270
|
+
if (node.dynamicImports) {
|
|
271
|
+
for (const dynamicPath of node.dynamicImports) {
|
|
272
|
+
if (dynamicPath.startsWith('__DYNAMIC_PATTERN__:')) {
|
|
273
|
+
const pattern = dynamicPath.split(':')[1];
|
|
274
|
+
const dir = path.dirname(currentPath);
|
|
275
|
+
for (const [otherPath, _] of this.projectGraph.entries()) {
|
|
276
|
+
if (otherPath.startsWith(dir) && !reachableFiles.has(otherPath)) {
|
|
277
|
+
reachableFiles.add(otherPath);
|
|
278
|
+
queue.push(otherPath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --- ANALYSIS PHASE ---
|
|
287
|
+
if (this.verbose) {
|
|
288
|
+
console.log(`[Reachability] Total reachable files: ${reachableFiles.size}`);
|
|
289
|
+
for (const f of reachableFiles) console.log(` ⢠Reachable: ${path.relative(this.cwd, f)}`);
|
|
290
|
+
}
|
|
151
291
|
for (const [filePath, node] of this.projectGraph.entries()) {
|
|
152
292
|
const isSuppressed = node.localSuppressedRules.has('*') || node.localSuppressedRules.has('unused-file');
|
|
293
|
+
const isReachable = reachableFiles.has(filePath);
|
|
153
294
|
|
|
154
|
-
|
|
155
|
-
|
|
295
|
+
// Check for Orphaned Files
|
|
296
|
+
if (!isReachable && !isSuppressed) {
|
|
297
|
+
const relPath = path.relative(this.cwd, filePath).replace(/\\/g, '/');
|
|
156
298
|
report.orphanedFiles.push(relPath);
|
|
157
|
-
if (this.verbose) {
|
|
158
|
-
console.log(`[Orphan Auditor] File marked as orphan: ${relPath}`);
|
|
159
|
-
console.log(` - Incoming Edges: ${node.incomingEdges.size}`);
|
|
160
|
-
console.log(` - isEntry: ${node.isEntry}, isLibraryEntry: ${node.isLibraryEntry}, isFrameworkComponent: ${node.isFrameworkComponent}`);
|
|
161
|
-
console.log(` - Suppressed: ${isSuppressed}`);
|
|
162
|
-
}
|
|
163
299
|
}
|
|
164
300
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
301
|
+
// Check for Dead Exports
|
|
302
|
+
if (isReachable) {
|
|
303
|
+
const isPublicAPI = node.isEntry || node.isLibraryEntry;
|
|
304
|
+
|
|
305
|
+
for (const [symbol, meta] of node.internalExports.entries()) {
|
|
306
|
+
if (symbol === '*' || symbol === 'default' || node.localSuppressedRules.has('unused-export') || node.localSuppressedRules.has(`unused-export:${symbol}`)) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const isSymbolUsed = isPublicAPI || node.isSymbolReferencedExternally(symbol, this.projectGraph);
|
|
311
|
+
|
|
312
|
+
if (!isSymbolUsed) {
|
|
313
|
+
const loc = node.symbolSourceLocations.get(symbol) || { line: 0, column: 0 };
|
|
314
|
+
report.deadExports.push({
|
|
315
|
+
symbol,
|
|
316
|
+
file: path.relative(this.cwd, filePath),
|
|
317
|
+
line: loc.line
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Member Analysis
|
|
322
|
+
if (meta.members && meta.members.length > 0 && isSymbolUsed) {
|
|
323
|
+
for (const member of meta.members) {
|
|
324
|
+
const fullMemberName = `${symbol}.${member.name}`;
|
|
325
|
+
|
|
326
|
+
if (isPublicAPI && member.isPublic !== false) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// UPGRADE: Check for dynamic usage in ANY reachable file, not just direct imports
|
|
331
|
+
let isMemberUsed = false;
|
|
332
|
+
for (const [otherPath, otherNode] of this.projectGraph.entries()) {
|
|
333
|
+
if (reachableFiles.has(otherPath)) {
|
|
334
|
+
if (otherNode.instantiatedIdentifiers.has(member.name) ||
|
|
335
|
+
otherNode.propertyAccessChains.has(fullMemberName) ||
|
|
336
|
+
otherNode.rawStringReferences.has(member.name)) {
|
|
337
|
+
isMemberUsed = true;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!isMemberUsed && !node.isSymbolReferencedExternally(fullMemberName, this.projectGraph)) {
|
|
344
|
+
const loc = node.symbolSourceLocations.get(fullMemberName) || { line: 0, column: 0 };
|
|
345
|
+
report.deadExports.push({
|
|
346
|
+
symbol: fullMemberName,
|
|
347
|
+
file: path.relative(this.cwd, filePath),
|
|
348
|
+
line: loc.line
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
168
353
|
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
169
356
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
357
|
+
// --- DEPENDENCY ANALYSIS ---
|
|
358
|
+
const usedByReachableFiles = new Set();
|
|
359
|
+
reachableFiles.forEach(filePath => {
|
|
360
|
+
const node = this.projectGraph.get(filePath);
|
|
361
|
+
if (node) {
|
|
362
|
+
node.externalPackageUsage.forEach(pkg => usedByReachableFiles.add(pkg));
|
|
363
|
+
|
|
364
|
+
// Conservative check for dynamic imports in reachable files
|
|
365
|
+
if (node.calculatedDynamicImports) {
|
|
366
|
+
node.calculatedDynamicImports.forEach(entry => {
|
|
367
|
+
const expr = typeof entry === 'string' ? entry : (entry.pattern || entry.text || '');
|
|
368
|
+
// If it looks like a package name (no dots/slashes), assume it's used
|
|
369
|
+
if (expr && !expr.includes('.') && !expr.includes('/') && !expr.includes('`') && !expr.includes("'") && !expr.includes('"')) {
|
|
370
|
+
usedByReachableFiles.add(expr);
|
|
371
|
+
}
|
|
372
|
+
// Check raw strings for potential package names
|
|
373
|
+
node.rawStringReferences.forEach(str => {
|
|
374
|
+
if (!str.startsWith('.') && !str.startsWith('/')) {
|
|
375
|
+
const pkg = str.startsWith('@') ? str.split('/').slice(0, 2).join('/') : str.split('/')[0];
|
|
376
|
+
usedByReachableFiles.add(pkg);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
176
379
|
});
|
|
177
380
|
}
|
|
178
381
|
}
|
|
179
|
-
}
|
|
382
|
+
});
|
|
180
383
|
|
|
181
|
-
// 2. Dependency Analysis (Extended)
|
|
182
|
-
// Compare manifest dependencies with actually used packages
|
|
183
384
|
for (const [manifestPath, deps] of this.manifestDependencies.entries()) {
|
|
184
385
|
const allDeps = [...(deps.dependencies || []), ...(deps.devDependencies || [])];
|
|
185
|
-
|
|
186
386
|
const hasTypeScriptFiles = Array.from(this.projectGraph.keys()).some(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
187
387
|
|
|
188
388
|
for (const dep of allDeps) {
|
|
189
|
-
// Skip @types packages and known safe packages
|
|
190
389
|
if (dep.startsWith('@types/') || dep === 'entkapp' || (dep === 'typescript' && hasTypeScriptFiles)) {
|
|
191
390
|
continue;
|
|
192
391
|
}
|
|
193
392
|
|
|
194
|
-
|
|
195
|
-
if (!this.usedExternalPackages.has(dep)) {
|
|
393
|
+
if (!usedByReachableFiles.has(dep)) {
|
|
196
394
|
report.unusedDependencies.push({
|
|
197
395
|
package: dep,
|
|
198
396
|
type: deps.dependencies.includes(dep) ? 'dependency' : 'devDependency',
|