gitnexus 1.4.1 → 1.4.6
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 +215 -194
- package/dist/cli/ai-context.d.ts +2 -1
- package/dist/cli/ai-context.js +117 -90
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +57 -30
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +1 -1
- package/dist/cli/eval-server.js +14 -6
- package/dist/cli/index.js +18 -25
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/setup.js +42 -32
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +3 -2
- package/dist/cli/tool.js +48 -13
- package/dist/cli/wiki.js +2 -2
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -1
- package/dist/core/augmentation/engine.js +99 -72
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +74 -47
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +5 -2
- package/dist/core/ingestion/ast-cache.js +3 -2
- package/dist/core/ingestion/call-processor.d.ts +5 -7
- package/dist/core/ingestion/call-processor.js +430 -283
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
- package/dist/core/ingestion/entry-point-scoring.js +94 -24
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.d.ts +5 -1
- package/dist/core/ingestion/framework-detection.js +48 -8
- package/dist/core/ingestion/heritage-processor.d.ts +13 -5
- package/dist/core/ingestion/heritage-processor.js +109 -55
- package/dist/core/ingestion/import-processor.d.ts +16 -20
- package/dist/core/ingestion/import-processor.js +202 -696
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +3 -11
- package/dist/core/ingestion/parsing-processor.js +85 -181
- package/dist/core/ingestion/pipeline.d.ts +5 -1
- package/dist/core/ingestion/pipeline.js +192 -116
- package/dist/core/ingestion/process-processor.js +2 -1
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/symbol-table.d.ts +21 -1
- package/dist/core/ingestion/symbol-table.js +40 -12
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -11
- package/dist/core/ingestion/tree-sitter-queries.js +642 -485
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +611 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +406 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +449 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
- package/dist/core/ingestion/type-extractors/shared.js +703 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/types.js +1 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +98 -0
- package/dist/core/ingestion/utils.js +1064 -9
- package/dist/core/ingestion/workers/parse-worker.d.ts +38 -4
- package/dist/core/ingestion/workers/parse-worker.js +251 -359
- package/dist/core/ingestion/workers/worker-pool.js +8 -0
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/csv-generator.js +20 -4
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +82 -82
- package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
- package/dist/core/{kuzu → lbug}/schema.js +304 -289
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +17 -16
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +9 -9
- package/dist/core/tree-sitter/parser-loader.js +9 -2
- package/dist/core/wiki/generator.d.ts +4 -52
- package/dist/core/wiki/generator.js +53 -552
- package/dist/core/wiki/graph-queries.d.ts +4 -46
- package/dist/core/wiki/graph-queries.js +103 -282
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/llm-client.js +11 -73
- package/dist/core/wiki/prompts.d.ts +8 -52
- package/dist/core/wiki/prompts.js +86 -200
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -9
- package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +77 -79
- package/dist/mcp/local/local-backend.d.ts +7 -6
- package/dist/mcp/local/local-backend.js +176 -147
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +18 -19
- package/dist/mcp/tools.js +103 -104
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +55 -1
- package/dist/types/pipeline.d.ts +1 -1
- package/hooks/claude/gitnexus-hook.cjs +238 -155
- package/hooks/claude/pre-tool-use.sh +79 -79
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +99 -96
- package/scripts/patch-tree-sitter-swift.cjs +74 -74
- package/skills/gitnexus-cli.md +82 -82
- package/skills/gitnexus-debugging.md +89 -89
- package/skills/gitnexus-exploring.md +78 -78
- package/skills/gitnexus-guide.md +64 -64
- package/skills/gitnexus-impact-analysis.md +97 -97
- package/skills/gitnexus-pr-review.md +163 -163
- package/skills/gitnexus-refactoring.md +121 -121
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
- package/dist/core/wiki/diagrams.d.ts +0 -27
- package/dist/core/wiki/diagrams.js +0 -163
|
@@ -1,16 +1,27 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
1
|
import Parser from 'tree-sitter';
|
|
4
|
-
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
2
|
+
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
5
3
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
6
4
|
import { generateId } from '../../lib/utils.js';
|
|
7
|
-
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
|
|
5
|
+
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js';
|
|
8
6
|
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
7
|
+
import { extractNamedBindings } from './named-binding-extraction.js';
|
|
8
|
+
import { getTreeSitterBufferSize } from './constants.js';
|
|
9
|
+
import { loadTsconfigPaths, loadGoModulePath, loadComposerConfig, loadCSharpProjectConfig, loadSwiftPackageConfig, } from './language-config.js';
|
|
10
|
+
import { buildSuffixIndex, resolveImportPath, appendKotlinWildcard, KOTLIN_EXTENSIONS, resolveJvmWildcard, resolveJvmMemberImport, resolveGoPackageDir, resolveGoPackage, resolveCSharpImport, resolveCSharpNamespaceDir, resolvePhpImport, resolveRustImport, resolveRubyImport, resolvePythonImport, } from './resolvers/index.js';
|
|
11
|
+
import { callRouters } from './call-routing.js';
|
|
9
12
|
const isDev = process.env.NODE_ENV === 'development';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Check if a file path is directly inside a package directory identified by its suffix.
|
|
15
|
+
* Used by the symbol resolver for Go and C# directory-level import matching.
|
|
16
|
+
*/
|
|
17
|
+
export function isFileInPackageDir(filePath, dirSuffix) {
|
|
18
|
+
// Prepend '/' so paths like "internal/auth/service.go" match suffix "/internal/auth/"
|
|
19
|
+
const normalized = '/' + filePath.replace(/\\/g, '/');
|
|
20
|
+
if (!normalized.includes(dirSuffix))
|
|
21
|
+
return false;
|
|
22
|
+
const afterDir = normalized.substring(normalized.indexOf(dirSuffix) + dirSuffix.length);
|
|
23
|
+
return !afterDir.includes('/');
|
|
24
|
+
}
|
|
14
25
|
export function buildImportResolutionContext(allPaths) {
|
|
15
26
|
const allFileList = allPaths;
|
|
16
27
|
const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
|
|
@@ -19,550 +30,159 @@ export function buildImportResolutionContext(allPaths) {
|
|
|
19
30
|
return { allFilePaths, allFileList, normalizedFileList, suffixIndex, resolveCache: new Map() };
|
|
20
31
|
}
|
|
21
32
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
33
|
+
* Shared language dispatch for import resolution.
|
|
34
|
+
* Used by both processImports and processImportsFromExtracted.
|
|
24
35
|
*/
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const baseUrl = compilerOptions.baseUrl || '.';
|
|
38
|
-
const aliases = new Map();
|
|
39
|
-
for (const [pattern, targets] of Object.entries(compilerOptions.paths)) {
|
|
40
|
-
if (!Array.isArray(targets) || targets.length === 0)
|
|
41
|
-
continue;
|
|
42
|
-
const target = targets[0];
|
|
43
|
-
// Convert glob patterns: "@/*" -> "@/", "src/*" -> "src/"
|
|
44
|
-
const aliasPrefix = pattern.endsWith('/*') ? pattern.slice(0, -1) : pattern;
|
|
45
|
-
const targetPrefix = target.endsWith('/*') ? target.slice(0, -1) : target;
|
|
46
|
-
aliases.set(aliasPrefix, targetPrefix);
|
|
36
|
+
function resolveLanguageImport(filePath, rawImportPath, language, configs, ctx) {
|
|
37
|
+
const { allFilePaths, allFileList, normalizedFileList, index, resolveCache } = ctx;
|
|
38
|
+
const { tsconfigPaths, goModule, composerConfig, swiftPackageConfig, csharpConfigs } = configs;
|
|
39
|
+
// JVM languages (Java + Kotlin): handle wildcards and member imports
|
|
40
|
+
if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
|
|
41
|
+
const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
|
|
42
|
+
if (rawImportPath.endsWith('.*')) {
|
|
43
|
+
const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index);
|
|
44
|
+
if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) {
|
|
45
|
+
const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
|
|
46
|
+
if (javaMatches.length > 0)
|
|
47
|
+
return { kind: 'files', files: javaMatches };
|
|
47
48
|
}
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return { aliases, baseUrl };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// File doesn't exist or isn't valid JSON - try next
|
|
49
|
+
if (matchedFiles.length > 0)
|
|
50
|
+
return { kind: 'files', files: matchedFiles };
|
|
51
|
+
// Fall through to standard resolution
|
|
57
52
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* Parse go.mod to extract module path.
|
|
63
|
-
*/
|
|
64
|
-
async function loadGoModulePath(repoRoot) {
|
|
65
|
-
try {
|
|
66
|
-
const goModPath = path.join(repoRoot, 'go.mod');
|
|
67
|
-
const content = await fs.readFile(goModPath, 'utf-8');
|
|
68
|
-
const match = content.match(/^module\s+(\S+)/m);
|
|
69
|
-
if (match) {
|
|
70
|
-
if (isDev) {
|
|
71
|
-
console.log(`📦 Loaded Go module path: ${match[1]}`);
|
|
72
|
-
}
|
|
73
|
-
return { modulePath: match[1] };
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
// No go.mod
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
async function loadComposerConfig(repoRoot) {
|
|
82
|
-
try {
|
|
83
|
-
const composerPath = path.join(repoRoot, 'composer.json');
|
|
84
|
-
const raw = await fs.readFile(composerPath, 'utf-8');
|
|
85
|
-
const composer = JSON.parse(raw);
|
|
86
|
-
const psr4Raw = composer.autoload?.['psr-4'] ?? {};
|
|
87
|
-
const psr4Dev = composer['autoload-dev']?.['psr-4'] ?? {};
|
|
88
|
-
const merged = { ...psr4Raw, ...psr4Dev };
|
|
89
|
-
const psr4 = new Map();
|
|
90
|
-
for (const [ns, dir] of Object.entries(merged)) {
|
|
91
|
-
const nsNorm = ns.replace(/\\+$/, '');
|
|
92
|
-
const dirNorm = dir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
93
|
-
psr4.set(nsNorm, dirNorm);
|
|
94
|
-
}
|
|
95
|
-
if (isDev) {
|
|
96
|
-
console.log(`📦 Loaded ${psr4.size} PSR-4 mappings from composer.json`);
|
|
97
|
-
}
|
|
98
|
-
return { psr4 };
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
async function loadSwiftPackageConfig(repoRoot) {
|
|
105
|
-
// Swift imports are module-name based (e.g., `import SiuperModel`)
|
|
106
|
-
// SPM convention: Sources/<TargetName>/ or Package/Sources/<TargetName>/
|
|
107
|
-
// We scan for these directories to build a target map
|
|
108
|
-
const targets = new Map();
|
|
109
|
-
const sourceDirs = ['Sources', 'Package/Sources', 'src'];
|
|
110
|
-
for (const sourceDir of sourceDirs) {
|
|
111
|
-
try {
|
|
112
|
-
const fullPath = path.join(repoRoot, sourceDir);
|
|
113
|
-
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
114
|
-
for (const entry of entries) {
|
|
115
|
-
if (entry.isDirectory()) {
|
|
116
|
-
targets.set(entry.name, sourceDir + '/' + entry.name);
|
|
117
|
-
}
|
|
53
|
+
else {
|
|
54
|
+
let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index);
|
|
55
|
+
if (!memberResolved && language === SupportedLanguages.Kotlin) {
|
|
56
|
+
memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
|
|
118
57
|
}
|
|
58
|
+
if (memberResolved)
|
|
59
|
+
return { kind: 'files', files: [memberResolved] };
|
|
60
|
+
// Fall through to standard resolution
|
|
119
61
|
}
|
|
120
|
-
catch {
|
|
121
|
-
// Directory doesn't exist
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (targets.size > 0) {
|
|
125
|
-
if (isDev) {
|
|
126
|
-
console.log(`📦 Loaded ${targets.size} Swift package targets`);
|
|
127
|
-
}
|
|
128
|
-
return { targets };
|
|
129
|
-
}
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
// ============================================================================
|
|
133
|
-
// IMPORT PATH RESOLUTION
|
|
134
|
-
// ============================================================================
|
|
135
|
-
/** All file extensions to try during resolution */
|
|
136
|
-
const EXTENSIONS = [
|
|
137
|
-
'',
|
|
138
|
-
// TypeScript/JavaScript
|
|
139
|
-
'.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js',
|
|
140
|
-
// Python
|
|
141
|
-
'.py', '/__init__.py',
|
|
142
|
-
// Java
|
|
143
|
-
'.java',
|
|
144
|
-
// Kotlin
|
|
145
|
-
'.kt', '.kts',
|
|
146
|
-
// C/C++
|
|
147
|
-
'.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh',
|
|
148
|
-
// C#
|
|
149
|
-
'.cs',
|
|
150
|
-
// Go
|
|
151
|
-
'.go',
|
|
152
|
-
// Rust
|
|
153
|
-
'.rs', '/mod.rs',
|
|
154
|
-
// PHP
|
|
155
|
-
'.php', '.phtml',
|
|
156
|
-
// Swift
|
|
157
|
-
'.swift',
|
|
158
|
-
];
|
|
159
|
-
/**
|
|
160
|
-
* Try to match a path (with extensions) against the known file set.
|
|
161
|
-
* Returns the matched file path or null.
|
|
162
|
-
*/
|
|
163
|
-
function tryResolveWithExtensions(basePath, allFiles) {
|
|
164
|
-
for (const ext of EXTENSIONS) {
|
|
165
|
-
const candidate = basePath + ext;
|
|
166
|
-
if (allFiles.has(candidate))
|
|
167
|
-
return candidate;
|
|
168
62
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Map: directory suffix -> list of file paths in that directory
|
|
177
|
-
const dirMap = new Map();
|
|
178
|
-
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
179
|
-
const normalized = normalizedFileList[i];
|
|
180
|
-
const original = allFileList[i];
|
|
181
|
-
const parts = normalized.split('/');
|
|
182
|
-
// Index all suffixes: "a/b/c.java" -> ["c.java", "b/c.java", "a/b/c.java"]
|
|
183
|
-
for (let j = parts.length - 1; j >= 0; j--) {
|
|
184
|
-
const suffix = parts.slice(j).join('/');
|
|
185
|
-
// Only store first match (longest path wins for ambiguous suffixes)
|
|
186
|
-
if (!exactMap.has(suffix)) {
|
|
187
|
-
exactMap.set(suffix, original);
|
|
188
|
-
}
|
|
189
|
-
const lower = suffix.toLowerCase();
|
|
190
|
-
if (!lowerMap.has(lower)) {
|
|
191
|
-
lowerMap.set(lower, original);
|
|
63
|
+
// Go: handle package-level imports
|
|
64
|
+
if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
|
|
65
|
+
const pkgSuffix = resolveGoPackageDir(rawImportPath, goModule);
|
|
66
|
+
if (pkgSuffix) {
|
|
67
|
+
const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
|
|
68
|
+
if (pkgFiles.length > 0) {
|
|
69
|
+
return { kind: 'package', files: pkgFiles, dirSuffix: pkgSuffix };
|
|
192
70
|
}
|
|
193
71
|
}
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const key = `${dirSuffix}:${ext}`;
|
|
204
|
-
let list = dirMap.get(key);
|
|
205
|
-
if (!list) {
|
|
206
|
-
list = [];
|
|
207
|
-
dirMap.set(key, list);
|
|
208
|
-
}
|
|
209
|
-
list.push(original);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
get: (suffix) => exactMap.get(suffix),
|
|
215
|
-
getInsensitive: (suffix) => lowerMap.get(suffix.toLowerCase()),
|
|
216
|
-
getFilesInDir: (dirSuffix, extension) => {
|
|
217
|
-
return dirMap.get(`${dirSuffix}:${extension}`) || [];
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Suffix-based resolution using index. O(1) per lookup instead of O(files).
|
|
223
|
-
*/
|
|
224
|
-
function suffixResolve(pathParts, normalizedFileList, allFileList, index) {
|
|
225
|
-
if (index) {
|
|
226
|
-
for (let i = 0; i < pathParts.length; i++) {
|
|
227
|
-
const suffix = pathParts.slice(i).join('/');
|
|
228
|
-
for (const ext of EXTENSIONS) {
|
|
229
|
-
const suffixWithExt = suffix + ext;
|
|
230
|
-
const result = index.get(suffixWithExt) || index.getInsensitive(suffixWithExt);
|
|
231
|
-
if (result)
|
|
232
|
-
return result;
|
|
72
|
+
// Fall through if no files found (package might be external)
|
|
73
|
+
}
|
|
74
|
+
// C#: handle namespace-based imports (using directives)
|
|
75
|
+
if (language === SupportedLanguages.CSharp && csharpConfigs.length > 0) {
|
|
76
|
+
const resolvedFiles = resolveCSharpImport(rawImportPath, csharpConfigs, normalizedFileList, allFileList, index);
|
|
77
|
+
if (resolvedFiles.length > 1) {
|
|
78
|
+
const dirSuffix = resolveCSharpNamespaceDir(rawImportPath, csharpConfigs);
|
|
79
|
+
if (dirSuffix) {
|
|
80
|
+
return { kind: 'package', files: resolvedFiles, dirSuffix };
|
|
233
81
|
}
|
|
234
82
|
}
|
|
83
|
+
if (resolvedFiles.length > 0)
|
|
84
|
+
return { kind: 'files', files: resolvedFiles };
|
|
235
85
|
return null;
|
|
236
86
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
* Resolve an import path to a file path in the repository.
|
|
253
|
-
*
|
|
254
|
-
* Language-specific preprocessing is applied before the generic resolution:
|
|
255
|
-
* - TypeScript/JavaScript: rewrites tsconfig path aliases
|
|
256
|
-
* - Rust: converts crate::/super::/self:: to relative paths
|
|
257
|
-
*
|
|
258
|
-
* Java wildcards and Go package imports are handled separately in processImports
|
|
259
|
-
* because they resolve to multiple files.
|
|
260
|
-
*/
|
|
261
|
-
const resolveImportPath = (currentFile, importPath, allFiles, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index) => {
|
|
262
|
-
const cacheKey = `${currentFile}::${importPath}`;
|
|
263
|
-
if (resolveCache.has(cacheKey))
|
|
264
|
-
return resolveCache.get(cacheKey) ?? null;
|
|
265
|
-
const cache = (result) => {
|
|
266
|
-
// Evict oldest 20% when cap is reached instead of clearing all
|
|
267
|
-
if (resolveCache.size >= RESOLVE_CACHE_CAP) {
|
|
268
|
-
const evictCount = Math.floor(RESOLVE_CACHE_CAP * 0.2);
|
|
269
|
-
const iter = resolveCache.keys();
|
|
270
|
-
for (let i = 0; i < evictCount; i++) {
|
|
271
|
-
const key = iter.next().value;
|
|
272
|
-
if (key !== undefined)
|
|
273
|
-
resolveCache.delete(key);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
resolveCache.set(cacheKey, result);
|
|
277
|
-
return result;
|
|
278
|
-
};
|
|
279
|
-
// ---- TypeScript/JavaScript: rewrite path aliases ----
|
|
280
|
-
if ((language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) &&
|
|
281
|
-
tsconfigPaths &&
|
|
282
|
-
!importPath.startsWith('.')) {
|
|
283
|
-
for (const [aliasPrefix, targetPrefix] of tsconfigPaths.aliases) {
|
|
284
|
-
if (importPath.startsWith(aliasPrefix)) {
|
|
285
|
-
const remainder = importPath.slice(aliasPrefix.length);
|
|
286
|
-
// Build the rewritten path relative to baseUrl
|
|
287
|
-
const rewritten = tsconfigPaths.baseUrl === '.'
|
|
288
|
-
? targetPrefix + remainder
|
|
289
|
-
: tsconfigPaths.baseUrl + '/' + targetPrefix + remainder;
|
|
290
|
-
// Try direct resolution from repo root
|
|
291
|
-
const resolved = tryResolveWithExtensions(rewritten, allFiles);
|
|
292
|
-
if (resolved)
|
|
293
|
-
return cache(resolved);
|
|
294
|
-
// Try suffix matching as fallback
|
|
295
|
-
const parts = rewritten.split('/').filter(Boolean);
|
|
296
|
-
const suffixResult = suffixResolve(parts, normalizedFileList, allFileList, index);
|
|
297
|
-
if (suffixResult)
|
|
298
|
-
return cache(suffixResult);
|
|
87
|
+
// PHP: handle namespace-based imports (use statements)
|
|
88
|
+
if (language === SupportedLanguages.PHP) {
|
|
89
|
+
const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
|
|
90
|
+
return resolved ? { kind: 'files', files: [resolved] } : null;
|
|
91
|
+
}
|
|
92
|
+
// Swift: handle module imports
|
|
93
|
+
if (language === SupportedLanguages.Swift && swiftPackageConfig) {
|
|
94
|
+
const targetDir = swiftPackageConfig.targets.get(rawImportPath);
|
|
95
|
+
if (targetDir) {
|
|
96
|
+
const dirPrefix = targetDir + '/';
|
|
97
|
+
const files = [];
|
|
98
|
+
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
99
|
+
if (normalizedFileList[i].startsWith(dirPrefix) && normalizedFileList[i].endsWith('.swift')) {
|
|
100
|
+
files.push(allFileList[i]);
|
|
101
|
+
}
|
|
299
102
|
}
|
|
103
|
+
if (files.length > 0)
|
|
104
|
+
return { kind: 'files', files };
|
|
300
105
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
106
|
+
return null; // External framework (Foundation, UIKit, etc.)
|
|
107
|
+
}
|
|
108
|
+
// Python: relative imports (PEP 328) + proximity-based bare imports
|
|
109
|
+
// Falls through to standard suffix resolution when proximity finds no match.
|
|
110
|
+
if (language === SupportedLanguages.Python) {
|
|
111
|
+
const resolved = resolvePythonImport(filePath, rawImportPath, allFilePaths);
|
|
112
|
+
if (resolved)
|
|
113
|
+
return { kind: 'files', files: [resolved] };
|
|
114
|
+
if (rawImportPath.startsWith('.'))
|
|
115
|
+
return null; // relative but unresolved — don't suffix-match
|
|
116
|
+
}
|
|
117
|
+
// Ruby: require / require_relative
|
|
118
|
+
if (language === SupportedLanguages.Ruby) {
|
|
119
|
+
const resolved = resolveRubyImport(rawImportPath, normalizedFileList, allFileList, index);
|
|
120
|
+
return resolved ? { kind: 'files', files: [resolved] } : null;
|
|
121
|
+
}
|
|
122
|
+
// Rust: expand top-level grouped imports: use {crate::a, crate::b}
|
|
123
|
+
if (language === SupportedLanguages.Rust && rawImportPath.startsWith('{') && rawImportPath.endsWith('}')) {
|
|
124
|
+
const inner = rawImportPath.slice(1, -1);
|
|
125
|
+
const parts = inner.split(',').map(p => p.trim()).filter(Boolean);
|
|
126
|
+
const resolved = [];
|
|
127
|
+
for (const part of parts) {
|
|
128
|
+
const r = resolveRustImport(filePath, part, allFilePaths);
|
|
129
|
+
if (r)
|
|
130
|
+
resolved.push(r);
|
|
320
131
|
}
|
|
132
|
+
return resolved.length > 0 ? { kind: 'files', files: resolved } : null;
|
|
321
133
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return cache(resolved);
|
|
326
|
-
}
|
|
327
|
-
// ---- Generic package/absolute import resolution (suffix matching) ----
|
|
328
|
-
// Java wildcards are handled in processImports, not here
|
|
329
|
-
if (importPath.endsWith('.*')) {
|
|
330
|
-
return cache(null);
|
|
331
|
-
}
|
|
332
|
-
const pathLike = importPath.includes('/')
|
|
333
|
-
? importPath
|
|
334
|
-
: importPath.replace(/\./g, '/');
|
|
335
|
-
const pathParts = pathLike.split('/').filter(Boolean);
|
|
336
|
-
const resolved = suffixResolve(pathParts, normalizedFileList, allFileList, index);
|
|
337
|
-
return cache(resolved);
|
|
338
|
-
};
|
|
339
|
-
// ============================================================================
|
|
340
|
-
// RUST MODULE RESOLUTION
|
|
341
|
-
// ============================================================================
|
|
342
|
-
/**
|
|
343
|
-
* Resolve Rust use-path to a file.
|
|
344
|
-
* Handles crate::, super::, self:: prefixes and :: path separators.
|
|
345
|
-
*/
|
|
346
|
-
function resolveRustImport(currentFile, importPath, allFiles) {
|
|
347
|
-
let rustPath;
|
|
348
|
-
if (importPath.startsWith('crate::')) {
|
|
349
|
-
// crate:: resolves from src/ directory (standard Rust layout)
|
|
350
|
-
rustPath = importPath.slice(7).replace(/::/g, '/');
|
|
351
|
-
// Try from src/ (standard layout)
|
|
352
|
-
const fromSrc = tryRustModulePath('src/' + rustPath, allFiles);
|
|
353
|
-
if (fromSrc)
|
|
354
|
-
return fromSrc;
|
|
355
|
-
// Try from repo root (non-standard)
|
|
356
|
-
const fromRoot = tryRustModulePath(rustPath, allFiles);
|
|
357
|
-
if (fromRoot)
|
|
358
|
-
return fromRoot;
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
if (importPath.startsWith('super::')) {
|
|
362
|
-
// super:: = parent directory of current file's module
|
|
363
|
-
const currentDir = currentFile.split('/').slice(0, -1);
|
|
364
|
-
currentDir.pop(); // Go up one level for super::
|
|
365
|
-
rustPath = importPath.slice(7).replace(/::/g, '/');
|
|
366
|
-
const fullPath = [...currentDir, rustPath].join('/');
|
|
367
|
-
return tryRustModulePath(fullPath, allFiles);
|
|
368
|
-
}
|
|
369
|
-
if (importPath.startsWith('self::')) {
|
|
370
|
-
// self:: = current module's directory
|
|
371
|
-
const currentDir = currentFile.split('/').slice(0, -1);
|
|
372
|
-
rustPath = importPath.slice(6).replace(/::/g, '/');
|
|
373
|
-
const fullPath = [...currentDir, rustPath].join('/');
|
|
374
|
-
return tryRustModulePath(fullPath, allFiles);
|
|
375
|
-
}
|
|
376
|
-
// Bare path without prefix (e.g., from a use in a nested module)
|
|
377
|
-
// Convert :: to / and try suffix matching
|
|
378
|
-
if (importPath.includes('::')) {
|
|
379
|
-
rustPath = importPath.replace(/::/g, '/');
|
|
380
|
-
return tryRustModulePath(rustPath, allFiles);
|
|
381
|
-
}
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Try to resolve a Rust module path to a file.
|
|
386
|
-
* Tries: path.rs, path/mod.rs, and with the last segment stripped
|
|
387
|
-
* (last segment might be a symbol name, not a module).
|
|
388
|
-
*/
|
|
389
|
-
function tryRustModulePath(modulePath, allFiles) {
|
|
390
|
-
// Try direct: path.rs
|
|
391
|
-
if (allFiles.has(modulePath + '.rs'))
|
|
392
|
-
return modulePath + '.rs';
|
|
393
|
-
// Try directory: path/mod.rs
|
|
394
|
-
if (allFiles.has(modulePath + '/mod.rs'))
|
|
395
|
-
return modulePath + '/mod.rs';
|
|
396
|
-
// Try path/lib.rs (for crate root)
|
|
397
|
-
if (allFiles.has(modulePath + '/lib.rs'))
|
|
398
|
-
return modulePath + '/lib.rs';
|
|
399
|
-
// The last segment might be a symbol (function, struct, etc.), not a module.
|
|
400
|
-
// Strip it and try again.
|
|
401
|
-
const lastSlash = modulePath.lastIndexOf('/');
|
|
402
|
-
if (lastSlash > 0) {
|
|
403
|
-
const parentPath = modulePath.substring(0, lastSlash);
|
|
404
|
-
if (allFiles.has(parentPath + '.rs'))
|
|
405
|
-
return parentPath + '.rs';
|
|
406
|
-
if (allFiles.has(parentPath + '/mod.rs'))
|
|
407
|
-
return parentPath + '/mod.rs';
|
|
408
|
-
}
|
|
409
|
-
return null;
|
|
134
|
+
// Standard single-file resolution
|
|
135
|
+
const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
|
|
136
|
+
return resolvedPath ? { kind: 'files', files: [resolvedPath] } : null;
|
|
410
137
|
}
|
|
411
138
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
139
|
+
* Apply an ImportResult: emit graph edges and update ImportMap/PackageMap.
|
|
140
|
+
* If namedBindings are provided and the import resolves to a single file,
|
|
141
|
+
* also populate the NamedImportMap for precise Tier 2a resolution.
|
|
414
142
|
*/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
143
|
+
function applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, namedBindings, namedImportMap) {
|
|
144
|
+
if (!result)
|
|
145
|
+
return;
|
|
146
|
+
if (result.kind === 'package' && packageMap) {
|
|
147
|
+
// Store directory suffix in PackageMap (skip ImportMap expansion)
|
|
148
|
+
for (const resolvedFile of result.files) {
|
|
149
|
+
addImportGraphEdge(filePath, resolvedFile);
|
|
419
150
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
* Resolve a JVM wildcard import (com.example.*) to all matching files.
|
|
430
|
-
* Works for both Java (.java) and Kotlin (.kt, .kts).
|
|
431
|
-
*/
|
|
432
|
-
function resolveJvmWildcard(importPath, normalizedFileList, allFileList, extensions, index) {
|
|
433
|
-
// "com.example.util.*" -> "com/example/util"
|
|
434
|
-
const packagePath = importPath.slice(0, -2).replace(/\./g, '/');
|
|
435
|
-
if (index) {
|
|
436
|
-
const candidates = extensions.flatMap(ext => index.getFilesInDir(packagePath, ext));
|
|
437
|
-
// Filter to only direct children (no subdirectories)
|
|
438
|
-
const packageSuffix = '/' + packagePath + '/';
|
|
439
|
-
return candidates.filter(f => {
|
|
440
|
-
const normalized = f.replace(/\\/g, '/');
|
|
441
|
-
const idx = normalized.indexOf(packageSuffix);
|
|
442
|
-
if (idx < 0)
|
|
443
|
-
return false;
|
|
444
|
-
const afterPkg = normalized.substring(idx + packageSuffix.length);
|
|
445
|
-
return !afterPkg.includes('/');
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
// Fallback: linear scan
|
|
449
|
-
const packageSuffix = '/' + packagePath + '/';
|
|
450
|
-
const matches = [];
|
|
451
|
-
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
452
|
-
const normalized = normalizedFileList[i];
|
|
453
|
-
if (normalized.includes(packageSuffix) &&
|
|
454
|
-
extensions.some(ext => normalized.endsWith(ext))) {
|
|
455
|
-
const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length);
|
|
456
|
-
if (!afterPackage.includes('/')) {
|
|
457
|
-
matches.push(allFileList[i]);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return matches;
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Try to resolve a JVM member/static import by stripping the member name.
|
|
465
|
-
* Java: "com.example.Constants.VALUE" -> resolve "com.example.Constants"
|
|
466
|
-
* Kotlin: "com.example.Constants.VALUE" -> resolve "com.example.Constants"
|
|
467
|
-
*/
|
|
468
|
-
function resolveJvmMemberImport(importPath, normalizedFileList, allFileList, extensions, index) {
|
|
469
|
-
// Member imports: com.example.Constants.VALUE or com.example.Constants.*
|
|
470
|
-
// The last segment is a member name if it starts with lowercase, is ALL_CAPS, or is a wildcard
|
|
471
|
-
const segments = importPath.split('.');
|
|
472
|
-
if (segments.length < 3)
|
|
473
|
-
return null;
|
|
474
|
-
const lastSeg = segments[segments.length - 1];
|
|
475
|
-
if (lastSeg === '*' || /^[a-z]/.test(lastSeg) || /^[A-Z_]+$/.test(lastSeg)) {
|
|
476
|
-
const classPath = segments.slice(0, -1).join('/');
|
|
477
|
-
for (const ext of extensions) {
|
|
478
|
-
const classSuffix = classPath + ext;
|
|
479
|
-
if (index) {
|
|
480
|
-
const result = index.get(classSuffix) || index.getInsensitive(classSuffix);
|
|
481
|
-
if (result)
|
|
482
|
-
return result;
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
const fullSuffix = '/' + classSuffix;
|
|
486
|
-
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
487
|
-
if (normalizedFileList[i].endsWith(fullSuffix) ||
|
|
488
|
-
normalizedFileList[i].toLowerCase().endsWith(fullSuffix.toLowerCase())) {
|
|
489
|
-
return allFileList[i];
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return null;
|
|
496
|
-
}
|
|
497
|
-
// ============================================================================
|
|
498
|
-
// GO PACKAGE RESOLUTION
|
|
499
|
-
// ============================================================================
|
|
500
|
-
/**
|
|
501
|
-
* Resolve a Go internal package import to all .go files in the package directory.
|
|
502
|
-
* Returns an array of file paths.
|
|
503
|
-
*/
|
|
504
|
-
function resolveGoPackage(importPath, goModule, normalizedFileList, allFileList) {
|
|
505
|
-
if (!importPath.startsWith(goModule.modulePath))
|
|
506
|
-
return [];
|
|
507
|
-
// Strip module path to get relative package path
|
|
508
|
-
const relativePkg = importPath.slice(goModule.modulePath.length + 1); // e.g., "internal/auth"
|
|
509
|
-
if (!relativePkg)
|
|
510
|
-
return [];
|
|
511
|
-
const pkgSuffix = '/' + relativePkg + '/';
|
|
512
|
-
const matches = [];
|
|
513
|
-
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
514
|
-
const normalized = normalizedFileList[i];
|
|
515
|
-
// File must be directly in the package directory (not a subdirectory)
|
|
516
|
-
if (normalized.includes(pkgSuffix) && normalized.endsWith('.go') && !normalized.endsWith('_test.go')) {
|
|
517
|
-
const afterPkg = normalized.substring(normalized.indexOf(pkgSuffix) + pkgSuffix.length);
|
|
518
|
-
if (!afterPkg.includes('/')) {
|
|
519
|
-
matches.push(allFileList[i]);
|
|
520
|
-
}
|
|
151
|
+
if (!packageMap.has(filePath))
|
|
152
|
+
packageMap.set(filePath, new Set());
|
|
153
|
+
packageMap.get(filePath).add(result.dirSuffix);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// 'files' kind, or 'package' without PackageMap — use ImportMap directly
|
|
157
|
+
const files = result.files;
|
|
158
|
+
for (const resolvedFile of files) {
|
|
159
|
+
addImportEdge(filePath, resolvedFile);
|
|
521
160
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
* e.g. "App\Http\Controllers\UserController" -> "app/Http/Controllers/UserController.php"
|
|
531
|
-
*/
|
|
532
|
-
function resolvePhpImport(importPath, composerConfig, allFiles, normalizedFileList, allFileList, index) {
|
|
533
|
-
// Normalize: replace backslashes with forward slashes
|
|
534
|
-
const normalized = importPath.replace(/\\/g, '/');
|
|
535
|
-
// Try PSR-4 resolution if composer.json was found
|
|
536
|
-
if (composerConfig) {
|
|
537
|
-
// Sort namespaces by length descending (longest match wins)
|
|
538
|
-
const sorted = [...composerConfig.psr4.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
539
|
-
for (const [nsPrefix, dirPrefix] of sorted) {
|
|
540
|
-
const nsPrefixSlash = nsPrefix.replace(/\\/g, '/');
|
|
541
|
-
if (normalized.startsWith(nsPrefixSlash + '/') || normalized === nsPrefixSlash) {
|
|
542
|
-
const remainder = normalized.slice(nsPrefixSlash.length).replace(/^\//, '');
|
|
543
|
-
const filePath = dirPrefix + (remainder ? '/' + remainder : '') + '.php';
|
|
544
|
-
if (allFiles.has(filePath))
|
|
545
|
-
return filePath;
|
|
546
|
-
if (index) {
|
|
547
|
-
const result = index.getInsensitive(filePath);
|
|
548
|
-
if (result)
|
|
549
|
-
return result;
|
|
550
|
-
}
|
|
161
|
+
// Record named bindings for precise Tier 2a resolution
|
|
162
|
+
if (namedBindings && namedImportMap && files.length === 1) {
|
|
163
|
+
const resolvedFile = files[0];
|
|
164
|
+
if (!namedImportMap.has(filePath))
|
|
165
|
+
namedImportMap.set(filePath, new Map());
|
|
166
|
+
const fileBindings = namedImportMap.get(filePath);
|
|
167
|
+
for (const binding of namedBindings) {
|
|
168
|
+
fileBindings.set(binding.local, { sourcePath: resolvedFile, exportedName: binding.exported });
|
|
551
169
|
}
|
|
552
170
|
}
|
|
553
171
|
}
|
|
554
|
-
// Fallback: suffix matching (works without composer.json)
|
|
555
|
-
const pathParts = normalized.split('/').filter(Boolean);
|
|
556
|
-
return suffixResolve(pathParts, normalizedFileList, allFileList, index);
|
|
557
172
|
}
|
|
558
173
|
// ============================================================================
|
|
559
174
|
// MAIN IMPORT PROCESSOR
|
|
560
175
|
// ============================================================================
|
|
561
|
-
export const processImports = async (graph, files, astCache,
|
|
176
|
+
export const processImports = async (graph, files, astCache, ctx, onProgress, repoRoot, allPaths) => {
|
|
177
|
+
const importMap = ctx.importMap;
|
|
178
|
+
const packageMap = ctx.packageMap;
|
|
179
|
+
const namedImportMap = ctx.namedImportMap;
|
|
562
180
|
// Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
|
|
563
181
|
const allFileList = allPaths ?? files.map(f => f.path);
|
|
564
182
|
const allFilePaths = new Set(allFileList);
|
|
565
183
|
const parser = await loadParser();
|
|
184
|
+
const logSkipped = isVerboseIngestionEnabled();
|
|
185
|
+
const skippedByLang = logSkipped ? new Map() : null;
|
|
566
186
|
const resolveCache = new Map();
|
|
567
187
|
// Pre-compute normalized file list once (forward slashes)
|
|
568
188
|
const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
|
|
@@ -573,12 +193,16 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
573
193
|
let totalImportsResolved = 0;
|
|
574
194
|
// Load language-specific configs once before the file loop
|
|
575
195
|
const effectiveRoot = repoRoot || '';
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
196
|
+
const configs = {
|
|
197
|
+
tsconfigPaths: await loadTsconfigPaths(effectiveRoot),
|
|
198
|
+
goModule: await loadGoModulePath(effectiveRoot),
|
|
199
|
+
composerConfig: await loadComposerConfig(effectiveRoot),
|
|
200
|
+
swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
|
|
201
|
+
csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
|
|
202
|
+
};
|
|
203
|
+
const resolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
|
|
204
|
+
// Helper: add an IMPORTS edge to the graph only (no ImportMap update)
|
|
205
|
+
const addImportGraphEdge = (filePath, resolvedPath) => {
|
|
582
206
|
const sourceId = generateId('File', filePath);
|
|
583
207
|
const targetId = generateId('File', resolvedPath);
|
|
584
208
|
const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
|
|
@@ -591,6 +215,10 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
591
215
|
confidence: 1.0,
|
|
592
216
|
reason: '',
|
|
593
217
|
});
|
|
218
|
+
};
|
|
219
|
+
// Helper: add an IMPORTS edge + update import map
|
|
220
|
+
const addImportEdge = (filePath, resolvedPath) => {
|
|
221
|
+
addImportGraphEdge(filePath, resolvedPath);
|
|
594
222
|
if (!importMap.has(filePath)) {
|
|
595
223
|
importMap.set(filePath, new Set());
|
|
596
224
|
}
|
|
@@ -605,6 +233,12 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
605
233
|
const language = getLanguageFromFilename(file.path);
|
|
606
234
|
if (!language)
|
|
607
235
|
continue;
|
|
236
|
+
if (!isLanguageAvailable(language)) {
|
|
237
|
+
if (skippedByLang) {
|
|
238
|
+
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
608
242
|
const queryStr = LANGUAGE_QUERIES[language];
|
|
609
243
|
if (!queryStr)
|
|
610
244
|
continue;
|
|
@@ -615,7 +249,7 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
615
249
|
let wasReparsed = false;
|
|
616
250
|
if (!tree) {
|
|
617
251
|
try {
|
|
618
|
-
tree = parser.parse(file.content, undefined, { bufferSize:
|
|
252
|
+
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
|
|
619
253
|
}
|
|
620
254
|
catch (parseError) {
|
|
621
255
|
continue;
|
|
@@ -662,83 +296,31 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
662
296
|
? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
|
|
663
297
|
: sourceNode.text.replace(/['"<>]/g, '');
|
|
664
298
|
totalImportsFound++;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
for (const matchedFile of matchedFiles) {
|
|
680
|
-
addImportEdge(file.path, matchedFile);
|
|
681
|
-
}
|
|
682
|
-
return; // skip single-file resolution
|
|
683
|
-
}
|
|
684
|
-
// Try member/static import resolution (strip member name)
|
|
685
|
-
let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index);
|
|
686
|
-
// Kotlin can import Java files in mixed codebases — try .java as fallback
|
|
687
|
-
if (!memberResolved && language === SupportedLanguages.Kotlin) {
|
|
688
|
-
memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
|
|
689
|
-
}
|
|
690
|
-
if (memberResolved) {
|
|
691
|
-
addImportEdge(file.path, memberResolved);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
// Fall through to normal resolution for regular imports
|
|
695
|
-
}
|
|
696
|
-
// ---- Go: handle package-level imports ----
|
|
697
|
-
if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
|
|
698
|
-
const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
|
|
699
|
-
if (pkgFiles.length > 0) {
|
|
700
|
-
for (const pkgFile of pkgFiles) {
|
|
701
|
-
addImportEdge(file.path, pkgFile);
|
|
702
|
-
}
|
|
703
|
-
return; // skip single-file resolution
|
|
704
|
-
}
|
|
705
|
-
// Fall through if no files found (package might be external)
|
|
706
|
-
}
|
|
707
|
-
// ---- PHP: handle namespace-based imports (use statements) ----
|
|
708
|
-
if (language === SupportedLanguages.PHP) {
|
|
709
|
-
const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
|
|
710
|
-
if (resolved) {
|
|
711
|
-
addImportEdge(file.path, resolved);
|
|
712
|
-
}
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
// ---- Swift: handle module imports ----
|
|
716
|
-
if (language === SupportedLanguages.Swift && swiftPackageConfig) {
|
|
717
|
-
// Swift imports are module names: `import SiuperModel`
|
|
718
|
-
// Resolve to the module's source directory → all .swift files in it
|
|
719
|
-
const targetDir = swiftPackageConfig.targets.get(rawImportPath);
|
|
720
|
-
if (targetDir) {
|
|
721
|
-
// Find all .swift files in this target directory
|
|
722
|
-
const dirPrefix = targetDir + '/';
|
|
723
|
-
for (const filePath2 of allFileList) {
|
|
724
|
-
if (filePath2.startsWith(dirPrefix) && filePath2.endsWith('.swift')) {
|
|
725
|
-
addImportEdge(file.path, filePath2);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
return;
|
|
299
|
+
const result = resolveLanguageImport(file.path, rawImportPath, language, configs, resolveCtx);
|
|
300
|
+
const bindings = namedImportMap ? extractNamedBindings(captureMap['import'], language) : undefined;
|
|
301
|
+
applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap);
|
|
302
|
+
}
|
|
303
|
+
// ---- Language-specific call-as-import routing (Ruby require, etc.) ----
|
|
304
|
+
if (captureMap['call']) {
|
|
305
|
+
const callNameNode = captureMap['call.name'];
|
|
306
|
+
if (callNameNode) {
|
|
307
|
+
const callRouter = callRouters[language];
|
|
308
|
+
const routed = callRouter(callNameNode.text, captureMap['call']);
|
|
309
|
+
if (routed && routed.kind === 'import') {
|
|
310
|
+
totalImportsFound++;
|
|
311
|
+
const result = resolveLanguageImport(file.path, routed.importPath, language, configs, resolveCtx);
|
|
312
|
+
applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge);
|
|
729
313
|
}
|
|
730
|
-
// External framework (Foundation, UIKit, etc.) — skip
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
// ---- Standard single-file resolution ----
|
|
734
|
-
const resolvedPath = resolveImportPath(file.path, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
|
|
735
|
-
if (resolvedPath) {
|
|
736
|
-
addImportEdge(file.path, resolvedPath);
|
|
737
314
|
}
|
|
738
315
|
}
|
|
739
316
|
});
|
|
740
317
|
// Tree is now owned by the LRU cache — no manual delete needed
|
|
741
318
|
}
|
|
319
|
+
if (skippedByLang && skippedByLang.size > 0) {
|
|
320
|
+
for (const [lang, count] of skippedByLang.entries()) {
|
|
321
|
+
console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in import processing — ${lang} parser not available.`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
742
324
|
if (isDev) {
|
|
743
325
|
console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
|
|
744
326
|
}
|
|
@@ -746,17 +328,25 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
746
328
|
// ============================================================================
|
|
747
329
|
// FAST PATH: Resolve pre-extracted imports (no parsing needed)
|
|
748
330
|
// ============================================================================
|
|
749
|
-
export const processImportsFromExtracted = async (graph, files, extractedImports,
|
|
750
|
-
const
|
|
751
|
-
const
|
|
331
|
+
export const processImportsFromExtracted = async (graph, files, extractedImports, ctx, onProgress, repoRoot, prebuiltCtx) => {
|
|
332
|
+
const importMap = ctx.importMap;
|
|
333
|
+
const packageMap = ctx.packageMap;
|
|
334
|
+
const namedImportMap = ctx.namedImportMap;
|
|
335
|
+
const importCtx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
|
|
336
|
+
const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = importCtx;
|
|
752
337
|
let totalImportsFound = 0;
|
|
753
338
|
let totalImportsResolved = 0;
|
|
754
339
|
const effectiveRoot = repoRoot || '';
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
340
|
+
const configs = {
|
|
341
|
+
tsconfigPaths: await loadTsconfigPaths(effectiveRoot),
|
|
342
|
+
goModule: await loadGoModulePath(effectiveRoot),
|
|
343
|
+
composerConfig: await loadComposerConfig(effectiveRoot),
|
|
344
|
+
swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
|
|
345
|
+
csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
|
|
346
|
+
};
|
|
347
|
+
const resolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
|
|
348
|
+
// Helper: add an IMPORTS edge to the graph only (no ImportMap update)
|
|
349
|
+
const addImportGraphEdge = (filePath, resolvedPath) => {
|
|
760
350
|
const sourceId = generateId('File', filePath);
|
|
761
351
|
const targetId = generateId('File', resolvedPath);
|
|
762
352
|
const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
|
|
@@ -769,6 +359,9 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
|
|
|
769
359
|
confidence: 1.0,
|
|
770
360
|
reason: '',
|
|
771
361
|
});
|
|
362
|
+
};
|
|
363
|
+
const addImportEdge = (filePath, resolvedPath) => {
|
|
364
|
+
addImportGraphEdge(filePath, resolvedPath);
|
|
772
365
|
if (!importMap.has(filePath)) {
|
|
773
366
|
importMap.set(filePath, new Set());
|
|
774
367
|
}
|
|
@@ -786,103 +379,16 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
|
|
|
786
379
|
}
|
|
787
380
|
const totalFiles = importsByFile.size;
|
|
788
381
|
let filesProcessed = 0;
|
|
789
|
-
// Pre-build a suffix index for O(1) suffix lookups instead of O(n) linear scans
|
|
790
|
-
const suffixIndex = new Map();
|
|
791
|
-
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
792
|
-
const normalized = normalizedFileList[i];
|
|
793
|
-
// Index by last path segment (filename) for fast suffix matching
|
|
794
|
-
const lastSlash = normalized.lastIndexOf('/');
|
|
795
|
-
const filename = lastSlash >= 0 ? normalized.substring(lastSlash + 1) : normalized;
|
|
796
|
-
let list = suffixIndex.get(filename);
|
|
797
|
-
if (!list) {
|
|
798
|
-
list = [];
|
|
799
|
-
suffixIndex.set(filename, list);
|
|
800
|
-
}
|
|
801
|
-
list.push(allFileList[i]);
|
|
802
|
-
}
|
|
803
382
|
for (const [filePath, fileImports] of importsByFile) {
|
|
804
383
|
filesProcessed++;
|
|
805
384
|
if (filesProcessed % 100 === 0) {
|
|
806
385
|
onProgress?.(filesProcessed, totalFiles);
|
|
807
386
|
await yieldToEventLoop();
|
|
808
387
|
}
|
|
809
|
-
for (const
|
|
388
|
+
for (const imp of fileImports) {
|
|
810
389
|
totalImportsFound++;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (resolveCache.has(cacheKey)) {
|
|
814
|
-
const cached = resolveCache.get(cacheKey);
|
|
815
|
-
if (cached)
|
|
816
|
-
addImportEdge(filePath, cached);
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
// JVM languages (Java + Kotlin): handle wildcards and member imports
|
|
820
|
-
if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
|
|
821
|
-
const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
|
|
822
|
-
if (rawImportPath.endsWith('.*')) {
|
|
823
|
-
const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index);
|
|
824
|
-
// Kotlin can import Java files in mixed codebases — try .java as fallback
|
|
825
|
-
if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) {
|
|
826
|
-
const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
|
|
827
|
-
for (const matchedFile of javaMatches) {
|
|
828
|
-
addImportEdge(filePath, matchedFile);
|
|
829
|
-
}
|
|
830
|
-
if (javaMatches.length > 0)
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
for (const matchedFile of matchedFiles) {
|
|
834
|
-
addImportEdge(filePath, matchedFile);
|
|
835
|
-
}
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index);
|
|
839
|
-
// Kotlin can import Java files in mixed codebases — try .java as fallback
|
|
840
|
-
if (!memberResolved && language === SupportedLanguages.Kotlin) {
|
|
841
|
-
memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
|
|
842
|
-
}
|
|
843
|
-
if (memberResolved) {
|
|
844
|
-
resolveCache.set(cacheKey, memberResolved);
|
|
845
|
-
addImportEdge(filePath, memberResolved);
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
// Go: handle package-level imports
|
|
850
|
-
if (language === SupportedLanguages.Go && goModule && rawImportPath.startsWith(goModule.modulePath)) {
|
|
851
|
-
const pkgFiles = resolveGoPackage(rawImportPath, goModule, normalizedFileList, allFileList);
|
|
852
|
-
if (pkgFiles.length > 0) {
|
|
853
|
-
for (const pkgFile of pkgFiles) {
|
|
854
|
-
addImportEdge(filePath, pkgFile);
|
|
855
|
-
}
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
// PHP: handle namespace-based imports (use statements)
|
|
860
|
-
if (language === SupportedLanguages.PHP) {
|
|
861
|
-
const resolved = resolvePhpImport(rawImportPath, composerConfig, allFilePaths, normalizedFileList, allFileList, index);
|
|
862
|
-
if (resolved) {
|
|
863
|
-
resolveCache.set(cacheKey, resolved);
|
|
864
|
-
addImportEdge(filePath, resolved);
|
|
865
|
-
}
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
// Swift: handle module imports
|
|
869
|
-
if (language === SupportedLanguages.Swift && swiftPackageConfig) {
|
|
870
|
-
const targetDir = swiftPackageConfig.targets.get(rawImportPath);
|
|
871
|
-
if (targetDir) {
|
|
872
|
-
const dirPrefix = targetDir + '/';
|
|
873
|
-
for (const fp of allFileList) {
|
|
874
|
-
if (fp.startsWith(dirPrefix) && fp.endsWith('.swift')) {
|
|
875
|
-
addImportEdge(filePath, fp);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
// Standard resolution (has its own internal cache)
|
|
882
|
-
const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
|
|
883
|
-
if (resolvedPath) {
|
|
884
|
-
addImportEdge(filePath, resolvedPath);
|
|
885
|
-
}
|
|
390
|
+
const result = resolveLanguageImport(filePath, imp.rawImportPath, imp.language, configs, resolveCtx);
|
|
391
|
+
applyImportResult(result, filePath, importMap, packageMap, addImportEdge, addImportGraphEdge, imp.namedBindings, namedImportMap);
|
|
886
392
|
}
|
|
887
393
|
}
|
|
888
394
|
onProgress?.(totalFiles, totalFiles);
|