@rigour-labs/core 3.0.2 → 3.0.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.
@@ -6,17 +6,18 @@
6
6
  * statements for packages, files, or modules that were never installed
7
7
  * or created.
8
8
  *
9
- * Detection strategy:
10
- * 1. Parse all import/require statements
11
- * 2. For relative imports: verify the target file exists
12
- * 3. For package imports: verify the package exists in node_modules or package.json
13
- * 4. For Python imports: verify the module exists in the project or site-packages
14
- * 5. For Go imports: verify relative package paths exist in the project
15
- * 6. For Ruby/C#: verify relative require/using paths exist
16
- *
17
- * Supported languages: JS/TS, Python, Go, Ruby, C#
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', 'buffer', 'child_process', 'cluster', 'console', 'constants',
239
- 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2',
240
- 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',
241
- 'process', 'punycode', 'querystring', 'readline', 'repl', 'stream',
242
- 'string_decoder', 'sys', 'timers', 'tls', 'trace_events', 'tty',
243
- 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib',
244
- 'node:assert', 'node:buffer', 'node:child_process', 'node:cluster',
245
- 'node:console', 'node:constants', 'node:crypto', 'node:dgram',
246
- 'node:dns', 'node:domain', 'node:events', 'node:fs', 'node:http',
247
- 'node:http2', 'node:https', 'node:inspector', 'node:module', 'node:net',
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) || name.startsWith('node:');
265
+ return builtins.has(name);
256
266
  }
257
267
  isPythonStdlib(modulePath) {
258
268
  const topLevel = modulePath.split('.')[0];
@@ -462,13 +472,24 @@ export class HallucinatedImportsGate extends Gate {
462
472
  return knownStdlibPaths.has(importPath);
463
473
  }
464
474
  /**
465
- * Check Ruby imports — verify require_relative paths exist
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
466
482
  */
467
- checkRubyImports(content, file, projectFiles, hallucinated) {
483
+ checkRubyImports(content, file, cwd, projectFiles, hallucinated) {
468
484
  const lines = content.split('\n');
485
+ // Parse Gemfile for known gem dependencies
486
+ const gemDeps = this.loadRubyGems(cwd);
469
487
  for (let i = 0; i < lines.length; i++) {
470
488
  const line = lines[i].trim();
471
- // require_relative 'path' — should resolve to a real file
489
+ // Skip comments
490
+ if (line.startsWith('#'))
491
+ continue;
492
+ // require_relative 'path' — must resolve to a real file
472
493
  const relMatch = line.match(/require_relative\s+['"]([^'"]+)['"]/);
473
494
  if (relMatch) {
474
495
  const reqPath = relMatch[1];
@@ -481,43 +502,406 @@ export class HallucinatedImportsGate extends Gate {
481
502
  reason: `require_relative '${reqPath}' — file not found in project`,
482
503
  });
483
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
+ }
484
535
  }
485
536
  }
486
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
+ }
566
+ }
567
+ }
568
+ catch { /* no Gemfile */ }
569
+ return gems;
570
+ }
571
+ /**
572
+ * Ruby standard library — covers Ruby 3.3+ (MRI)
573
+ * Includes both the default gems and bundled gems that ship with Ruby.
574
+ */
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
+ }
487
605
  /**
488
- * Check C# imports — verify relative using paths match project namespaces
489
- * (C# uses namespaces, not file paths — we check for obviously wrong namespaces)
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
490
614
  */
491
- checkCSharpImports(content, file, projectFiles, hallucinated) {
615
+ checkCSharpImports(content, file, cwd, projectFiles, hallucinated) {
492
616
  const lines = content.split('\n');
617
+ const nugetPackages = this.loadNuGetPackages(cwd);
493
618
  for (let i = 0; i < lines.length; i++) {
494
619
  const line = lines[i].trim();
495
- // using ProjectName.Something check if namespace maps to project files
496
- const usingMatch = line.match(/^using\s+([\w.]+)\s*;/);
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*;/);
497
623
  if (!usingMatch)
498
624
  continue;
499
625
  const namespace = usingMatch[1];
500
- // Skip System.* and Microsoft.* and common framework namespaces
501
- if (/^(?:System|Microsoft|Newtonsoft|NUnit|Xunit|Moq|AutoMapper)\b/.test(namespace))
626
+ // 1. Skip .NET framework and BCL namespaces
627
+ if (this.isDotNetFramework(namespace))
502
628
  continue;
503
- // Check if the namespace maps to any .cs file path in the project
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('.')))
632
+ continue;
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('.');
504
637
  const nsPath = namespace.replace(/\./g, '/');
505
- const hasMatch = [...projectFiles].some(f => f.endsWith('.cs') && (f.includes(nsPath) || f.includes(namespace.split('.')[0])));
506
- // Only flag if the project has NO files that could match this namespace
507
- if (!hasMatch && namespace.includes('.')) {
508
- // Could be a NuGet package we can't verify without .csproj parsing
509
- // Only flag obvious project-relative namespaces
510
- const topLevel = namespace.split('.')[0];
511
- const hasProjectFiles = [...projectFiles].some(f => f.endsWith('.cs') && f.includes(topLevel));
512
- if (hasProjectFiles) {
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) {
513
647
  hallucinated.push({
514
648
  file, line: i + 1, importPath: namespace, type: 'csharp',
515
- reason: `Namespace '${namespace}' — no matching files found in project`,
649
+ reason: `Namespace '${namespace}' — no matching files in project, not in NuGet packages`,
650
+ });
651
+ }
652
+ }
653
+ }
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`,
516
848
  });
517
849
  }
518
850
  }
519
851
  }
520
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
+ }
521
905
  async loadPackageJson(cwd) {
522
906
  try {
523
907
  const pkgPath = path.join(cwd, 'package.json');