ghost-import-hunter 2.0.2 → 3.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/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
- - `--help` - Show all commands
106
- - `--json` - Output as JSON
107
- - `--ignore` - Ignore patterns (glob)
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
- // Resolve Module Symbol
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
- // Check Named Imports
101
- if (node.importClause && node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
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 importSymbol = checker.getSymbolAtLocation(element.name);
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
- const aliasedSymbol = checker.getAliasedSymbol(importSymbol);
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
- else {
120
- // If no aliased symbol, it might be a direct interface/type or something we couldn't resolve deeply
121
- // For now, if we have a symbol, we assume it exists to avoid false positives unless we are sure.
122
- // But in our prototype `ghost` gave a symbol that resolved to `unknown`.
123
- // If `getAliasedSymbol` throws or returns undefined, it might be a local symbol.
124
- // Double check with module exports if possible
125
- // const moduleExports = checker.getExportsOfModule(symbol);
126
- // if (!moduleExports.some(e => e.name === importName)) {
127
- // // Potential hallucination or just a type?
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
- // No symbol found at location - definitely a hallucination
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('1.0.0')
51
+ .version('3.0.0')
15
52
  .argument('[directory]', 'Directory to scan', '.')
16
- .action(async (directory) => {
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
+ }
17
84
  console.log(chalk_1.default.blue(`👻 Ghost 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
- process.exit(1); // Fail the build
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost-import-hunter",
3
- "version": "2.0.2",
3
+ "version": "3.0.0",
4
4
  "description": "Deterministic tool to detect AI hallucinations and code bloat by verifying import safety against installed node_modules",
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,4 +45,4 @@
45
45
  "devDependencies": {
46
46
  "ts-node": "^10.9.2"
47
47
  }
48
- }
48
+ }