@rigour-labs/core 3.0.1 → 3.0.3
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.
|
@@ -6,17 +6,18 @@
|
|
|
6
6
|
* statements for packages, files, or modules that were never installed
|
|
7
7
|
* or created.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
9
|
+
* Supported languages (v3.0.1):
|
|
10
|
+
* JS/TS — package.json deps, node_modules fallback, Node.js builtins (22.x)
|
|
11
|
+
* Python — stdlib whitelist (3.12+), relative imports, local module resolution
|
|
12
|
+
* Go — stdlib whitelist (1.22+), go.mod module path, aliased imports
|
|
13
|
+
* Ruby — stdlib whitelist (3.3+), Gemfile parsing, require + require_relative
|
|
14
|
+
* C# — .NET 8 framework namespaces, .csproj NuGet parsing, using directives
|
|
15
|
+
* Rust — std/core/alloc crates, Cargo.toml deps, use/extern crate statements
|
|
16
|
+
* Java — java/javax/jakarta stdlib, build.gradle + pom.xml deps, import statements
|
|
17
|
+
* Kotlin — kotlin/kotlinx stdlib, Gradle deps, import statements
|
|
18
18
|
*
|
|
19
19
|
* @since v2.16.0
|
|
20
|
+
* @since v3.0.1 — Go stdlib fix, Ruby/C# strengthened, Rust/Java/Kotlin added
|
|
20
21
|
*/
|
|
21
22
|
import { Gate } from './base.js';
|
|
22
23
|
import { FileScanner } from '../utils/scanner.js';
|
|
@@ -45,9 +46,11 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
45
46
|
const hallucinated = [];
|
|
46
47
|
const files = await FileScanner.findFiles({
|
|
47
48
|
cwd: context.cwd,
|
|
48
|
-
patterns: ['**/*.{ts,js,tsx,jsx,py,go,rb,cs}'],
|
|
49
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'],
|
|
49
50
|
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
50
|
-
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**'
|
|
51
|
+
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**',
|
|
52
|
+
'**/target/debug/**', '**/target/release/**', // Rust
|
|
53
|
+
'**/out/**', '**/.gradle/**', '**/gradle/**'], // Java/Kotlin
|
|
51
54
|
});
|
|
52
55
|
Logger.info(`Hallucinated Imports: Scanning ${files.length} files`);
|
|
53
56
|
// Build lookup sets for fast resolution
|
|
@@ -75,10 +78,16 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
75
78
|
this.checkGoImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
76
79
|
}
|
|
77
80
|
else if (ext === '.rb') {
|
|
78
|
-
this.checkRubyImports(content, file, projectFiles, hallucinated);
|
|
81
|
+
this.checkRubyImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
79
82
|
}
|
|
80
83
|
else if (ext === '.cs') {
|
|
81
|
-
this.checkCSharpImports(content, file, projectFiles, hallucinated);
|
|
84
|
+
this.checkCSharpImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
85
|
+
}
|
|
86
|
+
else if (ext === '.rs') {
|
|
87
|
+
this.checkRustImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
88
|
+
}
|
|
89
|
+
else if (ext === '.java' || ext === '.kt') {
|
|
90
|
+
this.checkJavaKotlinImports(content, file, ext, context.cwd, projectFiles, hallucinated);
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
catch (e) { }
|
|
@@ -233,26 +242,27 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
233
242
|
shouldIgnore(importPath) {
|
|
234
243
|
return this.config.ignore_patterns.some(pattern => new RegExp(pattern).test(importPath));
|
|
235
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Node.js built-in modules — covers Node.js 18/20/22 LTS
|
|
247
|
+
* No third-party packages in this list (removed fs-extra hack).
|
|
248
|
+
*/
|
|
236
249
|
isNodeBuiltin(name) {
|
|
250
|
+
// Fast path: node: protocol prefix
|
|
251
|
+
if (name.startsWith('node:'))
|
|
252
|
+
return true;
|
|
237
253
|
const builtins = new Set([
|
|
238
|
-
'assert', '
|
|
239
|
-
'
|
|
240
|
-
'
|
|
241
|
-
'
|
|
242
|
-
'
|
|
243
|
-
'
|
|
244
|
-
'
|
|
245
|
-
'
|
|
246
|
-
'
|
|
247
|
-
'
|
|
248
|
-
'node:os', 'node:path', 'node:perf_hooks', 'node:process',
|
|
249
|
-
'node:punycode', 'node:querystring', 'node:readline', 'node:repl',
|
|
250
|
-
'node:stream', 'node:string_decoder', 'node:sys', 'node:timers',
|
|
251
|
-
'node:tls', 'node:trace_events', 'node:tty', 'node:url', 'node:util',
|
|
252
|
-
'node:v8', 'node:vm', 'node:wasi', 'node:worker_threads', 'node:zlib',
|
|
253
|
-
'fs-extra', // common enough to skip
|
|
254
|
+
'assert', 'assert/strict', 'async_hooks', 'buffer', 'child_process',
|
|
255
|
+
'cluster', 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel',
|
|
256
|
+
'dns', 'dns/promises', 'domain', 'events', 'fs', 'fs/promises',
|
|
257
|
+
'http', 'http2', 'https', 'inspector', 'inspector/promises', 'module',
|
|
258
|
+
'net', 'os', 'path', 'path/posix', 'path/win32', 'perf_hooks',
|
|
259
|
+
'process', 'punycode', 'querystring', 'readline', 'readline/promises',
|
|
260
|
+
'repl', 'stream', 'stream/consumers', 'stream/promises', 'stream/web',
|
|
261
|
+
'string_decoder', 'sys', 'test', 'timers', 'timers/promises',
|
|
262
|
+
'tls', 'trace_events', 'tty', 'url', 'util', 'util/types',
|
|
263
|
+
'v8', 'vm', 'wasi', 'worker_threads', 'zlib',
|
|
254
264
|
]);
|
|
255
|
-
return builtins.has(name)
|
|
265
|
+
return builtins.has(name);
|
|
256
266
|
}
|
|
257
267
|
isPythonStdlib(modulePath) {
|
|
258
268
|
const topLevel = modulePath.split('.')[0];
|
|
@@ -295,12 +305,31 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
295
305
|
return stdlibs.has(topLevel);
|
|
296
306
|
}
|
|
297
307
|
/**
|
|
298
|
-
* Check Go imports — verify relative
|
|
299
|
-
*
|
|
308
|
+
* Check Go imports — verify project-relative package paths exist.
|
|
309
|
+
*
|
|
310
|
+
* Strategy:
|
|
311
|
+
* 1. Skip Go standard library (comprehensive list of 150+ packages)
|
|
312
|
+
* 2. Skip external modules (any path containing a dot → domain name)
|
|
313
|
+
* 3. Parse go.mod for the project module path
|
|
314
|
+
* 4. Only flag imports that match the project module prefix but don't resolve
|
|
315
|
+
*
|
|
316
|
+
* @since v3.0.1 — fixed false positives on Go stdlib (encoding/json, net/http, etc.)
|
|
300
317
|
*/
|
|
301
318
|
checkGoImports(content, file, cwd, projectFiles, hallucinated) {
|
|
302
319
|
const lines = content.split('\n');
|
|
303
320
|
let inImportBlock = false;
|
|
321
|
+
// Try to read go.mod for the module path
|
|
322
|
+
const goModPath = path.join(cwd, 'go.mod');
|
|
323
|
+
let modulePath = null;
|
|
324
|
+
try {
|
|
325
|
+
if (fs.pathExistsSync(goModPath)) {
|
|
326
|
+
const goMod = fs.readFileSync(goModPath, 'utf-8');
|
|
327
|
+
const moduleMatch = goMod.match(/^module\s+(\S+)/m);
|
|
328
|
+
if (moduleMatch)
|
|
329
|
+
modulePath = moduleMatch[1];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch { /* no go.mod — skip project-relative checks entirely */ }
|
|
304
333
|
for (let i = 0; i < lines.length; i++) {
|
|
305
334
|
const line = lines[i].trim();
|
|
306
335
|
// Detect import block: import ( ... )
|
|
@@ -312,38 +341,155 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
312
341
|
inImportBlock = false;
|
|
313
342
|
continue;
|
|
314
343
|
}
|
|
315
|
-
// Single import: import "path"
|
|
316
|
-
const singleMatch = line.match(/^import\s+"([^"]+)"/);
|
|
317
|
-
const blockMatch = inImportBlock ? line.match(/^\s*"([^"]+)"/) : null;
|
|
344
|
+
// Single import: import "path" or import alias "path"
|
|
345
|
+
const singleMatch = line.match(/^import\s+(?:\w+\s+)?"([^"]+)"/);
|
|
346
|
+
const blockMatch = inImportBlock ? line.match(/^\s*(?:\w+\s+)?"([^"]+)"/) : null;
|
|
318
347
|
const importPath = singleMatch?.[1] || blockMatch?.[1];
|
|
319
348
|
if (!importPath)
|
|
320
349
|
continue;
|
|
321
|
-
// Skip Go
|
|
322
|
-
if (
|
|
350
|
+
// 1. Skip Go standard library — comprehensive list
|
|
351
|
+
if (this.isGoStdlib(importPath))
|
|
323
352
|
continue;
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
const hasMatchingFile = [...projectFiles].some(f => f.includes(dirPath));
|
|
353
|
+
// 2. If we have a module path, check project-relative imports FIRST
|
|
354
|
+
// (project imports like github.com/myorg/project/pkg also have dots)
|
|
355
|
+
if (modulePath && importPath.startsWith(modulePath + '/')) {
|
|
356
|
+
const relPath = importPath.slice(modulePath.length + 1);
|
|
357
|
+
const hasMatchingFile = [...projectFiles].some(f => f.endsWith('.go') && f.startsWith(relPath));
|
|
330
358
|
if (!hasMatchingFile) {
|
|
331
359
|
hallucinated.push({
|
|
332
360
|
file, line: i + 1, importPath, type: 'go',
|
|
333
|
-
reason: `Go import '${importPath}' — package
|
|
361
|
+
reason: `Go import '${importPath}' — package directory '${relPath}' not found in project`,
|
|
334
362
|
});
|
|
335
363
|
}
|
|
364
|
+
continue;
|
|
336
365
|
}
|
|
366
|
+
// 3. Skip external modules — any import containing a dot is a domain
|
|
367
|
+
// e.g. github.com/*, google.golang.org/*, go.uber.org/*
|
|
368
|
+
if (importPath.includes('.'))
|
|
369
|
+
continue;
|
|
370
|
+
// 4. No dots, no go.mod match, not stdlib → likely an internal package
|
|
371
|
+
// without go.mod context we can't verify, so skip to avoid false positives
|
|
337
372
|
}
|
|
338
373
|
}
|
|
339
374
|
/**
|
|
340
|
-
*
|
|
375
|
+
* Comprehensive Go standard library package list.
|
|
376
|
+
* Includes all packages from Go 1.22+ (latest stable).
|
|
377
|
+
* Go stdlib is identified by having NO dots in the import path.
|
|
378
|
+
* We maintain an explicit list for packages with slashes (e.g. encoding/json).
|
|
379
|
+
*
|
|
380
|
+
* @since v3.0.1
|
|
381
|
+
*/
|
|
382
|
+
isGoStdlib(importPath) {
|
|
383
|
+
// Fast check: single-segment packages are always stdlib if no dots
|
|
384
|
+
if (!importPath.includes('/') && !importPath.includes('.'))
|
|
385
|
+
return true;
|
|
386
|
+
// Check the full path against known stdlib packages with sub-paths
|
|
387
|
+
const topLevel = importPath.split('/')[0];
|
|
388
|
+
// All Go stdlib top-level packages (including those with sub-packages)
|
|
389
|
+
const stdlibTopLevel = new Set([
|
|
390
|
+
// Single-word packages
|
|
391
|
+
'archive', 'bufio', 'builtin', 'bytes', 'cmp', 'compress',
|
|
392
|
+
'container', 'context', 'crypto', 'database', 'debug',
|
|
393
|
+
'embed', 'encoding', 'errors', 'expvar', 'flag', 'fmt',
|
|
394
|
+
'go', 'hash', 'html', 'image', 'index', 'io', 'iter',
|
|
395
|
+
'log', 'maps', 'math', 'mime', 'net', 'os', 'path',
|
|
396
|
+
'plugin', 'reflect', 'regexp', 'runtime', 'slices', 'sort',
|
|
397
|
+
'strconv', 'strings', 'structs', 'sync', 'syscall',
|
|
398
|
+
'testing', 'text', 'time', 'unicode', 'unique', 'unsafe',
|
|
399
|
+
// Internal packages (used by stdlib, sometimes by tools)
|
|
400
|
+
'internal', 'vendor',
|
|
401
|
+
]);
|
|
402
|
+
if (stdlibTopLevel.has(topLevel))
|
|
403
|
+
return true;
|
|
404
|
+
// Explicit full-path list for maximum safety — covers all Go 1.22 stdlib paths
|
|
405
|
+
// This catches any edge case the top-level check might miss
|
|
406
|
+
const knownStdlibPaths = new Set([
|
|
407
|
+
// archive/*
|
|
408
|
+
'archive/tar', 'archive/zip',
|
|
409
|
+
// compress/*
|
|
410
|
+
'compress/bzip2', 'compress/flate', 'compress/gzip', 'compress/lzw', 'compress/zlib',
|
|
411
|
+
// container/*
|
|
412
|
+
'container/heap', 'container/list', 'container/ring',
|
|
413
|
+
// crypto/*
|
|
414
|
+
'crypto/aes', 'crypto/cipher', 'crypto/des', 'crypto/dsa',
|
|
415
|
+
'crypto/ecdh', 'crypto/ecdsa', 'crypto/ed25519', 'crypto/elliptic',
|
|
416
|
+
'crypto/hmac', 'crypto/md5', 'crypto/rand', 'crypto/rc4',
|
|
417
|
+
'crypto/rsa', 'crypto/sha1', 'crypto/sha256', 'crypto/sha512',
|
|
418
|
+
'crypto/subtle', 'crypto/tls', 'crypto/x509', 'crypto/x509/pkix',
|
|
419
|
+
// database/*
|
|
420
|
+
'database/sql', 'database/sql/driver',
|
|
421
|
+
// debug/*
|
|
422
|
+
'debug/buildinfo', 'debug/dwarf', 'debug/elf', 'debug/gosym',
|
|
423
|
+
'debug/macho', 'debug/pe', 'debug/plan9obj',
|
|
424
|
+
// encoding/*
|
|
425
|
+
'encoding/ascii85', 'encoding/asn1', 'encoding/base32', 'encoding/base64',
|
|
426
|
+
'encoding/binary', 'encoding/csv', 'encoding/gob', 'encoding/hex',
|
|
427
|
+
'encoding/json', 'encoding/pem', 'encoding/xml',
|
|
428
|
+
// go/*
|
|
429
|
+
'go/ast', 'go/build', 'go/build/constraint', 'go/constant',
|
|
430
|
+
'go/doc', 'go/doc/comment', 'go/format', 'go/importer',
|
|
431
|
+
'go/parser', 'go/printer', 'go/scanner', 'go/token', 'go/types', 'go/version',
|
|
432
|
+
// hash/*
|
|
433
|
+
'hash/adler32', 'hash/crc32', 'hash/crc64', 'hash/fnv', 'hash/maphash',
|
|
434
|
+
// html/*
|
|
435
|
+
'html/template',
|
|
436
|
+
// image/*
|
|
437
|
+
'image/color', 'image/color/palette', 'image/draw',
|
|
438
|
+
'image/gif', 'image/jpeg', 'image/png',
|
|
439
|
+
// index/*
|
|
440
|
+
'index/suffixarray',
|
|
441
|
+
// io/*
|
|
442
|
+
'io/fs', 'io/ioutil',
|
|
443
|
+
// log/*
|
|
444
|
+
'log/slog', 'log/syslog',
|
|
445
|
+
// math/*
|
|
446
|
+
'math/big', 'math/bits', 'math/cmplx', 'math/rand', 'math/rand/v2',
|
|
447
|
+
// mime/*
|
|
448
|
+
'mime/multipart', 'mime/quotedprintable',
|
|
449
|
+
// net/*
|
|
450
|
+
'net/http', 'net/http/cgi', 'net/http/cookiejar', 'net/http/fcgi',
|
|
451
|
+
'net/http/httptest', 'net/http/httptrace', 'net/http/httputil',
|
|
452
|
+
'net/http/pprof', 'net/mail', 'net/netip', 'net/rpc',
|
|
453
|
+
'net/rpc/jsonrpc', 'net/smtp', 'net/textproto', 'net/url',
|
|
454
|
+
// os/*
|
|
455
|
+
'os/exec', 'os/signal', 'os/user',
|
|
456
|
+
// path/*
|
|
457
|
+
'path/filepath',
|
|
458
|
+
// regexp/*
|
|
459
|
+
'regexp/syntax',
|
|
460
|
+
// runtime/*
|
|
461
|
+
'runtime/cgo', 'runtime/coverage', 'runtime/debug', 'runtime/metrics',
|
|
462
|
+
'runtime/pprof', 'runtime/race', 'runtime/trace',
|
|
463
|
+
// sync/*
|
|
464
|
+
'sync/atomic',
|
|
465
|
+
// testing/*
|
|
466
|
+
'testing/fstest', 'testing/iotest', 'testing/quick', 'testing/slogtest',
|
|
467
|
+
// text/*
|
|
468
|
+
'text/scanner', 'text/tabwriter', 'text/template', 'text/template/parse',
|
|
469
|
+
// unicode/*
|
|
470
|
+
'unicode/utf16', 'unicode/utf8',
|
|
471
|
+
]);
|
|
472
|
+
return knownStdlibPaths.has(importPath);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Check Ruby imports — require, require_relative, Gemfile verification
|
|
476
|
+
*
|
|
477
|
+
* Strategy:
|
|
478
|
+
* 1. require_relative: verify target .rb file exists in project
|
|
479
|
+
* 2. require: skip stdlib, skip gems from Gemfile/gemspec, flag unknown local requires
|
|
480
|
+
*
|
|
481
|
+
* @since v3.0.1 — strengthened with stdlib whitelist and Gemfile parsing
|
|
341
482
|
*/
|
|
342
|
-
checkRubyImports(content, file, projectFiles, hallucinated) {
|
|
483
|
+
checkRubyImports(content, file, cwd, projectFiles, hallucinated) {
|
|
343
484
|
const lines = content.split('\n');
|
|
485
|
+
// Parse Gemfile for known gem dependencies
|
|
486
|
+
const gemDeps = this.loadRubyGems(cwd);
|
|
344
487
|
for (let i = 0; i < lines.length; i++) {
|
|
345
488
|
const line = lines[i].trim();
|
|
346
|
-
//
|
|
489
|
+
// Skip comments
|
|
490
|
+
if (line.startsWith('#'))
|
|
491
|
+
continue;
|
|
492
|
+
// require_relative 'path' — must resolve to a real file
|
|
347
493
|
const relMatch = line.match(/require_relative\s+['"]([^'"]+)['"]/);
|
|
348
494
|
if (relMatch) {
|
|
349
495
|
const reqPath = relMatch[1];
|
|
@@ -356,43 +502,406 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
356
502
|
reason: `require_relative '${reqPath}' — file not found in project`,
|
|
357
503
|
});
|
|
358
504
|
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
// require 'something' — check stdlib, gems, then local
|
|
508
|
+
const reqMatch = line.match(/^require\s+['"]([^'"]+)['"]/);
|
|
509
|
+
if (reqMatch) {
|
|
510
|
+
const reqPath = reqMatch[1];
|
|
511
|
+
// Skip Ruby stdlib
|
|
512
|
+
if (this.isRubyStdlib(reqPath))
|
|
513
|
+
continue;
|
|
514
|
+
// Skip gems listed in Gemfile
|
|
515
|
+
const gemName = reqPath.split('/')[0];
|
|
516
|
+
if (gemDeps.has(gemName))
|
|
517
|
+
continue;
|
|
518
|
+
// Check if it resolves to a project file
|
|
519
|
+
const candidates = [
|
|
520
|
+
reqPath + '.rb',
|
|
521
|
+
reqPath,
|
|
522
|
+
'lib/' + reqPath + '.rb',
|
|
523
|
+
'lib/' + reqPath,
|
|
524
|
+
];
|
|
525
|
+
const found = candidates.some(c => projectFiles.has(c));
|
|
526
|
+
if (!found) {
|
|
527
|
+
// If we have a Gemfile and it's not in it, it might be hallucinated
|
|
528
|
+
if (gemDeps.size > 0) {
|
|
529
|
+
hallucinated.push({
|
|
530
|
+
file, line: i + 1, importPath: reqPath, type: 'ruby',
|
|
531
|
+
reason: `require '${reqPath}' — not in stdlib, Gemfile, or project files`,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/** Load gem names from Gemfile */
|
|
539
|
+
loadRubyGems(cwd) {
|
|
540
|
+
const gems = new Set();
|
|
541
|
+
try {
|
|
542
|
+
const gemfilePath = path.join(cwd, 'Gemfile');
|
|
543
|
+
if (fs.pathExistsSync(gemfilePath)) {
|
|
544
|
+
const content = fs.readFileSync(gemfilePath, 'utf-8');
|
|
545
|
+
const gemPattern = /gem\s+['"]([^'"]+)['"]/g;
|
|
546
|
+
let m;
|
|
547
|
+
while ((m = gemPattern.exec(content)) !== null) {
|
|
548
|
+
gems.add(m[1]);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Also check .gemspec
|
|
552
|
+
const gemspecs = [...new Set()]; // placeholder
|
|
553
|
+
const files = fs.readdirSync?.(cwd) || [];
|
|
554
|
+
for (const f of files) {
|
|
555
|
+
if (typeof f === 'string' && f.endsWith('.gemspec')) {
|
|
556
|
+
try {
|
|
557
|
+
const spec = fs.readFileSync(path.join(cwd, f), 'utf-8');
|
|
558
|
+
const depPattern = /add_(?:runtime_)?dependency\s+['"]([^'"]+)['"]/g;
|
|
559
|
+
let dm;
|
|
560
|
+
while ((dm = depPattern.exec(spec)) !== null) {
|
|
561
|
+
gems.add(dm[1]);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch { /* skip */ }
|
|
565
|
+
}
|
|
359
566
|
}
|
|
360
567
|
}
|
|
568
|
+
catch { /* no Gemfile */ }
|
|
569
|
+
return gems;
|
|
361
570
|
}
|
|
362
571
|
/**
|
|
363
|
-
*
|
|
364
|
-
*
|
|
572
|
+
* Ruby standard library — covers Ruby 3.3+ (MRI)
|
|
573
|
+
* Includes both the default gems and bundled gems that ship with Ruby.
|
|
365
574
|
*/
|
|
366
|
-
|
|
575
|
+
isRubyStdlib(name) {
|
|
576
|
+
const topLevel = name.split('/')[0];
|
|
577
|
+
const stdlibs = new Set([
|
|
578
|
+
// Core libs (always available)
|
|
579
|
+
'abbrev', 'base64', 'benchmark', 'bigdecimal', 'cgi', 'csv',
|
|
580
|
+
'date', 'delegate', 'did_you_mean', 'digest', 'drb', 'english',
|
|
581
|
+
'erb', 'error_highlight', 'etc', 'fcntl', 'fiddle', 'fileutils',
|
|
582
|
+
'find', 'forwardable', 'getoptlong', 'io', 'ipaddr', 'irb',
|
|
583
|
+
'json', 'logger', 'matrix', 'minitest', 'monitor', 'mutex_m',
|
|
584
|
+
'net', 'nkf', 'objspace', 'observer', 'open3', 'open-uri',
|
|
585
|
+
'openssl', 'optparse', 'ostruct', 'pathname', 'pp', 'prettyprint',
|
|
586
|
+
'prime', 'pstore', 'psych', 'racc', 'rake', 'rdoc', 'readline',
|
|
587
|
+
'reline', 'resolv', 'resolv-replace', 'rinda', 'ruby2_keywords',
|
|
588
|
+
'rubygems', 'securerandom', 'set', 'shellwords', 'singleton',
|
|
589
|
+
'socket', 'stringio', 'strscan', 'syntax_suggest', 'syslog',
|
|
590
|
+
'tempfile', 'time', 'timeout', 'tmpdir', 'tsort', 'un',
|
|
591
|
+
'unicode_normalize', 'uri', 'weakref', 'yaml', 'zlib',
|
|
592
|
+
// Default gems (ship with Ruby, can be overridden)
|
|
593
|
+
'bundler', 'debug', 'net-ftp', 'net-http', 'net-imap',
|
|
594
|
+
'net-pop', 'net-protocol', 'net-smtp', 'power_assert',
|
|
595
|
+
'test-unit', 'rexml', 'rss', 'typeprof',
|
|
596
|
+
// Common C extensions
|
|
597
|
+
'stringio', 'io/console', 'io/nonblock', 'io/wait',
|
|
598
|
+
'rbconfig', 'mkmf', 'thread',
|
|
599
|
+
// Rails-adjacent but actually stdlib
|
|
600
|
+
'webrick', 'cmath', 'complex', 'rational',
|
|
601
|
+
'coverage', 'ripper', 'win32ole', 'win32api',
|
|
602
|
+
]);
|
|
603
|
+
return stdlibs.has(topLevel);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Check C# imports — using directives against .NET framework, NuGet, and project
|
|
607
|
+
*
|
|
608
|
+
* Strategy:
|
|
609
|
+
* 1. Skip .NET framework namespaces (System.*, Microsoft.*, etc.)
|
|
610
|
+
* 2. Skip NuGet packages from .csproj PackageReference
|
|
611
|
+
* 3. Flag project-relative namespaces that don't resolve
|
|
612
|
+
*
|
|
613
|
+
* @since v3.0.1 — .csproj NuGet parsing, comprehensive framework namespace list
|
|
614
|
+
*/
|
|
615
|
+
checkCSharpImports(content, file, cwd, projectFiles, hallucinated) {
|
|
367
616
|
const lines = content.split('\n');
|
|
617
|
+
const nugetPackages = this.loadNuGetPackages(cwd);
|
|
368
618
|
for (let i = 0; i < lines.length; i++) {
|
|
369
619
|
const line = lines[i].trim();
|
|
370
|
-
// using
|
|
371
|
-
|
|
620
|
+
// Match: using Namespace; and using static Namespace.Class;
|
|
621
|
+
// Skip: using alias = Namespace; and using (var x = ...) disposable
|
|
622
|
+
const usingMatch = line.match(/^using\s+(?:static\s+)?([\w.]+)\s*;/);
|
|
372
623
|
if (!usingMatch)
|
|
373
624
|
continue;
|
|
374
625
|
const namespace = usingMatch[1];
|
|
375
|
-
// Skip
|
|
376
|
-
if (
|
|
626
|
+
// 1. Skip .NET framework and BCL namespaces
|
|
627
|
+
if (this.isDotNetFramework(namespace))
|
|
628
|
+
continue;
|
|
629
|
+
// 2. Skip NuGet packages from .csproj
|
|
630
|
+
const topLevel = namespace.split('.')[0];
|
|
631
|
+
if (nugetPackages.has(topLevel) || nugetPackages.has(namespace.split('.').slice(0, 2).join('.')))
|
|
377
632
|
continue;
|
|
378
|
-
// Check if the namespace maps to any .cs file
|
|
633
|
+
// 3. Check if the namespace maps to any .cs file in the project
|
|
634
|
+
// C# namespaces often have a root prefix (project name) not in the directory tree
|
|
635
|
+
// e.g. MyProject.Services.UserService → check Services/UserService AND MyProject/Services/UserService
|
|
636
|
+
const nsParts = namespace.split('.');
|
|
379
637
|
const nsPath = namespace.replace(/\./g, '/');
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
638
|
+
// Also check without root prefix (common convention: namespace root != directory root)
|
|
639
|
+
const nsPathNoRoot = nsParts.slice(1).join('/');
|
|
640
|
+
const csFiles = [...projectFiles].filter(f => f.endsWith('.cs'));
|
|
641
|
+
const hasMatch = csFiles.some(f => f.includes(nsPath) || (nsPathNoRoot && f.includes(nsPathNoRoot)));
|
|
642
|
+
// Only flag if we have .csproj context (proves this is a real .NET project)
|
|
643
|
+
if (!hasMatch && namespace.includes('.') && nugetPackages.size >= 0) {
|
|
644
|
+
// Check if we actually have .csproj context (a real .NET project)
|
|
645
|
+
const hasCsproj = this.hasCsprojFile(cwd);
|
|
646
|
+
if (hasCsproj) {
|
|
388
647
|
hallucinated.push({
|
|
389
648
|
file, line: i + 1, importPath: namespace, type: 'csharp',
|
|
390
|
-
reason: `Namespace '${namespace}' — no matching files
|
|
649
|
+
reason: `Namespace '${namespace}' — no matching files in project, not in NuGet packages`,
|
|
391
650
|
});
|
|
392
651
|
}
|
|
393
652
|
}
|
|
394
653
|
}
|
|
395
654
|
}
|
|
655
|
+
/** Check if any .csproj file exists in the project root */
|
|
656
|
+
hasCsprojFile(cwd) {
|
|
657
|
+
try {
|
|
658
|
+
const files = fs.readdirSync?.(cwd) || [];
|
|
659
|
+
return files.some((f) => typeof f === 'string' && f.endsWith('.csproj'));
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/** Parse .csproj files for PackageReference names */
|
|
666
|
+
loadNuGetPackages(cwd) {
|
|
667
|
+
const packages = new Set();
|
|
668
|
+
try {
|
|
669
|
+
const files = fs.readdirSync?.(cwd) || [];
|
|
670
|
+
for (const f of files) {
|
|
671
|
+
if (typeof f === 'string' && f.endsWith('.csproj')) {
|
|
672
|
+
try {
|
|
673
|
+
const content = fs.readFileSync(path.join(cwd, f), 'utf-8');
|
|
674
|
+
const pkgPattern = /PackageReference\s+Include="([^"]+)"/g;
|
|
675
|
+
let m;
|
|
676
|
+
while ((m = pkgPattern.exec(content)) !== null) {
|
|
677
|
+
packages.add(m[1]);
|
|
678
|
+
// Also add top-level namespace (e.g. Newtonsoft.Json → Newtonsoft)
|
|
679
|
+
packages.add(m[1].split('.')[0]);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch { /* skip */ }
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch { /* no .csproj */ }
|
|
687
|
+
return packages;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* .NET 8 framework and common ecosystem namespaces
|
|
691
|
+
* Covers BCL, ASP.NET, EF Core, and major ecosystem packages
|
|
692
|
+
*/
|
|
693
|
+
isDotNetFramework(namespace) {
|
|
694
|
+
const topLevel = namespace.split('.')[0];
|
|
695
|
+
const frameworkPrefixes = new Set([
|
|
696
|
+
// BCL / .NET Runtime
|
|
697
|
+
'System', 'Microsoft', 'Windows',
|
|
698
|
+
// Common ecosystem (NuGet defaults everyone uses)
|
|
699
|
+
'Newtonsoft', 'NUnit', 'Xunit', 'Moq', 'AutoMapper',
|
|
700
|
+
'FluentAssertions', 'FluentValidation', 'Serilog', 'NLog',
|
|
701
|
+
'Dapper', 'MediatR', 'Polly', 'Swashbuckle', 'Hangfire',
|
|
702
|
+
'StackExchange', 'Npgsql', 'MongoDB', 'MySql', 'Oracle',
|
|
703
|
+
'Amazon', 'Google', 'Azure', 'Grpc',
|
|
704
|
+
'Bogus', 'Humanizer', 'CsvHelper', 'MailKit', 'MimeKit',
|
|
705
|
+
'RestSharp', 'Refit', 'AutoFixture', 'Shouldly',
|
|
706
|
+
'IdentityModel', 'IdentityServer4',
|
|
707
|
+
]);
|
|
708
|
+
return frameworkPrefixes.has(topLevel);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Check Rust imports — use/extern crate against std/core/alloc and Cargo.toml
|
|
712
|
+
*
|
|
713
|
+
* Strategy:
|
|
714
|
+
* 1. Skip Rust std, core, alloc crates
|
|
715
|
+
* 2. Skip crates listed in Cargo.toml [dependencies]
|
|
716
|
+
* 3. Flag unknown extern crate and use statements for project modules that don't exist
|
|
717
|
+
*
|
|
718
|
+
* @since v3.0.1
|
|
719
|
+
*/
|
|
720
|
+
checkRustImports(content, file, cwd, projectFiles, hallucinated) {
|
|
721
|
+
const lines = content.split('\n');
|
|
722
|
+
const cargoDeps = this.loadCargoDeps(cwd);
|
|
723
|
+
for (let i = 0; i < lines.length; i++) {
|
|
724
|
+
const line = lines[i].trim();
|
|
725
|
+
if (line.startsWith('//') || line.startsWith('/*'))
|
|
726
|
+
continue;
|
|
727
|
+
// extern crate foo;
|
|
728
|
+
const externMatch = line.match(/^extern\s+crate\s+(\w+)/);
|
|
729
|
+
if (externMatch) {
|
|
730
|
+
const crateName = externMatch[1];
|
|
731
|
+
if (this.isRustStdCrate(crateName))
|
|
732
|
+
continue;
|
|
733
|
+
if (cargoDeps.has(crateName))
|
|
734
|
+
continue;
|
|
735
|
+
hallucinated.push({
|
|
736
|
+
file, line: i + 1, importPath: crateName, type: 'rust',
|
|
737
|
+
reason: `extern crate '${crateName}' — not in Cargo.toml or Rust std`,
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
// use foo::bar::baz; or use foo::{bar, baz};
|
|
742
|
+
const useMatch = line.match(/^(?:pub\s+)?use\s+(\w+)::/);
|
|
743
|
+
if (useMatch) {
|
|
744
|
+
const crateName = useMatch[1];
|
|
745
|
+
if (this.isRustStdCrate(crateName))
|
|
746
|
+
continue;
|
|
747
|
+
if (cargoDeps.has(crateName))
|
|
748
|
+
continue;
|
|
749
|
+
// 'crate' and 'self' and 'super' are Rust path keywords
|
|
750
|
+
if (['crate', 'self', 'super'].includes(crateName))
|
|
751
|
+
continue;
|
|
752
|
+
hallucinated.push({
|
|
753
|
+
file, line: i + 1, importPath: crateName, type: 'rust',
|
|
754
|
+
reason: `use ${crateName}:: — crate not in Cargo.toml or Rust std`,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/** Load dependency names from Cargo.toml */
|
|
760
|
+
loadCargoDeps(cwd) {
|
|
761
|
+
const deps = new Set();
|
|
762
|
+
try {
|
|
763
|
+
const cargoPath = path.join(cwd, 'Cargo.toml');
|
|
764
|
+
if (fs.pathExistsSync(cargoPath)) {
|
|
765
|
+
const content = fs.readFileSync(cargoPath, 'utf-8');
|
|
766
|
+
// Match [dependencies] section entries: name = "version" or name = { ... }
|
|
767
|
+
const depPattern = /^\s*(\w[\w-]*)\s*=/gm;
|
|
768
|
+
let inDeps = false;
|
|
769
|
+
for (const line of content.split('\n')) {
|
|
770
|
+
if (/^\[(?:.*-)?dependencies/.test(line.trim())) {
|
|
771
|
+
inDeps = true;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (/^\[/.test(line.trim()) && inDeps) {
|
|
775
|
+
inDeps = false;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (inDeps) {
|
|
779
|
+
const m = line.match(/^\s*([\w][\w-]*)\s*=/);
|
|
780
|
+
if (m)
|
|
781
|
+
deps.add(m[1].replace(/-/g, '_')); // Rust uses _ in code for - in Cargo
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch { /* no Cargo.toml */ }
|
|
787
|
+
return deps;
|
|
788
|
+
}
|
|
789
|
+
/** Rust standard crates — std, core, alloc, proc_macro, and common test crates */
|
|
790
|
+
isRustStdCrate(name) {
|
|
791
|
+
const stdCrates = new Set([
|
|
792
|
+
'std', 'core', 'alloc', 'proc_macro', 'test',
|
|
793
|
+
// Common proc-macro / compiler crates
|
|
794
|
+
'proc_macro2', 'syn', 'quote',
|
|
795
|
+
]);
|
|
796
|
+
return stdCrates.has(name);
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Check Java/Kotlin imports — against stdlib and build dependencies
|
|
800
|
+
*
|
|
801
|
+
* Strategy:
|
|
802
|
+
* 1. Skip java.*, javax.*, jakarta.* (Java stdlib/EE)
|
|
803
|
+
* 2. Skip kotlin.*, kotlinx.* (Kotlin stdlib)
|
|
804
|
+
* 3. Skip deps from build.gradle or pom.xml
|
|
805
|
+
* 4. Flag project-relative imports that don't resolve
|
|
806
|
+
*
|
|
807
|
+
* @since v3.0.1
|
|
808
|
+
*/
|
|
809
|
+
checkJavaKotlinImports(content, file, ext, cwd, projectFiles, hallucinated) {
|
|
810
|
+
const lines = content.split('\n');
|
|
811
|
+
const buildDeps = this.loadJavaDeps(cwd);
|
|
812
|
+
const isKotlin = ext === '.kt';
|
|
813
|
+
for (let i = 0; i < lines.length; i++) {
|
|
814
|
+
const line = lines[i].trim();
|
|
815
|
+
// import com.example.package.Class
|
|
816
|
+
const importMatch = line.match(/^import\s+(?:static\s+)?([\w.]+)/);
|
|
817
|
+
if (!importMatch)
|
|
818
|
+
continue;
|
|
819
|
+
const importPath = importMatch[1];
|
|
820
|
+
// Skip Java stdlib
|
|
821
|
+
if (this.isJavaStdlib(importPath))
|
|
822
|
+
continue;
|
|
823
|
+
// Skip Kotlin stdlib
|
|
824
|
+
if (isKotlin && this.isKotlinStdlib(importPath))
|
|
825
|
+
continue;
|
|
826
|
+
// Skip known build dependencies (by group prefix)
|
|
827
|
+
const parts = importPath.split('.');
|
|
828
|
+
const group2 = parts.slice(0, 2).join('.');
|
|
829
|
+
const group3 = parts.slice(0, 3).join('.');
|
|
830
|
+
if (buildDeps.has(group2) || buildDeps.has(group3))
|
|
831
|
+
continue;
|
|
832
|
+
// Check if it resolves to a project file
|
|
833
|
+
const javaPath = importPath.replace(/\./g, '/');
|
|
834
|
+
const candidates = [
|
|
835
|
+
javaPath + '.java',
|
|
836
|
+
javaPath + '.kt',
|
|
837
|
+
'src/main/java/' + javaPath + '.java',
|
|
838
|
+
'src/main/kotlin/' + javaPath + '.kt',
|
|
839
|
+
];
|
|
840
|
+
const found = candidates.some(c => projectFiles.has(c)) ||
|
|
841
|
+
[...projectFiles].some(f => f.includes(javaPath));
|
|
842
|
+
if (!found) {
|
|
843
|
+
// Only flag if we have build deps context (Gradle/Maven project)
|
|
844
|
+
if (buildDeps.size > 0) {
|
|
845
|
+
hallucinated.push({
|
|
846
|
+
file, line: i + 1, importPath, type: isKotlin ? 'kotlin' : 'java',
|
|
847
|
+
reason: `import '${importPath}' — not in stdlib, build deps, or project files`,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/** Load dependency group IDs from build.gradle or pom.xml */
|
|
854
|
+
loadJavaDeps(cwd) {
|
|
855
|
+
const deps = new Set();
|
|
856
|
+
try {
|
|
857
|
+
// Gradle: build.gradle or build.gradle.kts
|
|
858
|
+
for (const gradleFile of ['build.gradle', 'build.gradle.kts']) {
|
|
859
|
+
const gradlePath = path.join(cwd, gradleFile);
|
|
860
|
+
if (fs.pathExistsSync(gradlePath)) {
|
|
861
|
+
const content = fs.readFileSync(gradlePath, 'utf-8');
|
|
862
|
+
// Match: implementation 'group:artifact:version' or "group:artifact:version"
|
|
863
|
+
const depPattern = /(?:implementation|api|compile|testImplementation|runtimeOnly)\s*[('"]([^:'"]+)/g;
|
|
864
|
+
let m;
|
|
865
|
+
while ((m = depPattern.exec(content)) !== null) {
|
|
866
|
+
deps.add(m[1]); // group ID like "com.google.guava"
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// Maven: pom.xml
|
|
871
|
+
const pomPath = path.join(cwd, 'pom.xml');
|
|
872
|
+
if (fs.pathExistsSync(pomPath)) {
|
|
873
|
+
const content = fs.readFileSync(pomPath, 'utf-8');
|
|
874
|
+
const groupPattern = /<groupId>([^<]+)<\/groupId>/g;
|
|
875
|
+
let m;
|
|
876
|
+
while ((m = groupPattern.exec(content)) !== null) {
|
|
877
|
+
deps.add(m[1]);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
catch { /* no build files */ }
|
|
882
|
+
return deps;
|
|
883
|
+
}
|
|
884
|
+
/** Java standard library and Jakarta EE namespaces */
|
|
885
|
+
isJavaStdlib(importPath) {
|
|
886
|
+
const prefixes = [
|
|
887
|
+
'java.', 'javax.', 'jakarta.',
|
|
888
|
+
'sun.', 'com.sun.', 'jdk.',
|
|
889
|
+
// Android SDK
|
|
890
|
+
'android.', 'androidx.',
|
|
891
|
+
// Common ecosystem (so ubiquitous they're basically stdlib)
|
|
892
|
+
'org.junit.', 'org.slf4j.', 'org.apache.logging.',
|
|
893
|
+
];
|
|
894
|
+
return prefixes.some(p => importPath.startsWith(p));
|
|
895
|
+
}
|
|
896
|
+
/** Kotlin standard library namespaces */
|
|
897
|
+
isKotlinStdlib(importPath) {
|
|
898
|
+
const prefixes = [
|
|
899
|
+
'kotlin.', 'kotlinx.',
|
|
900
|
+
// Java interop (Kotlin can use Java stdlib directly)
|
|
901
|
+
'java.', 'javax.', 'jakarta.',
|
|
902
|
+
];
|
|
903
|
+
return prefixes.some(p => importPath.startsWith(p));
|
|
904
|
+
}
|
|
396
905
|
async loadPackageJson(cwd) {
|
|
397
906
|
try {
|
|
398
907
|
const pkgPath = path.join(cwd, 'package.json');
|