ghost-import-hunter 1.0.2 ā 2.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 +8 -3
- package/dist/analyzer.js +142 -0
- package/dist/index.js +5 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
## š Features
|
|
16
16
|
|
|
17
17
|
- **Deterministic Validation** - Verify every import against your actual installed modules. No guessing or regex.
|
|
18
|
+
- **Deep Export Analysis** - Uses TypeScript Compiler API to correctly resolve `export *`, re-exports, and aliases.
|
|
19
|
+
- **Local File Scanning** - Validates imports from your own local files, not just external packages.
|
|
18
20
|
- **Zero Configuration** - Works out of the box. Just run `npx ghost-hunter` in your project root.
|
|
19
21
|
- **CI/CD Ready** - Fails the build if hallucinations are detected. Preventing bad code from merging.
|
|
20
22
|
|
|
@@ -54,9 +56,12 @@ Ghost Hunter uses three core technologies to ensure your code is safe:
|
|
|
54
56
|
**Role:** Finding your files.
|
|
55
57
|
Just like your terminal finds files when you type `ls *.ts`, Ghost Hunter uses `glob` to scan your entire project's TypeScript and JavaScript files, ignoring junk like `node_modules`.
|
|
56
58
|
|
|
57
|
-
### 2.
|
|
58
|
-
**Role:**
|
|
59
|
-
|
|
59
|
+
### 2. TypeScript Compiler API - The Brain
|
|
60
|
+
**Role:** Understanding your code.
|
|
61
|
+
Unlike v1 which used Regex, v2.0 uses the real **TypeScript Compiler API** to parse your code, resolve symbols, and track exports across files. This allows it to:
|
|
62
|
+
- Follow `export * from ...` chains.
|
|
63
|
+
- Understand aliased imports (`import { foo as bar }`).
|
|
64
|
+
- Detect missing exports in local files.
|
|
60
65
|
|
|
61
66
|
### 3. `chalk` - The Reporter
|
|
62
67
|
**Role:** Making sense of the output.
|
package/dist/analyzer.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.analyzeProject = analyzeProject;
|
|
37
|
+
const ts = __importStar(require("typescript"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const glob_1 = require("glob");
|
|
40
|
+
async function analyzeProject(directory) {
|
|
41
|
+
const report = { hallucinations: [], unused: [] };
|
|
42
|
+
// 1. Find all files in the project
|
|
43
|
+
const files = await (0, glob_1.glob)('**/*.{ts,tsx,js,jsx}', {
|
|
44
|
+
cwd: directory,
|
|
45
|
+
ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'],
|
|
46
|
+
absolute: true
|
|
47
|
+
});
|
|
48
|
+
if (files.length === 0) {
|
|
49
|
+
return report;
|
|
50
|
+
}
|
|
51
|
+
// 2. Create TypeScript Program
|
|
52
|
+
const program = ts.createProgram(files, {
|
|
53
|
+
target: ts.ScriptTarget.ESNext,
|
|
54
|
+
module: ts.ModuleKind.CommonJS,
|
|
55
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
56
|
+
allowJs: true,
|
|
57
|
+
jsx: ts.JsxEmit.React,
|
|
58
|
+
esModuleInterop: true,
|
|
59
|
+
skipLibCheck: true
|
|
60
|
+
});
|
|
61
|
+
const checker = program.getTypeChecker();
|
|
62
|
+
// 3. Analyze each file
|
|
63
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
64
|
+
// Skip external library files (node_modules)
|
|
65
|
+
if (sourceFile.fileName.includes('node_modules'))
|
|
66
|
+
continue;
|
|
67
|
+
// Also ensure we are only analyzing files we actually found (extra safety)
|
|
68
|
+
// Normalize paths for comparison
|
|
69
|
+
const normalizedFilePath = path.resolve(sourceFile.fileName);
|
|
70
|
+
const isProjectFile = files.some(f => path.resolve(f) === normalizedFilePath);
|
|
71
|
+
if (!isProjectFile)
|
|
72
|
+
continue;
|
|
73
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
74
|
+
if (ts.isImportDeclaration(node)) {
|
|
75
|
+
visitImportDeclaration(node, sourceFile, checker, report);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return report;
|
|
80
|
+
}
|
|
81
|
+
function visitImportDeclaration(node, sourceFile, checker, report) {
|
|
82
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
83
|
+
if (!ts.isStringLiteral(moduleSpecifier))
|
|
84
|
+
return;
|
|
85
|
+
const moduleName = moduleSpecifier.text;
|
|
86
|
+
// Resolve Module Symbol
|
|
87
|
+
const symbol = checker.getSymbolAtLocation(moduleSpecifier);
|
|
88
|
+
if (!symbol) {
|
|
89
|
+
// Module not found at all
|
|
90
|
+
// Get line number
|
|
91
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
92
|
+
report.hallucinations.push({
|
|
93
|
+
file: sourceFile.fileName,
|
|
94
|
+
line: line + 1,
|
|
95
|
+
module: moduleName,
|
|
96
|
+
member: '*' // Whole module missing
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Check Named Imports
|
|
101
|
+
if (node.importClause && node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
|
|
102
|
+
node.importClause.namedBindings.elements.forEach(element => {
|
|
103
|
+
const importName = element.propertyName?.text || element.name.text;
|
|
104
|
+
const importSymbol = checker.getSymbolAtLocation(element.name);
|
|
105
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(element.getStart());
|
|
106
|
+
if (importSymbol) {
|
|
107
|
+
const aliasedSymbol = checker.getAliasedSymbol(importSymbol);
|
|
108
|
+
if (aliasedSymbol) {
|
|
109
|
+
// Check for "unknown" symbol or missing declarations which indicates a hallucination
|
|
110
|
+
if (aliasedSymbol.escapedName === 'unknown' || !aliasedSymbol.declarations || aliasedSymbol.declarations.length === 0) {
|
|
111
|
+
report.hallucinations.push({
|
|
112
|
+
file: sourceFile.fileName,
|
|
113
|
+
line: line + 1,
|
|
114
|
+
module: moduleName,
|
|
115
|
+
member: importName
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
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
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// No symbol found at location - definitely a hallucination
|
|
133
|
+
report.hallucinations.push({
|
|
134
|
+
file: sourceFile.fileName,
|
|
135
|
+
line: line + 1,
|
|
136
|
+
module: moduleName,
|
|
137
|
+
member: importName
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
-
const
|
|
10
|
-
const validator_1 = require("./validator");
|
|
9
|
+
const analyzer_1 = require("./analyzer");
|
|
11
10
|
const program = new commander_1.Command();
|
|
12
11
|
program
|
|
13
12
|
.name('ghost-hunter')
|
|
@@ -17,14 +16,15 @@ program
|
|
|
17
16
|
.action(async (directory) => {
|
|
18
17
|
console.log(chalk_1.default.blue(`š» Ghost Hunter scanning: ${directory}...`));
|
|
19
18
|
try {
|
|
20
|
-
|
|
21
|
-
const report = await (0,
|
|
19
|
+
// New v2 Engine using TS Compiler API
|
|
20
|
+
const report = await (0, analyzer_1.analyzeProject)(directory);
|
|
22
21
|
if (report.hallucinations.length > 0) {
|
|
23
22
|
console.log(chalk_1.default.red('\nšØ Hallucinations Detected (AI Lied!):'));
|
|
24
23
|
report.hallucinations.forEach(h => {
|
|
25
|
-
console.log(` - ${chalk_1.default.bold(h.module)}: Used member ${chalk_1.default.bold(h.member)} does not exist
|
|
24
|
+
console.log(` - ${chalk_1.default.bold(h.module)}: Used member ${chalk_1.default.bold(h.member)} does not exist.`);
|
|
26
25
|
console.log(` File: ${h.file}:${h.line}`);
|
|
27
26
|
});
|
|
27
|
+
process.exit(1); // Fail the build
|
|
28
28
|
}
|
|
29
29
|
else {
|
|
30
30
|
console.log(chalk_1.default.green('\nā
No Hallucinations detected.'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost-import-hunter",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
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
|
+
}
|