thuban 0.3.3 → 0.3.4
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/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +12 -7
- package/cli.js +0 -2627
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -758
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -276
- package/packages/scanner/dependency-graph.js +0 -541
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-collector.js +0 -64
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -301
- package/packages/scanner/hallucination-detector.js +0 -822
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -100
- package/packages/scanner/license-manager.js +0 -331
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/monitor-notifier.js +0 -64
- package/packages/scanner/monitor-service.js +0 -103
- package/packages/scanner/monitor-store.js +0 -117
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/scan-diff.js +0 -50
- package/packages/scanner/scan-runner.js +0 -172
- package/packages/scanner/secret-scanner.js +0 -612
- package/packages/scanner/secret-scanner.test.js +0 -103
- package/packages/scanner/sentinel-core.js +0 -616
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -583
- package/packages/scanner/tech-debt-cost.js +0 -194
- package/packages/scanner/widget-generator.js +0 -415
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
function collectFiles(dir, ignorePatterns = []) {
|
|
5
|
-
const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv', 'coverage', '.cache', '.turbo'];
|
|
6
|
-
const allIgnore = [...defaultIgnore, ...ignorePatterns];
|
|
7
|
-
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs', '.php', '.rb', '.json'];
|
|
8
|
-
const maxFileSize = 1024 * 1024;
|
|
9
|
-
const maxFiles = 50000;
|
|
10
|
-
const files = [];
|
|
11
|
-
const visitedPaths = new Set(); // Track real paths to handle symlinks
|
|
12
|
-
|
|
13
|
-
function walk(currentDir) {
|
|
14
|
-
if (files.length >= maxFiles) return;
|
|
15
|
-
let entries;
|
|
16
|
-
try {
|
|
17
|
-
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
18
|
-
} catch (error) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
if (files.length >= maxFiles) return;
|
|
24
|
-
if (allIgnore.some(pattern => entry.name === pattern || entry.name.match(pattern))) continue;
|
|
25
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
26
|
-
|
|
27
|
-
// Handle symlinks — resolve and skip if already visited or points outside
|
|
28
|
-
if (entry.isSymbolicLink()) {
|
|
29
|
-
try {
|
|
30
|
-
const realPath = fs.realpathSync(fullPath);
|
|
31
|
-
if (visitedPaths.has(realPath)) continue;
|
|
32
|
-
visitedPaths.add(realPath);
|
|
33
|
-
const stat = fs.statSync(fullPath); // follow symlink
|
|
34
|
-
if (stat.isDirectory()) {
|
|
35
|
-
walk(fullPath);
|
|
36
|
-
} else if (stat.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
37
|
-
if (stat.size > 0 && stat.size <= maxFileSize) files.push(fullPath);
|
|
38
|
-
}
|
|
39
|
-
} catch {
|
|
40
|
-
continue; // Broken symlink — skip
|
|
41
|
-
}
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (entry.isDirectory()) {
|
|
46
|
-
walk(fullPath);
|
|
47
|
-
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
48
|
-
try {
|
|
49
|
-
const stat = fs.statSync(fullPath);
|
|
50
|
-
if (stat.size > 0 && stat.size <= maxFileSize) files.push(fullPath);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
// Skip files we can't stat
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
walk(dir);
|
|
59
|
-
return files;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
module.exports = {
|
|
63
|
-
collectFiles,
|
|
64
|
-
};
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File Watcher
|
|
3
|
-
*
|
|
4
|
-
* @purpose Event-driven FileWatcher component
|
|
5
|
-
* @module sentinel
|
|
6
|
-
* @layer application
|
|
7
|
-
* @imports-from fs, path, events
|
|
8
|
-
* @exports-to sentinel
|
|
9
|
-
* @side-effects File system reads, Console output, Event emission
|
|
10
|
-
* @critical-for sentinel
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Sentinel File Watcher
|
|
15
|
-
*
|
|
16
|
-
* Real-time file system monitoring for the Orion codebase.
|
|
17
|
-
* Detects file changes, additions, and deletions to trigger scans.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const EventEmitter = require('events');
|
|
23
|
-
|
|
24
|
-
class FileWatcher extends EventEmitter {
|
|
25
|
-
constructor(config = {}) {
|
|
26
|
-
super();
|
|
27
|
-
|
|
28
|
-
this.config = {
|
|
29
|
-
rootPath: config.rootPath || process.cwd(),
|
|
30
|
-
watchPatterns: config.watchPatterns || ['**/*.js', '**/*.ts', '**/*.json'],
|
|
31
|
-
ignorePatterns: config.ignorePatterns || [
|
|
32
|
-
'node_modules/**',
|
|
33
|
-
'.git/**',
|
|
34
|
-
'dist/**',
|
|
35
|
-
'build/**',
|
|
36
|
-
'.orion/snapshots/**',
|
|
37
|
-
'coverage/**',
|
|
38
|
-
'*.log'
|
|
39
|
-
],
|
|
40
|
-
debounceMs: config.debounceMs || 300,
|
|
41
|
-
...config
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
this.watchers = [];
|
|
45
|
-
this.watchedFiles = new Set();
|
|
46
|
-
this.debounceTimers = new Map();
|
|
47
|
-
this.isRunning = false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Start watching files
|
|
52
|
-
*/
|
|
53
|
-
async start() {
|
|
54
|
-
if (this.isRunning) {
|
|
55
|
-
return { success: true, message: 'Already watching' };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
console.log('[FILE-WATCHER] Starting file watcher...');
|
|
59
|
-
console.log(`[FILE-WATCHER] Root path: ${this.config.rootPath}`);
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
// Discover files to watch
|
|
63
|
-
await this._discoverFiles(this.config.rootPath);
|
|
64
|
-
|
|
65
|
-
// Set up watchers
|
|
66
|
-
this._setupWatchers();
|
|
67
|
-
|
|
68
|
-
this.isRunning = true;
|
|
69
|
-
console.log(`[FILE-WATCHER] Watching ${this.watchedFiles.size} files`);
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
success: true,
|
|
73
|
-
filesWatched: this.watchedFiles.size
|
|
74
|
-
};
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.error('[FILE-WATCHER] Start failed:', error.message);
|
|
77
|
-
return { success: false, error: error.message };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Stop watching files
|
|
83
|
-
*/
|
|
84
|
-
async stop() {
|
|
85
|
-
console.log('[FILE-WATCHER] Stopping file watcher...');
|
|
86
|
-
|
|
87
|
-
// Close all watchers
|
|
88
|
-
for (const watcher of this.watchers) {
|
|
89
|
-
try {
|
|
90
|
-
watcher.close();
|
|
91
|
-
} catch (e) {
|
|
92
|
-
// Ignore close errors
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Clear debounce timers
|
|
97
|
-
for (const timer of this.debounceTimers.values()) {
|
|
98
|
-
clearTimeout(timer);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.watchers = [];
|
|
102
|
-
this.debounceTimers.clear();
|
|
103
|
-
this.isRunning = false;
|
|
104
|
-
|
|
105
|
-
console.log('[FILE-WATCHER] Stopped');
|
|
106
|
-
return { success: true };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get count of watched files
|
|
111
|
-
*/
|
|
112
|
-
getWatchedCount() {
|
|
113
|
-
return this.watchedFiles.size;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Get list of watched files
|
|
118
|
-
*/
|
|
119
|
-
getWatchedFiles() {
|
|
120
|
-
return Array.from(this.watchedFiles);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Check if a file path should be ignored
|
|
125
|
-
*/
|
|
126
|
-
shouldIgnore(filePath) {
|
|
127
|
-
const relativePath = path.relative(this.config.rootPath, filePath);
|
|
128
|
-
|
|
129
|
-
for (const pattern of this.config.ignorePatterns) {
|
|
130
|
-
if (this._matchPattern(relativePath, pattern)) {
|
|
131
|
-
return true;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Check if a file path matches watch patterns
|
|
140
|
-
*/
|
|
141
|
-
shouldWatch(filePath) {
|
|
142
|
-
const relativePath = path.relative(this.config.rootPath, filePath);
|
|
143
|
-
|
|
144
|
-
// First check if ignored
|
|
145
|
-
if (this.shouldIgnore(filePath)) {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Then check if matches watch patterns
|
|
150
|
-
for (const pattern of this.config.watchPatterns) {
|
|
151
|
-
if (this._matchPattern(relativePath, pattern)) {
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Private methods
|
|
160
|
-
|
|
161
|
-
async _discoverFiles(dir) {
|
|
162
|
-
try {
|
|
163
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
164
|
-
|
|
165
|
-
for (const entry of entries) {
|
|
166
|
-
const fullPath = path.join(dir, entry.name);
|
|
167
|
-
|
|
168
|
-
// Skip ignored directories early
|
|
169
|
-
if (entry.isDirectory()) {
|
|
170
|
-
if (!this.shouldIgnore(fullPath)) {
|
|
171
|
-
await this._discoverFiles(fullPath);
|
|
172
|
-
}
|
|
173
|
-
} else if (entry.isFile()) {
|
|
174
|
-
if (this.shouldWatch(fullPath)) {
|
|
175
|
-
this.watchedFiles.add(fullPath);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
// Directory might not exist or be inaccessible
|
|
181
|
-
if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
|
|
182
|
-
console.warn(`[FILE-WATCHER] Error scanning ${dir}:`, error.message);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
_setupWatchers() {
|
|
188
|
-
// Group files by directory for efficient watching
|
|
189
|
-
const directories = new Set();
|
|
190
|
-
for (const file of this.watchedFiles) {
|
|
191
|
-
directories.add(path.dirname(file));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Watch each directory
|
|
195
|
-
for (const dir of directories) {
|
|
196
|
-
try {
|
|
197
|
-
const watcher = fs.watch(dir, { persistent: true }, (eventType, filename) => {
|
|
198
|
-
if (filename) {
|
|
199
|
-
const fullPath = path.join(dir, filename);
|
|
200
|
-
this._handleFileEvent(eventType, fullPath);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
watcher.on('error', (error) => {
|
|
205
|
-
console.warn(`[FILE-WATCHER] Watcher error for ${dir}:`, error.message);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
this.watchers.push(watcher);
|
|
209
|
-
} catch (error) {
|
|
210
|
-
console.warn(`[FILE-WATCHER] Could not watch ${dir}:`, error.message);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Also set up a recursive watcher for the root (if supported)
|
|
215
|
-
try {
|
|
216
|
-
const rootWatcher = fs.watch(this.config.rootPath, { recursive: true }, (eventType, filename) => {
|
|
217
|
-
if (filename) {
|
|
218
|
-
const fullPath = path.join(this.config.rootPath, filename);
|
|
219
|
-
this._handleFileEvent(eventType, fullPath);
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
rootWatcher.on('error', (error) => {
|
|
224
|
-
// Recursive watch might not be supported on all platforms
|
|
225
|
-
if (error.code !== 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
|
|
226
|
-
console.warn('[FILE-WATCHER] Root watcher error:', error.message);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
this.watchers.push(rootWatcher);
|
|
231
|
-
} catch (error) {
|
|
232
|
-
// Recursive watching might not be available
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
_handleFileEvent(eventType, filePath) {
|
|
237
|
-
// Skip if ignored
|
|
238
|
-
if (this.shouldIgnore(filePath)) {
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Debounce rapid events
|
|
243
|
-
if (this.debounceTimers.has(filePath)) {
|
|
244
|
-
clearTimeout(this.debounceTimers.get(filePath));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this.debounceTimers.set(filePath, setTimeout(() => {
|
|
248
|
-
this.debounceTimers.delete(filePath);
|
|
249
|
-
this._processFileEvent(eventType, filePath);
|
|
250
|
-
}, this.config.debounceMs));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
_processFileEvent(eventType, filePath) {
|
|
254
|
-
// Check if file exists
|
|
255
|
-
const exists = fs.existsSync(filePath);
|
|
256
|
-
const isFile = exists && fs.statSync(filePath).isFile();
|
|
257
|
-
|
|
258
|
-
let event;
|
|
259
|
-
|
|
260
|
-
if (!exists) {
|
|
261
|
-
// File was deleted
|
|
262
|
-
if (this.watchedFiles.has(filePath)) {
|
|
263
|
-
this.watchedFiles.delete(filePath);
|
|
264
|
-
event = {
|
|
265
|
-
type: 'delete',
|
|
266
|
-
filePath,
|
|
267
|
-
timestamp: new Date().toISOString()
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
} else if (isFile && this.shouldWatch(filePath)) {
|
|
271
|
-
if (this.watchedFiles.has(filePath)) {
|
|
272
|
-
// File was modified
|
|
273
|
-
event = {
|
|
274
|
-
type: 'change',
|
|
275
|
-
filePath,
|
|
276
|
-
timestamp: new Date().toISOString()
|
|
277
|
-
};
|
|
278
|
-
} else {
|
|
279
|
-
// New file added
|
|
280
|
-
this.watchedFiles.add(filePath);
|
|
281
|
-
event = {
|
|
282
|
-
type: 'add',
|
|
283
|
-
filePath,
|
|
284
|
-
timestamp: new Date().toISOString()
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (event) {
|
|
290
|
-
this.emit(event.type, event);
|
|
291
|
-
this.emit('any', event);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
_matchPattern(filePath, pattern) {
|
|
296
|
-
// Simple glob-like pattern matching
|
|
297
|
-
// Supports: *, **, ?
|
|
298
|
-
|
|
299
|
-
// Normalize separators
|
|
300
|
-
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
301
|
-
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
302
|
-
|
|
303
|
-
// Convert glob pattern to regex
|
|
304
|
-
let regexPattern = normalizedPattern
|
|
305
|
-
.replace(/\./g, '\\.') // Escape dots
|
|
306
|
-
.replace(/\*\*/g, '<<<GLOBSTAR>>>') // Temporarily replace **
|
|
307
|
-
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
308
|
-
.replace(/<<<GLOBSTAR>>>/g, '.*') // ** matches anything including /
|
|
309
|
-
.replace(/\?/g, '.'); // ? matches single char
|
|
310
|
-
|
|
311
|
-
// Anchor the pattern
|
|
312
|
-
regexPattern = '^' + regexPattern + '$';
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
const regex = new RegExp(regexPattern);
|
|
316
|
-
return regex.test(normalizedPath);
|
|
317
|
-
} catch (e) {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
module.exports = FileWatcher;
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thuban Ghost Code Detector
|
|
3
|
-
*
|
|
4
|
-
* @purpose Find AI-generated functions that were pasted but never called
|
|
5
|
-
* @module thuban
|
|
6
|
-
* @layer scanner
|
|
7
|
-
* @exports-to cli
|
|
8
|
-
* @critical-for Unique feature — no other tool targets "pasted but unwired" code
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
class GhostCodeDetector {
|
|
15
|
-
constructor(opts = {}) {
|
|
16
|
-
this.rootPath = opts.rootPath || process.cwd();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Scan files for ghost (uncalled) functions
|
|
21
|
-
*/
|
|
22
|
-
scan(files) {
|
|
23
|
-
const functions = []; // All function definitions
|
|
24
|
-
const references = {}; // Function name → reference count
|
|
25
|
-
|
|
26
|
-
// Pass 1: Find all function definitions
|
|
27
|
-
for (const file of files) {
|
|
28
|
-
let content;
|
|
29
|
-
try {
|
|
30
|
-
const stat = fs.statSync(file);
|
|
31
|
-
if (stat.size > 512 * 1024) continue;
|
|
32
|
-
content = fs.readFileSync(file, 'utf-8');
|
|
33
|
-
} catch (e) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const relPath = path.relative(this.rootPath, file);
|
|
38
|
-
|
|
39
|
-
// Skip test files, config files, and entry points
|
|
40
|
-
if (this._isSkippable(relPath)) continue;
|
|
41
|
-
|
|
42
|
-
const lines = content.split('\n');
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < lines.length; i++) {
|
|
45
|
-
const line = lines[i];
|
|
46
|
-
const fn = this._extractFunction(line, i, lines);
|
|
47
|
-
if (fn) {
|
|
48
|
-
fn.file = relPath;
|
|
49
|
-
fn.line = i + 1;
|
|
50
|
-
fn.lineCount = this._countFunctionLines(lines, i);
|
|
51
|
-
functions.push(fn);
|
|
52
|
-
|
|
53
|
-
// Initialize reference counter
|
|
54
|
-
if (!references[fn.name]) references[fn.name] = 0;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Pass 2: Count references to each function
|
|
60
|
-
for (const file of files) {
|
|
61
|
-
let content;
|
|
62
|
-
try {
|
|
63
|
-
content = fs.readFileSync(file, 'utf-8');
|
|
64
|
-
} catch (e) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Strip comments and strings before counting references to avoid false negatives
|
|
69
|
-
// from functions that appear only in commented-out code
|
|
70
|
-
const strippedContent = this._stripCommentsAndStrings(content);
|
|
71
|
-
|
|
72
|
-
for (const fn of functions) {
|
|
73
|
-
if (fn.name.length < 3) continue; // Skip very short names (likely false positives)
|
|
74
|
-
|
|
75
|
-
// Count how many times this function name appears (excluding its definition)
|
|
76
|
-
const regex = new RegExp(`\\b${this._escapeRegex(fn.name)}\\b`, 'g');
|
|
77
|
-
const matches = strippedContent.match(regex);
|
|
78
|
-
if (matches) {
|
|
79
|
-
const relPath = path.relative(this.rootPath, file);
|
|
80
|
-
// Subtract 1 if this is the file where it's defined (the definition itself)
|
|
81
|
-
const isDefFile = relPath === fn.file;
|
|
82
|
-
references[fn.name] += matches.length - (isDefFile ? 1 : 0);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Pass 3: Find ghosts — defined but never referenced elsewhere
|
|
88
|
-
const ghosts = functions
|
|
89
|
-
.filter(fn => {
|
|
90
|
-
const refs = references[fn.name] || 0;
|
|
91
|
-
// It's a ghost if:
|
|
92
|
-
// - Referenced 0 times outside its definition
|
|
93
|
-
// - Not an export (exports are meant to be used externally)
|
|
94
|
-
// - Not a constructor, lifecycle method, or callback pattern
|
|
95
|
-
return refs <= 0 && !fn.isExport && !fn.isLifecycle && fn.lineCount >= 5;
|
|
96
|
-
})
|
|
97
|
-
.map(fn => ({
|
|
98
|
-
...fn,
|
|
99
|
-
references: references[fn.name] || 0,
|
|
100
|
-
wastedLines: fn.lineCount,
|
|
101
|
-
category: 'ghost',
|
|
102
|
-
severity: fn.lineCount > 30 ? 'high' : 'medium',
|
|
103
|
-
message: `${fn.name}() — ${fn.lineCount} lines, never called`,
|
|
104
|
-
}))
|
|
105
|
-
.sort((a, b) => b.wastedLines - a.wastedLines);
|
|
106
|
-
|
|
107
|
-
const totalWastedLines = ghosts.reduce((sum, g) => sum + g.wastedLines, 0);
|
|
108
|
-
const totalLines = files.reduce((sum, f) => {
|
|
109
|
-
try { return sum + fs.readFileSync(f, 'utf-8').split('\n').length; } catch (e) { return sum; }
|
|
110
|
-
}, 0);
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
ghosts,
|
|
114
|
-
totalGhosts: ghosts.length,
|
|
115
|
-
totalWastedLines,
|
|
116
|
-
wastedPercent: totalLines > 0 ? Math.round((totalWastedLines / totalLines) * 1000) / 10 : 0,
|
|
117
|
-
totalLines,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ─── Private ─────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
_extractFunction(line, lineIndex, lines) {
|
|
124
|
-
const trimmed = line.trim();
|
|
125
|
-
|
|
126
|
-
// JavaScript/TypeScript: function name(
|
|
127
|
-
let match = trimmed.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
|
|
128
|
-
if (match) {
|
|
129
|
-
return {
|
|
130
|
-
name: match[1],
|
|
131
|
-
type: 'function',
|
|
132
|
-
isExport: this._isExported(line, lineIndex, lines),
|
|
133
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// JavaScript/TypeScript: const name = (async) function
|
|
138
|
-
match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function/);
|
|
139
|
-
if (match) {
|
|
140
|
-
return {
|
|
141
|
-
name: match[1],
|
|
142
|
-
type: 'const-function',
|
|
143
|
-
isExport: this._isExported(line, lineIndex, lines),
|
|
144
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// JavaScript/TypeScript: const name = (...) =>
|
|
149
|
-
match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\(?/);
|
|
150
|
-
if (match && (trimmed.includes('=>') || (lineIndex + 1 < lines.length && lines[lineIndex + 1].includes('=>')))) {
|
|
151
|
-
return {
|
|
152
|
-
name: match[1],
|
|
153
|
-
type: 'arrow',
|
|
154
|
-
isExport: this._isExported(line, lineIndex, lines),
|
|
155
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Kotlin: fun functionName(
|
|
160
|
-
match = trimmed.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
|
|
161
|
-
if (match) {
|
|
162
|
-
return {
|
|
163
|
-
name: match[1],
|
|
164
|
-
type: 'kotlin-fun',
|
|
165
|
-
isExport: !trimmed.startsWith('private'),
|
|
166
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Rust: fn function_name(
|
|
171
|
-
match = trimmed.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
|
|
172
|
-
if (match) {
|
|
173
|
-
return {
|
|
174
|
-
name: match[1],
|
|
175
|
-
type: 'rust-fn',
|
|
176
|
-
isExport: /^pub/.test(trimmed),
|
|
177
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// PHP: function functionName( or public/private/protected function functionName(
|
|
182
|
-
match = trimmed.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
|
|
183
|
-
if (match) {
|
|
184
|
-
return {
|
|
185
|
-
name: match[1],
|
|
186
|
-
type: 'php-function',
|
|
187
|
-
isExport: !/private/.test(trimmed),
|
|
188
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Ruby: def method_name or def self.method_name
|
|
193
|
-
match = trimmed.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/);
|
|
194
|
-
if (match) {
|
|
195
|
-
return {
|
|
196
|
-
name: match[1],
|
|
197
|
-
type: 'ruby-def',
|
|
198
|
-
isExport: !match[1].startsWith('_'),
|
|
199
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Class method: name(
|
|
204
|
-
match = trimmed.match(/^(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/);
|
|
205
|
-
if (match && !['if', 'for', 'while', 'switch', 'catch', 'constructor', 'render', 'toString'].includes(match[1])) {
|
|
206
|
-
return {
|
|
207
|
-
name: match[1],
|
|
208
|
-
type: 'method',
|
|
209
|
-
isExport: false,
|
|
210
|
-
isLifecycle: this._isLifecycle(match[1]),
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
_isExported(line, lineIndex, lines) {
|
|
218
|
-
if (/^export\s/.test(line.trim())) return true;
|
|
219
|
-
if (/module\.exports/.test(line)) return true;
|
|
220
|
-
|
|
221
|
-
// Check if it's exported later in the file
|
|
222
|
-
// (simplified — check last 20 lines for module.exports containing the function name)
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
_isLifecycle(name) {
|
|
227
|
-
const lifecycle = [
|
|
228
|
-
// JS/TS lifecycle
|
|
229
|
-
'constructor', 'render', 'componentDidMount', 'componentWillUnmount',
|
|
230
|
-
'componentDidUpdate', 'shouldComponentUpdate', 'getSnapshotBeforeUpdate',
|
|
231
|
-
'getDerivedStateFromProps', 'useEffect', 'useState', 'useMemo',
|
|
232
|
-
'toString', 'valueOf', 'toJSON', 'inspect',
|
|
233
|
-
'setup', 'teardown', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll',
|
|
234
|
-
'init', 'destroy', 'configure', 'bootstrap', 'main',
|
|
235
|
-
'get', 'set', 'post', 'put', 'delete', 'patch', 'handle',
|
|
236
|
-
// Kotlin/Java lifecycle
|
|
237
|
-
'onCreate', 'onStart', 'onResume', 'onPause', 'onStop', 'onDestroy',
|
|
238
|
-
'onCreateView', 'onViewCreated', 'onDestroyView', 'hashCode', 'equals',
|
|
239
|
-
// Rust lifecycle / trait methods
|
|
240
|
-
'new', 'default', 'drop', 'fmt', 'from', 'into', 'clone', 'copy',
|
|
241
|
-
// Ruby lifecycle
|
|
242
|
-
'initialize', 'to_s', 'to_str', 'inspect', 'freeze', 'dup',
|
|
243
|
-
// PHP lifecycle
|
|
244
|
-
'__construct', '__destruct', '__toString', '__get', '__set', '__call',
|
|
245
|
-
];
|
|
246
|
-
return lifecycle.includes(name) || name.startsWith('_') || name.startsWith('on');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
_countFunctionLines(lines, startLine) {
|
|
250
|
-
let depth = 0;
|
|
251
|
-
let started = false;
|
|
252
|
-
let count = 0;
|
|
253
|
-
|
|
254
|
-
for (let i = startLine; i < lines.length && i < startLine + 200; i++) {
|
|
255
|
-
const line = lines[i];
|
|
256
|
-
count++;
|
|
257
|
-
|
|
258
|
-
for (const ch of line) {
|
|
259
|
-
if (ch === '{') { depth++; started = true; }
|
|
260
|
-
if (ch === '}') depth--;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (started && depth <= 0) break;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return count;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
_isSkippable(relPath) {
|
|
270
|
-
const skip = [
|
|
271
|
-
/\.test\./i, /\.spec\./i, /test[\/\\]/i, /spec[\/\\]/i,
|
|
272
|
-
/__test__/i, /__mocks__/i, /\.config\./i, /\.d\.ts$/,
|
|
273
|
-
/index\.(js|ts)$/, /server\.(js|ts)$/, /app\.(js|ts)$/,
|
|
274
|
-
/routes?\.(js|ts)$/, /middleware/i,
|
|
275
|
-
];
|
|
276
|
-
return skip.some(r => r.test(relPath));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
_escapeRegex(str) {
|
|
280
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
_stripCommentsAndStrings(content) {
|
|
284
|
-
// Replace string literals with placeholder (preserve length approximately)
|
|
285
|
-
let result = content
|
|
286
|
-
// Template literals
|
|
287
|
-
.replace(/`[^`\\]*(?:\\[\s\S][^`\\]*)*`/g, '""')
|
|
288
|
-
// Double-quoted strings
|
|
289
|
-
.replace(/"[^"\\\n]*(?:\\.[^"\\\n]*)*"/g, '""')
|
|
290
|
-
// Single-quoted strings
|
|
291
|
-
.replace(/'[^'\\\n]*(?:\\.[^'\\\n]*)*'/g, "''")
|
|
292
|
-
// Block comments (JS/Java/Go/Rust/PHP/Kotlin)
|
|
293
|
-
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
294
|
-
// Line comments: //, #
|
|
295
|
-
.replace(/\/\/[^\n]*/g, ' ')
|
|
296
|
-
.replace(/#[^\n]*/g, ' ');
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
module.exports = GhostCodeDetector;
|