ghost-import-hunter 2.0.2 → 3.0.1
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 +30 -3
- package/dist/analyzer.js +110 -25
- package/dist/index.js +303 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,9 +102,36 @@ Add to your GitHub Actions or GitLab CI:
|
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
### Command Line Options
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
|
|
106
|
+
To see all available commands, you can view the help menu:
|
|
107
|
+
|
|
108
|
+
**Using npx:**
|
|
109
|
+
```bash
|
|
110
|
+
npx ghost-import-hunter --help
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Local Development:**
|
|
114
|
+
```bash
|
|
115
|
+
node dist/index.js --help
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Output:**
|
|
119
|
+
```text
|
|
120
|
+
Usage: ghost-import-hunter [options] [directory]
|
|
121
|
+
|
|
122
|
+
A deterministic tool to find AI hallucinations and unused code
|
|
123
|
+
|
|
124
|
+
Arguments:
|
|
125
|
+
directory Directory to scan (default: ".")
|
|
126
|
+
|
|
127
|
+
Options:
|
|
128
|
+
-V, --version output the version number
|
|
129
|
+
--fix Automatically fix unused imports
|
|
130
|
+
--interactive Interactively fix unused imports and hallucinations
|
|
131
|
+
--prune Uninstall completely unused dependencies from package.json
|
|
132
|
+
--uninstall-self Uninstall ghost-import-hunter globally from your system
|
|
133
|
+
-h, --help display help for command
|
|
134
|
+
```
|
|
108
135
|
|
|
109
136
|
---
|
|
110
137
|
|
package/dist/analyzer.js
CHANGED
|
@@ -38,7 +38,7 @@ const ts = __importStar(require("typescript"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const glob_1 = require("glob");
|
|
40
40
|
async function analyzeProject(directory) {
|
|
41
|
-
const report = { hallucinations: [], unused: [] };
|
|
41
|
+
const report = { hallucinations: [], unused: [], usedModules: [] };
|
|
42
42
|
// 1. Find all files in the project
|
|
43
43
|
const files = await (0, glob_1.glob)('**/*.{ts,tsx,js,jsx}', {
|
|
44
44
|
cwd: directory,
|
|
@@ -59,80 +59,165 @@ async function analyzeProject(directory) {
|
|
|
59
59
|
skipLibCheck: true
|
|
60
60
|
});
|
|
61
61
|
const checker = program.getTypeChecker();
|
|
62
|
+
const allUsedModules = new Set();
|
|
62
63
|
// 3. Analyze each file
|
|
63
64
|
for (const sourceFile of program.getSourceFiles()) {
|
|
64
65
|
// Skip external library files (node_modules)
|
|
65
66
|
if (sourceFile.fileName.includes('node_modules'))
|
|
66
67
|
continue;
|
|
67
68
|
// Also ensure we are only analyzing files we actually found (extra safety)
|
|
68
|
-
// Normalize paths for comparison
|
|
69
69
|
const normalizedFilePath = path.resolve(sourceFile.fileName);
|
|
70
70
|
const isProjectFile = files.some(f => path.resolve(f) === normalizedFilePath);
|
|
71
71
|
if (!isProjectFile)
|
|
72
72
|
continue;
|
|
73
|
+
// Track imports to find unused ones
|
|
74
|
+
// Map<Symbol, UnusedItem>
|
|
75
|
+
const trackedImports = new Map();
|
|
76
|
+
// Pass 1: Collect Imports & Check Hallucinations
|
|
73
77
|
ts.forEachChild(sourceFile, (node) => {
|
|
74
78
|
if (ts.isImportDeclaration(node)) {
|
|
75
|
-
visitImportDeclaration(node, sourceFile, checker, report);
|
|
79
|
+
visitImportDeclaration(node, sourceFile, checker, report, trackedImports, allUsedModules);
|
|
76
80
|
}
|
|
77
81
|
});
|
|
82
|
+
// Pass 2: Check Usage
|
|
83
|
+
// We visit all nodes EXCEPT ImportDeclarations (which we already processed)
|
|
84
|
+
// If we find an identifier that resolves to a symbol in 'trackedImports', delete it from the map.
|
|
85
|
+
const visitUsage = (node) => {
|
|
86
|
+
if (ts.isIdentifier(node)) {
|
|
87
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
88
|
+
if (symbol) {
|
|
89
|
+
// Check direct match
|
|
90
|
+
if (trackedImports.has(symbol)) {
|
|
91
|
+
trackedImports.delete(symbol);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Handle aliased symbols (e.g. import { a as b } ... b usage checks alias a)
|
|
95
|
+
// Actually, 'getSymbolAtLocation' on the usage 'b' returns the local symbol for 'b'.
|
|
96
|
+
// And that IS the key in our map.
|
|
97
|
+
// However, TypeScript sometimes handles equality strictly.
|
|
98
|
+
// What if it's a shorthand property? { b } -> uses b.
|
|
99
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
100
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
101
|
+
if (aliased && trackedImports.has(aliased)) {
|
|
102
|
+
trackedImports.delete(aliased);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Recurse, but don't enter ImportDeclarations again
|
|
109
|
+
if (!ts.isImportDeclaration(node)) {
|
|
110
|
+
ts.forEachChild(node, visitUsage);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
114
|
+
if (!ts.isImportDeclaration(node)) {
|
|
115
|
+
visitUsage(node);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// Any remaining imports are unused
|
|
119
|
+
trackedImports.forEach((unusedItem) => {
|
|
120
|
+
report.unused.push(unusedItem);
|
|
121
|
+
});
|
|
78
122
|
}
|
|
123
|
+
report.usedModules = Array.from(allUsedModules);
|
|
79
124
|
return report;
|
|
80
125
|
}
|
|
81
|
-
function visitImportDeclaration(node, sourceFile, checker, report) {
|
|
126
|
+
function visitImportDeclaration(node, sourceFile, checker, report, trackedImports, allUsedModules) {
|
|
82
127
|
const moduleSpecifier = node.moduleSpecifier;
|
|
83
128
|
if (!ts.isStringLiteral(moduleSpecifier))
|
|
84
129
|
return;
|
|
85
130
|
const moduleName = moduleSpecifier.text;
|
|
86
|
-
|
|
131
|
+
allUsedModules.add(moduleName);
|
|
132
|
+
// Resolve Module Symbol (Hallucination Check)
|
|
87
133
|
const symbol = checker.getSymbolAtLocation(moduleSpecifier);
|
|
88
134
|
if (!symbol) {
|
|
89
|
-
// Module not found at all
|
|
90
|
-
// Get line number
|
|
91
135
|
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
92
136
|
report.hallucinations.push({
|
|
93
137
|
file: sourceFile.fileName,
|
|
94
138
|
line: line + 1,
|
|
139
|
+
start: node.getStart(),
|
|
140
|
+
end: node.getEnd(),
|
|
95
141
|
module: moduleName,
|
|
96
142
|
member: '*' // Whole module missing
|
|
97
143
|
});
|
|
98
|
-
return;
|
|
144
|
+
return; // Can't check usage if module missing
|
|
145
|
+
}
|
|
146
|
+
// Default Import
|
|
147
|
+
if (node.importClause?.name) {
|
|
148
|
+
const defaultImport = node.importClause.name;
|
|
149
|
+
const defaultSymbol = checker.getSymbolAtLocation(defaultImport);
|
|
150
|
+
if (defaultSymbol) {
|
|
151
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(defaultImport.getStart());
|
|
152
|
+
trackedImports.set(defaultSymbol, {
|
|
153
|
+
file: sourceFile.fileName,
|
|
154
|
+
line: line + 1,
|
|
155
|
+
start: defaultImport.getStart(),
|
|
156
|
+
end: defaultImport.getEnd(),
|
|
157
|
+
module: moduleName,
|
|
158
|
+
member: 'default'
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Namespace Import (* as name)
|
|
163
|
+
if (node.importClause?.namedBindings && ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
164
|
+
const namespaceImport = node.importClause.namedBindings.name;
|
|
165
|
+
const nsSymbol = checker.getSymbolAtLocation(namespaceImport);
|
|
166
|
+
if (nsSymbol) {
|
|
167
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(namespaceImport.getStart());
|
|
168
|
+
trackedImports.set(nsSymbol, {
|
|
169
|
+
file: sourceFile.fileName,
|
|
170
|
+
line: line + 1,
|
|
171
|
+
start: namespaceImport.getStart(),
|
|
172
|
+
end: namespaceImport.getEnd(),
|
|
173
|
+
module: moduleName,
|
|
174
|
+
member: '* as ' + namespaceImport.text
|
|
175
|
+
});
|
|
176
|
+
}
|
|
99
177
|
}
|
|
100
|
-
//
|
|
101
|
-
if (node.importClause
|
|
178
|
+
// Named Imports
|
|
179
|
+
if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
|
|
102
180
|
node.importClause.namedBindings.elements.forEach(element => {
|
|
103
181
|
const importName = element.propertyName?.text || element.name.text;
|
|
104
|
-
const
|
|
182
|
+
const localName = element.name; // The identifier in the code
|
|
183
|
+
const importSymbol = checker.getSymbolAtLocation(localName);
|
|
105
184
|
const { line } = sourceFile.getLineAndCharacterOfPosition(element.getStart());
|
|
106
185
|
if (importSymbol) {
|
|
107
|
-
|
|
186
|
+
// Hallucination Check for Named Exports
|
|
187
|
+
let aliasedSymbol;
|
|
188
|
+
if (importSymbol.flags & ts.SymbolFlags.Alias) {
|
|
189
|
+
aliasedSymbol = checker.getAliasedSymbol(importSymbol);
|
|
190
|
+
}
|
|
108
191
|
if (aliasedSymbol) {
|
|
109
|
-
// Check for "unknown" symbol or missing declarations which indicates a hallucination
|
|
110
192
|
if (aliasedSymbol.escapedName === 'unknown' || !aliasedSymbol.declarations || aliasedSymbol.declarations.length === 0) {
|
|
111
193
|
report.hallucinations.push({
|
|
112
194
|
file: sourceFile.fileName,
|
|
113
195
|
line: line + 1,
|
|
196
|
+
start: element.getStart(),
|
|
197
|
+
end: element.getEnd(),
|
|
114
198
|
module: moduleName,
|
|
115
199
|
member: importName
|
|
116
200
|
});
|
|
201
|
+
return; // Don't track unused if it's a hallucination
|
|
117
202
|
}
|
|
118
203
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// }
|
|
129
|
-
}
|
|
204
|
+
// Add to tracked imports for usage check
|
|
205
|
+
trackedImports.set(importSymbol, {
|
|
206
|
+
file: sourceFile.fileName,
|
|
207
|
+
line: line + 1,
|
|
208
|
+
start: element.getStart(),
|
|
209
|
+
end: element.getEnd(),
|
|
210
|
+
module: moduleName,
|
|
211
|
+
member: importName
|
|
212
|
+
});
|
|
130
213
|
}
|
|
131
214
|
else {
|
|
132
|
-
//
|
|
215
|
+
// Hallucination
|
|
133
216
|
report.hallucinations.push({
|
|
134
217
|
file: sourceFile.fileName,
|
|
135
218
|
line: line + 1,
|
|
219
|
+
start: element.getStart(),
|
|
220
|
+
end: element.getEnd(),
|
|
136
221
|
module: moduleName,
|
|
137
222
|
member: importName
|
|
138
223
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
38
|
};
|
|
6
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
40
|
const commander_1 = require("commander");
|
|
8
41
|
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const ts = __importStar(require("typescript"));
|
|
45
|
+
const readline = __importStar(require("readline"));
|
|
9
46
|
const analyzer_1 = require("./analyzer");
|
|
10
47
|
const program = new commander_1.Command();
|
|
11
48
|
program
|
|
12
|
-
.name('ghost-hunter')
|
|
49
|
+
.name('ghost-import-hunter')
|
|
13
50
|
.description('A deterministic tool to find AI hallucinations and unused code')
|
|
14
|
-
.version('
|
|
51
|
+
.version('3.0.1')
|
|
15
52
|
.argument('[directory]', 'Directory to scan', '.')
|
|
16
|
-
.
|
|
17
|
-
|
|
53
|
+
.option('--fix', 'Automatically fix unused imports')
|
|
54
|
+
.option('--interactive', 'Interactively fix unused imports and hallucinations')
|
|
55
|
+
.option('--prune', 'Uninstall completely unused dependencies from package.json')
|
|
56
|
+
.option('--uninstall-self', 'Uninstall ghost-import-hunter globally from your system')
|
|
57
|
+
.action(async (directory, options) => {
|
|
58
|
+
if (options.uninstallSelf) {
|
|
59
|
+
console.log(chalk_1.default.red('\n⚠️ WARNING: This will completely remove ghost-import-hunter from your system.'));
|
|
60
|
+
const rl = readline.createInterface({
|
|
61
|
+
input: process.stdin,
|
|
62
|
+
output: process.stdout
|
|
63
|
+
});
|
|
64
|
+
const answer = await new Promise(resolve => {
|
|
65
|
+
rl.question(chalk_1.default.yellow(`❓ Are you sure you want to uninstall ghost-import-hunter? (y/N) `), resolve);
|
|
66
|
+
});
|
|
67
|
+
rl.close();
|
|
68
|
+
if (answer.toLowerCase() === 'y') {
|
|
69
|
+
console.log(chalk_1.default.blue('\n🗑️ Uninstalling ghost-import-hunter...'));
|
|
70
|
+
try {
|
|
71
|
+
const { execSync } = require('child_process');
|
|
72
|
+
execSync('npm uninstall -g ghost-import-hunter', { stdio: 'inherit' });
|
|
73
|
+
console.log(chalk_1.default.green('✨ Successfully uninstalled ghost-import-hunter. Goodbye! 👋'));
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(chalk_1.default.red('❌ Failed to uninstall:'), err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
console.log(chalk_1.default.green('ℹ️ Uninstall cancelled. Thank you for keeping me around! 👻'));
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk_1.default.blue(`👻 Ghost Import Hunter scanning: ${directory}...`));
|
|
18
85
|
try {
|
|
19
86
|
// New v2 Engine using TS Compiler API
|
|
20
87
|
const report = await (0, analyzer_1.analyzeProject)(directory);
|
|
88
|
+
let hasError = false;
|
|
21
89
|
if (report.hallucinations.length > 0) {
|
|
22
90
|
console.log(chalk_1.default.red('\n🚨 Hallucinations Detected (AI Lied!):'));
|
|
23
91
|
report.hallucinations.forEach(h => {
|
|
24
92
|
console.log(` - ${chalk_1.default.bold(h.module)}: Used member ${chalk_1.default.bold(h.member)} does not exist.`);
|
|
25
93
|
console.log(` File: ${h.file}:${h.line}`);
|
|
26
94
|
});
|
|
27
|
-
|
|
95
|
+
hasError = true;
|
|
28
96
|
}
|
|
29
97
|
else {
|
|
30
98
|
console.log(chalk_1.default.green('\n✅ No Hallucinations detected.'));
|
|
@@ -36,10 +104,240 @@ program
|
|
|
36
104
|
console.log(` File: ${u.file}:${u.line}`);
|
|
37
105
|
});
|
|
38
106
|
}
|
|
107
|
+
if (options.interactive) {
|
|
108
|
+
const fixes = [];
|
|
109
|
+
const allIssues = [...report.hallucinations, ...report.unused];
|
|
110
|
+
if (allIssues.length === 0) {
|
|
111
|
+
console.log(chalk_1.default.green('\n✨ No issues to fix!'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(chalk_1.default.blue(`\n🕵️ Interactive Mode: Found ${allIssues.length} issues.`));
|
|
115
|
+
const rl = readline.createInterface({
|
|
116
|
+
input: process.stdin,
|
|
117
|
+
output: process.stdout
|
|
118
|
+
});
|
|
119
|
+
for (const issue of allIssues) {
|
|
120
|
+
const isHallucination = 'member' in issue && report.hallucinations.includes(issue);
|
|
121
|
+
const type = isHallucination ? chalk_1.default.red('Hallucination') : chalk_1.default.yellow('Unused');
|
|
122
|
+
console.log(chalk_1.default.gray('--------------------------------------------------'));
|
|
123
|
+
console.log(`${type}: ${chalk_1.default.bold(issue.module)} (${issue.member})`);
|
|
124
|
+
console.log(` File: ${issue.file}:${issue.line}`);
|
|
125
|
+
const answer = await new Promise(resolve => {
|
|
126
|
+
rl.question(chalk_1.default.cyan(' Action? [d]elete, [s]kip (default: skip): '), resolve);
|
|
127
|
+
});
|
|
128
|
+
if (answer.toLowerCase() === 'd') {
|
|
129
|
+
fixes.push(issue);
|
|
130
|
+
console.log(chalk_1.default.green(' -> Marked for deletion.'));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(chalk_1.default.gray(' -> Skipped.'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
rl.close();
|
|
137
|
+
if (fixes.length > 0) {
|
|
138
|
+
console.log(chalk_1.default.blue(`\n🔧 Applying ${fixes.length} fixes...`));
|
|
139
|
+
await fixImports(fixes);
|
|
140
|
+
console.log(chalk_1.default.green('✨ Fixes applied!'));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(chalk_1.default.yellow('\nℹ️ No changes made.'));
|
|
144
|
+
}
|
|
145
|
+
// Skip the batch block below if we ran interactive
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (options.fix && report.unused.length > 0) {
|
|
149
|
+
const rl = readline.createInterface({
|
|
150
|
+
input: process.stdin,
|
|
151
|
+
output: process.stdout
|
|
152
|
+
});
|
|
153
|
+
const answer = await new Promise(resolve => {
|
|
154
|
+
rl.question(chalk_1.default.yellow(`\n❓ Found ${report.unused.length} unused imports. Do you want to fix them? (y/N) `), resolve);
|
|
155
|
+
});
|
|
156
|
+
rl.close();
|
|
157
|
+
if (answer.toLowerCase() === 'y') {
|
|
158
|
+
console.log(chalk_1.default.blue('\n🔧 Fixing unused imports...'));
|
|
159
|
+
await fixImports(report.unused);
|
|
160
|
+
console.log(chalk_1.default.green('✨ Auto-fix complete!'));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(chalk_1.default.yellow('ℹ️ Auto-fix cancelled.'));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (options.prune) {
|
|
167
|
+
const packageJsonPath = path.join(directory, 'package.json');
|
|
168
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
169
|
+
console.log(chalk_1.default.blue('\n🔍 Checking for completely unused dependencies...'));
|
|
170
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
171
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
172
|
+
// Filter out types and our own tool just in case
|
|
173
|
+
const projectDeps = deps.filter(d => !d.startsWith('@types/') && d !== 'ghost-import-hunter');
|
|
174
|
+
const toRemove = projectDeps.filter(dep => !report.usedModules.includes(dep));
|
|
175
|
+
if (toRemove.length > 0) {
|
|
176
|
+
console.log(chalk_1.default.yellow(`\n🗑️ Found ${toRemove.length} unused dependencies:`));
|
|
177
|
+
toRemove.forEach(d => console.log(` - ${chalk_1.default.bold(d)}`));
|
|
178
|
+
const rl = readline.createInterface({
|
|
179
|
+
input: process.stdin,
|
|
180
|
+
output: process.stdout
|
|
181
|
+
});
|
|
182
|
+
const answer = await new Promise(resolve => {
|
|
183
|
+
rl.question(chalk_1.default.red(`\n❓ Are you sure you want to uninstall these packages? (y/N) `), resolve);
|
|
184
|
+
});
|
|
185
|
+
rl.close();
|
|
186
|
+
if (answer.toLowerCase() === 'y') {
|
|
187
|
+
console.log(chalk_1.default.blue(`\n📦 Uninstalling ${toRemove.join(', ')}...`));
|
|
188
|
+
try {
|
|
189
|
+
const { execSync } = require('child_process');
|
|
190
|
+
execSync(`npm uninstall ${toRemove.join(' ')}`, { stdio: 'inherit', cwd: directory });
|
|
191
|
+
console.log(chalk_1.default.green('✨ Pruning complete!'));
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
console.error(chalk_1.default.red('❌ Failed to uninstall packages:'), err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(chalk_1.default.yellow('ℹ️ Pruning cancelled.'));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log(chalk_1.default.green('\n✨ No unused dependencies found in package.json!'));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
console.log(chalk_1.default.yellow('\n⚠️ No package.json found in the specified directory. Cannot prune dependencies.'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (hasError) {
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
39
212
|
}
|
|
40
213
|
catch (error) {
|
|
41
214
|
console.error(chalk_1.default.red('Error scanning project:'), error);
|
|
42
215
|
process.exit(1);
|
|
43
216
|
}
|
|
44
217
|
});
|
|
218
|
+
async function fixImports(unused) {
|
|
219
|
+
// Group by file
|
|
220
|
+
const fileMap = new Map();
|
|
221
|
+
unused.forEach(u => {
|
|
222
|
+
if (!fileMap.has(u.file))
|
|
223
|
+
fileMap.set(u.file, []);
|
|
224
|
+
fileMap.get(u.file).push(u);
|
|
225
|
+
});
|
|
226
|
+
for (const [file, items] of fileMap) {
|
|
227
|
+
if (!fs.existsSync(file))
|
|
228
|
+
continue;
|
|
229
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
230
|
+
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
231
|
+
const replacements = [];
|
|
232
|
+
// Helper: Check usage
|
|
233
|
+
// usage is defined by matching line and member
|
|
234
|
+
// items contains the UNUSED ones.
|
|
235
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
236
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
237
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
238
|
+
const lineNum = line + 1;
|
|
239
|
+
const moduleName = node.moduleSpecifier.text;
|
|
240
|
+
// Find unused items for this import declaration
|
|
241
|
+
// (Note: duplicate logic with analyzer?) a bit, but we are fixing now.
|
|
242
|
+
// We assume strict line match is safe enough.
|
|
243
|
+
const unusedItems = items.filter(u => u.line === lineNum && u.module === moduleName);
|
|
244
|
+
if (unusedItems.length === 0)
|
|
245
|
+
return;
|
|
246
|
+
// Determine what to remove
|
|
247
|
+
let shouldRemoveWhole = false;
|
|
248
|
+
const importClause = node.importClause;
|
|
249
|
+
if (!importClause) {
|
|
250
|
+
// Import "mod"; -> side effect.
|
|
251
|
+
// If analyzer reported it as unused, it means it's unused. (Wait, analyzer skips side effects? No, visitImportDeclaration checks string literal.)
|
|
252
|
+
// But our Unused logic tracks SYMBOLS. Side effect imports usually don't introduce symbols.
|
|
253
|
+
// If analyzer reported it, we should remove it.
|
|
254
|
+
// Check if member is '*'?
|
|
255
|
+
if (unusedItems.some(u => u.member === '*')) {
|
|
256
|
+
shouldRemoveWhole = true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Check Default
|
|
261
|
+
let removeDefault = false;
|
|
262
|
+
if (importClause.name) {
|
|
263
|
+
// Check if default is unused
|
|
264
|
+
if (unusedItems.some(u => u.member === 'default')) {
|
|
265
|
+
removeDefault = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Check Namespace
|
|
269
|
+
let removeNamespace = false;
|
|
270
|
+
if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings)) {
|
|
271
|
+
const txt = '* as ' + importClause.namedBindings.name.text;
|
|
272
|
+
if (unusedItems.some(u => u.member === txt)) {
|
|
273
|
+
removeNamespace = true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Check Named
|
|
277
|
+
let newNamedElements = [];
|
|
278
|
+
let hasNamed = false;
|
|
279
|
+
if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
|
|
280
|
+
hasNamed = true;
|
|
281
|
+
importClause.namedBindings.elements.forEach(el => {
|
|
282
|
+
const name = el.propertyName?.text || el.name.text;
|
|
283
|
+
// Is this specific named import unused?
|
|
284
|
+
const isUnused = unusedItems.some(u => u.member === name);
|
|
285
|
+
if (!isUnused) {
|
|
286
|
+
newNamedElements.push(el);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// Decision Logic
|
|
291
|
+
const keptDefault = importClause.name && !removeDefault;
|
|
292
|
+
const keptNamespace = importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings) && !removeNamespace;
|
|
293
|
+
const keptNamed = hasNamed && newNamedElements.length > 0;
|
|
294
|
+
// If nothing kept, remove whole
|
|
295
|
+
if (!keptDefault && !keptNamespace && !keptNamed) {
|
|
296
|
+
shouldRemoveWhole = true;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// Reconstruct Import
|
|
300
|
+
// We use ts.factory to create a new node
|
|
301
|
+
// But we need to handle "Default, Named" vs "Default" vs "Named"
|
|
302
|
+
const newImportClause = ts.factory.updateImportClause(importClause, importClause.isTypeOnly, keptDefault ? importClause.name : undefined, keptNamespace
|
|
303
|
+
? importClause.namedBindings
|
|
304
|
+
// Mixed default + named?
|
|
305
|
+
: (keptNamed
|
|
306
|
+
? ts.factory.createNamedImports(newNamedElements)
|
|
307
|
+
: undefined));
|
|
308
|
+
// Create new declaration
|
|
309
|
+
const newDecl = ts.factory.updateImportDeclaration(node, node.modifiers, newImportClause, node.moduleSpecifier, node.assertClause);
|
|
310
|
+
// Print
|
|
311
|
+
const printer = ts.createPrinter();
|
|
312
|
+
const newText = printer.printNode(ts.EmitHint.Unspecified, newDecl, sourceFile);
|
|
313
|
+
replacements.push({
|
|
314
|
+
start: node.getStart(),
|
|
315
|
+
end: node.getEnd(),
|
|
316
|
+
text: newText
|
|
317
|
+
});
|
|
318
|
+
return; // Done with this node
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (shouldRemoveWhole) {
|
|
322
|
+
// Remove the whole line(s) to avoid empty gaps
|
|
323
|
+
// node.getFullStart() includes previous newlines?
|
|
324
|
+
// Safe approach: remove node.getStart() to node.getEnd(), then cleanup empty lines later or just leave usage of prettier to user?
|
|
325
|
+
// "getFullStart" keeps leading trivia.
|
|
326
|
+
replacements.push({
|
|
327
|
+
start: node.getFullStart(),
|
|
328
|
+
end: node.getEnd(),
|
|
329
|
+
text: '' // Delete
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Apply replacements from bottom to top
|
|
335
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
336
|
+
let newContent = content;
|
|
337
|
+
for (const r of replacements) {
|
|
338
|
+
newContent = newContent.substring(0, r.start) + r.text + newContent.substring(r.end);
|
|
339
|
+
}
|
|
340
|
+
fs.writeFileSync(file, newContent);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
45
343
|
program.parse();
|
package/package.json
CHANGED