gitnexus 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -214
- package/dist/cli/ai-context.d.ts +1 -2
- package/dist/cli/ai-context.js +90 -117
- package/dist/cli/analyze.d.ts +0 -2
- package/dist/cli/analyze.js +2 -20
- package/dist/cli/index.js +25 -17
- package/dist/cli/setup.js +19 -17
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/graph/types.d.ts +2 -5
- package/dist/core/ingestion/ast-cache.js +2 -3
- package/dist/core/ingestion/call-processor.d.ts +5 -5
- package/dist/core/ingestion/call-processor.js +258 -173
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/entry-point-scoring.d.ts +1 -2
- package/dist/core/ingestion/entry-point-scoring.js +22 -81
- package/dist/core/ingestion/framework-detection.d.ts +1 -5
- package/dist/core/ingestion/framework-detection.js +8 -39
- package/dist/core/ingestion/heritage-processor.d.ts +4 -13
- package/dist/core/ingestion/heritage-processor.js +28 -92
- package/dist/core/ingestion/import-processor.d.ts +19 -17
- package/dist/core/ingestion/import-processor.js +695 -170
- package/dist/core/ingestion/parsing-processor.d.ts +10 -1
- package/dist/core/ingestion/parsing-processor.js +177 -41
- package/dist/core/ingestion/pipeline.js +26 -49
- package/dist/core/ingestion/process-processor.js +1 -2
- package/dist/core/ingestion/symbol-table.d.ts +1 -12
- package/dist/core/ingestion/symbol-table.js +12 -19
- package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
- package/dist/core/ingestion/tree-sitter-queries.js +485 -590
- package/dist/core/ingestion/utils.d.ts +0 -67
- package/dist/core/ingestion/utils.js +9 -692
- package/dist/core/ingestion/workers/parse-worker.d.ts +3 -20
- package/dist/core/ingestion/workers/parse-worker.js +345 -84
- package/dist/core/ingestion/workers/worker-pool.js +0 -8
- package/dist/core/kuzu/csv-generator.js +3 -19
- package/dist/core/kuzu/kuzu-adapter.js +19 -14
- package/dist/core/kuzu/schema.d.ts +3 -3
- package/dist/core/kuzu/schema.js +288 -303
- package/dist/core/search/bm25-index.js +6 -7
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/diagrams.d.ts +27 -0
- package/dist/core/wiki/diagrams.js +163 -0
- package/dist/core/wiki/generator.d.ts +50 -2
- package/dist/core/wiki/generator.js +548 -49
- package/dist/core/wiki/graph-queries.d.ts +42 -0
- package/dist/core/wiki/graph-queries.js +276 -97
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/llm-client.js +73 -11
- package/dist/core/wiki/prompts.d.ts +52 -8
- package/dist/core/wiki/prompts.js +200 -86
- package/dist/mcp/core/kuzu-adapter.d.ts +3 -1
- package/dist/mcp/core/kuzu-adapter.js +44 -13
- package/dist/mcp/local/local-backend.js +128 -128
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +19 -18
- package/dist/mcp/tools.js +104 -103
- package/hooks/claude/gitnexus-hook.cjs +155 -238
- package/hooks/claude/pre-tool-use.sh +79 -79
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +96 -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/cli/lazy-action.d.ts +0 -6
- package/dist/cli/lazy-action.js +0 -18
- package/dist/cli/skill-gen.d.ts +0 -26
- package/dist/cli/skill-gen.js +0 -549
- package/dist/core/ingestion/constants.d.ts +0 -16
- package/dist/core/ingestion/constants.js +0 -16
- package/dist/core/ingestion/export-detection.d.ts +0 -18
- package/dist/core/ingestion/export-detection.js +0 -230
- package/dist/core/ingestion/language-config.d.ts +0 -46
- package/dist/core/ingestion/language-config.js +0 -167
- package/dist/core/ingestion/mro-processor.d.ts +0 -45
- package/dist/core/ingestion/mro-processor.js +0 -369
- package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
- package/dist/core/ingestion/named-binding-extraction.js +0 -363
- package/dist/core/ingestion/resolvers/csharp.d.ts +0 -22
- package/dist/core/ingestion/resolvers/csharp.js +0 -109
- package/dist/core/ingestion/resolvers/go.d.ts +0 -19
- package/dist/core/ingestion/resolvers/go.js +0 -42
- package/dist/core/ingestion/resolvers/index.d.ts +0 -16
- package/dist/core/ingestion/resolvers/index.js +0 -11
- package/dist/core/ingestion/resolvers/jvm.d.ts +0 -23
- package/dist/core/ingestion/resolvers/jvm.js +0 -87
- package/dist/core/ingestion/resolvers/php.d.ts +0 -15
- package/dist/core/ingestion/resolvers/php.js +0 -35
- package/dist/core/ingestion/resolvers/rust.d.ts +0 -15
- package/dist/core/ingestion/resolvers/rust.js +0 -73
- package/dist/core/ingestion/resolvers/standard.d.ts +0 -28
- package/dist/core/ingestion/resolvers/standard.js +0 -145
- package/dist/core/ingestion/resolvers/utils.d.ts +0 -33
- package/dist/core/ingestion/resolvers/utils.js +0 -120
- package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
- package/dist/core/ingestion/symbol-resolver.js +0 -83
- package/dist/core/ingestion/type-env.d.ts +0 -27
- package/dist/core/ingestion/type-env.js +0 -86
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/c-cpp.js +0 -60
- package/dist/core/ingestion/type-extractors/csharp.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/csharp.js +0 -89
- package/dist/core/ingestion/type-extractors/go.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/go.js +0 -105
- package/dist/core/ingestion/type-extractors/index.d.ts +0 -21
- package/dist/core/ingestion/type-extractors/index.js +0 -29
- package/dist/core/ingestion/type-extractors/jvm.d.ts +0 -3
- package/dist/core/ingestion/type-extractors/jvm.js +0 -121
- package/dist/core/ingestion/type-extractors/php.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/php.js +0 -31
- package/dist/core/ingestion/type-extractors/python.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/python.js +0 -41
- package/dist/core/ingestion/type-extractors/rust.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/rust.js +0 -39
- package/dist/core/ingestion/type-extractors/shared.d.ts +0 -17
- package/dist/core/ingestion/type-extractors/shared.js +0 -97
- package/dist/core/ingestion/type-extractors/swift.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/swift.js +0 -43
- package/dist/core/ingestion/type-extractors/types.d.ts +0 -14
- package/dist/core/ingestion/type-extractors/types.js +0 -1
- package/dist/core/ingestion/type-extractors/typescript.d.ts +0 -2
- package/dist/core/ingestion/type-extractors/typescript.js +0 -46
- package/dist/mcp/compatible-stdio-transport.d.ts +0 -25
- package/dist/mcp/compatible-stdio-transport.js +0 -200
|
@@ -1,29 +1,16 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
1
3
|
import Parser from 'tree-sitter';
|
|
2
|
-
import {
|
|
4
|
+
import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
3
5
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
4
6
|
import { generateId } from '../../lib/utils.js';
|
|
5
|
-
import { getLanguageFromFilename,
|
|
7
|
+
import { getLanguageFromFilename, yieldToEventLoop } from './utils.js';
|
|
6
8
|
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, } from './resolvers/index.js';
|
|
11
9
|
const isDev = process.env.NODE_ENV === 'development';
|
|
12
10
|
export const createImportMap = () => new Map();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* Check if a file path is directly inside a package directory identified by its suffix.
|
|
17
|
-
* Used by the symbol resolver for Go and C# directory-level import matching.
|
|
18
|
-
*/
|
|
19
|
-
export function isFileInPackageDir(filePath, dirSuffix) {
|
|
20
|
-
// Prepend '/' so paths like "internal/auth/service.go" match suffix "/internal/auth/"
|
|
21
|
-
const normalized = '/' + filePath.replace(/\\/g, '/');
|
|
22
|
-
if (!normalized.includes(dirSuffix))
|
|
23
|
-
return false;
|
|
24
|
-
const afterDir = normalized.substring(normalized.indexOf(dirSuffix) + dirSuffix.length);
|
|
25
|
-
return !afterDir.includes('/');
|
|
26
|
-
}
|
|
11
|
+
/** Max entries in the resolve cache. Beyond this, the cache is cleared to bound memory.
|
|
12
|
+
* 100K entries ≈ 15MB — covers the most common import patterns. */
|
|
13
|
+
const RESOLVE_CACHE_CAP = 100_000;
|
|
27
14
|
export function buildImportResolutionContext(allPaths) {
|
|
28
15
|
const allFileList = allPaths;
|
|
29
16
|
const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
|
|
@@ -32,142 +19,550 @@ export function buildImportResolutionContext(allPaths) {
|
|
|
32
19
|
return { allFilePaths, allFileList, normalizedFileList, suffixIndex, resolveCache: new Map() };
|
|
33
20
|
}
|
|
34
21
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
22
|
+
* Parse tsconfig.json to extract path aliases.
|
|
23
|
+
* Tries tsconfig.json, tsconfig.app.json, tsconfig.base.json in order.
|
|
37
24
|
*/
|
|
38
|
-
function
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
25
|
+
async function loadTsconfigPaths(repoRoot) {
|
|
26
|
+
const candidates = ['tsconfig.json', 'tsconfig.app.json', 'tsconfig.base.json'];
|
|
27
|
+
for (const filename of candidates) {
|
|
28
|
+
try {
|
|
29
|
+
const tsconfigPath = path.join(repoRoot, filename);
|
|
30
|
+
const raw = await fs.readFile(tsconfigPath, 'utf-8');
|
|
31
|
+
// Strip JSON comments (// and /* */ style) for robustness
|
|
32
|
+
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
33
|
+
const tsconfig = JSON.parse(stripped);
|
|
34
|
+
const compilerOptions = tsconfig.compilerOptions;
|
|
35
|
+
if (!compilerOptions?.paths)
|
|
36
|
+
continue;
|
|
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);
|
|
47
|
+
}
|
|
48
|
+
if (aliases.size > 0) {
|
|
49
|
+
if (isDev) {
|
|
50
|
+
console.log(`📦 Loaded ${aliases.size} path aliases from ${filename}`);
|
|
51
|
+
}
|
|
52
|
+
return { aliases, baseUrl };
|
|
50
53
|
}
|
|
51
|
-
if (matchedFiles.length > 0)
|
|
52
|
-
return { kind: 'files', files: matchedFiles };
|
|
53
|
-
// Fall through to standard resolution
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
catch {
|
|
56
|
+
// File doesn't exist or isn't valid JSON - try next
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
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]}`);
|
|
59
72
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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`);
|
|
63
97
|
}
|
|
98
|
+
return { psr4 };
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
64
102
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
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
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function buildSuffixIndex(normalizedFileList, allFileList) {
|
|
172
|
+
// Map: normalized suffix -> original file path
|
|
173
|
+
const exactMap = new Map();
|
|
174
|
+
// Map: lowercase suffix -> original file path
|
|
175
|
+
const lowerMap = new Map();
|
|
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);
|
|
72
192
|
}
|
|
73
193
|
}
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
194
|
+
// Index directory membership
|
|
195
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
196
|
+
if (lastSlash >= 0) {
|
|
197
|
+
// Build all directory suffixes
|
|
198
|
+
const dirParts = parts.slice(0, -1);
|
|
199
|
+
const fileName = parts[parts.length - 1];
|
|
200
|
+
const ext = fileName.substring(fileName.lastIndexOf('.'));
|
|
201
|
+
for (let j = dirParts.length - 1; j >= 0; j--) {
|
|
202
|
+
const dirSuffix = dirParts.slice(j).join('/');
|
|
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;
|
|
83
233
|
}
|
|
84
234
|
}
|
|
85
|
-
if (resolvedFiles.length > 0)
|
|
86
|
-
return { kind: 'files', files: resolvedFiles };
|
|
87
235
|
return null;
|
|
88
236
|
}
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
237
|
+
// Fallback: linear scan (for backward compatibility)
|
|
238
|
+
for (let i = 0; i < pathParts.length; i++) {
|
|
239
|
+
const suffix = pathParts.slice(i).join('/');
|
|
240
|
+
for (const ext of EXTENSIONS) {
|
|
241
|
+
const suffixWithExt = suffix + ext;
|
|
242
|
+
const suffixPattern = '/' + suffixWithExt;
|
|
243
|
+
const matchIdx = normalizedFileList.findIndex(filePath => filePath.endsWith(suffixPattern) || filePath.toLowerCase().endsWith(suffixPattern.toLowerCase()));
|
|
244
|
+
if (matchIdx !== -1) {
|
|
245
|
+
return allFileList[matchIdx];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
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);
|
|
104
299
|
}
|
|
105
|
-
if (files.length > 0)
|
|
106
|
-
return { kind: 'files', files };
|
|
107
300
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
301
|
+
}
|
|
302
|
+
// ---- Rust: convert module path syntax to file paths ----
|
|
303
|
+
if (language === SupportedLanguages.Rust) {
|
|
304
|
+
const rustResult = resolveRustImport(currentFile, importPath, allFiles);
|
|
305
|
+
if (rustResult)
|
|
306
|
+
return cache(rustResult);
|
|
307
|
+
// Fall through to generic resolution if Rust-specific didn't match
|
|
308
|
+
}
|
|
309
|
+
// ---- Generic relative import resolution (./ and ../) ----
|
|
310
|
+
const currentDir = currentFile.split('/').slice(0, -1);
|
|
311
|
+
const parts = importPath.split('/');
|
|
312
|
+
for (const part of parts) {
|
|
313
|
+
if (part === '.')
|
|
314
|
+
continue;
|
|
315
|
+
if (part === '..') {
|
|
316
|
+
currentDir.pop();
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
currentDir.push(part);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const basePath = currentDir.join('/');
|
|
323
|
+
if (importPath.startsWith('.')) {
|
|
324
|
+
const resolved = tryResolveWithExtensions(basePath, allFiles);
|
|
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;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Append .* to a Kotlin import path if the AST has a wildcard_import sibling node.
|
|
413
|
+
* Pure function — returns a new string without mutating the input.
|
|
414
|
+
*/
|
|
415
|
+
const appendKotlinWildcard = (importPath, importNode) => {
|
|
416
|
+
for (let i = 0; i < importNode.childCount; i++) {
|
|
417
|
+
if (importNode.child(i)?.type === 'wildcard_import') {
|
|
418
|
+
return importPath.endsWith('.*') ? importPath : `${importPath}.*`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return importPath;
|
|
422
|
+
};
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// JVM MULTI-FILE RESOLUTION (Java + Kotlin)
|
|
425
|
+
// ============================================================================
|
|
426
|
+
/** Kotlin file extensions for JVM resolver reuse */
|
|
427
|
+
const KOTLIN_EXTENSIONS = ['.kt', '.kts'];
|
|
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
|
+
}
|
|
119
459
|
}
|
|
120
|
-
return resolved.length > 0 ? { kind: 'files', files: resolved } : null;
|
|
121
460
|
}
|
|
122
|
-
|
|
123
|
-
const resolvedPath = resolveImportPath(filePath, rawImportPath, allFilePaths, allFileList, normalizedFileList, resolveCache, language, tsconfigPaths, index);
|
|
124
|
-
return resolvedPath ? { kind: 'files', files: [resolvedPath] } : null;
|
|
461
|
+
return matches;
|
|
125
462
|
}
|
|
126
463
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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"
|
|
130
467
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
493
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
}
|
|
148
521
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
522
|
+
}
|
|
523
|
+
return matches;
|
|
524
|
+
}
|
|
525
|
+
// ============================================================================
|
|
526
|
+
// PHP PSR-4 IMPORT RESOLUTION
|
|
527
|
+
// ============================================================================
|
|
528
|
+
/**
|
|
529
|
+
* Resolve a PHP use-statement import path using PSR-4 mappings.
|
|
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
|
+
}
|
|
157
551
|
}
|
|
158
552
|
}
|
|
159
553
|
}
|
|
554
|
+
// Fallback: suffix matching (works without composer.json)
|
|
555
|
+
const pathParts = normalized.split('/').filter(Boolean);
|
|
556
|
+
return suffixResolve(pathParts, normalizedFileList, allFileList, index);
|
|
160
557
|
}
|
|
161
558
|
// ============================================================================
|
|
162
559
|
// MAIN IMPORT PROCESSOR
|
|
163
560
|
// ============================================================================
|
|
164
|
-
export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths
|
|
561
|
+
export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths) => {
|
|
165
562
|
// Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
|
|
166
563
|
const allFileList = allPaths ?? files.map(f => f.path);
|
|
167
564
|
const allFilePaths = new Set(allFileList);
|
|
168
565
|
const parser = await loadParser();
|
|
169
|
-
const logSkipped = isVerboseIngestionEnabled();
|
|
170
|
-
const skippedByLang = logSkipped ? new Map() : null;
|
|
171
566
|
const resolveCache = new Map();
|
|
172
567
|
// Pre-compute normalized file list once (forward slashes)
|
|
173
568
|
const normalizedFileList = allFileList.map(p => p.replace(/\\/g, '/'));
|
|
@@ -178,16 +573,12 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
178
573
|
let totalImportsResolved = 0;
|
|
179
574
|
// Load language-specific configs once before the file loop
|
|
180
575
|
const effectiveRoot = repoRoot || '';
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
};
|
|
188
|
-
const ctx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
|
|
189
|
-
// Helper: add an IMPORTS edge to the graph only (no ImportMap update)
|
|
190
|
-
const addImportGraphEdge = (filePath, resolvedPath) => {
|
|
576
|
+
const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
|
|
577
|
+
const goModule = await loadGoModulePath(effectiveRoot);
|
|
578
|
+
const composerConfig = await loadComposerConfig(effectiveRoot);
|
|
579
|
+
const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot);
|
|
580
|
+
// Helper: add an IMPORTS edge + update import map
|
|
581
|
+
const addImportEdge = (filePath, resolvedPath) => {
|
|
191
582
|
const sourceId = generateId('File', filePath);
|
|
192
583
|
const targetId = generateId('File', resolvedPath);
|
|
193
584
|
const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
|
|
@@ -200,10 +591,6 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
200
591
|
confidence: 1.0,
|
|
201
592
|
reason: '',
|
|
202
593
|
});
|
|
203
|
-
};
|
|
204
|
-
// Helper: add an IMPORTS edge + update import map
|
|
205
|
-
const addImportEdge = (filePath, resolvedPath) => {
|
|
206
|
-
addImportGraphEdge(filePath, resolvedPath);
|
|
207
594
|
if (!importMap.has(filePath)) {
|
|
208
595
|
importMap.set(filePath, new Set());
|
|
209
596
|
}
|
|
@@ -218,12 +605,6 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
218
605
|
const language = getLanguageFromFilename(file.path);
|
|
219
606
|
if (!language)
|
|
220
607
|
continue;
|
|
221
|
-
if (!isLanguageAvailable(language)) {
|
|
222
|
-
if (skippedByLang) {
|
|
223
|
-
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
|
|
224
|
-
}
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
608
|
const queryStr = LANGUAGE_QUERIES[language];
|
|
228
609
|
if (!queryStr)
|
|
229
610
|
continue;
|
|
@@ -234,7 +615,7 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
234
615
|
let wasReparsed = false;
|
|
235
616
|
if (!tree) {
|
|
236
617
|
try {
|
|
237
|
-
tree = parser.parse(file.content, undefined, { bufferSize:
|
|
618
|
+
tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
|
|
238
619
|
}
|
|
239
620
|
catch (parseError) {
|
|
240
621
|
continue;
|
|
@@ -281,18 +662,83 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
281
662
|
? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
|
|
282
663
|
: sourceNode.text.replace(/['"<>]/g, '');
|
|
283
664
|
totalImportsFound++;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
665
|
+
// ---- JVM languages (Java + Kotlin): handle wildcards and member imports ----
|
|
666
|
+
if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) {
|
|
667
|
+
const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS;
|
|
668
|
+
if (rawImportPath.endsWith('.*')) {
|
|
669
|
+
const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index);
|
|
670
|
+
// Kotlin can import Java files in mixed codebases — try .java as fallback
|
|
671
|
+
if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) {
|
|
672
|
+
const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index);
|
|
673
|
+
for (const matchedFile of javaMatches) {
|
|
674
|
+
addImportEdge(file.path, matchedFile);
|
|
675
|
+
}
|
|
676
|
+
if (javaMatches.length > 0)
|
|
677
|
+
return;
|
|
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;
|
|
729
|
+
}
|
|
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
|
+
}
|
|
287
738
|
}
|
|
288
739
|
});
|
|
289
740
|
// Tree is now owned by the LRU cache — no manual delete needed
|
|
290
741
|
}
|
|
291
|
-
if (skippedByLang && skippedByLang.size > 0) {
|
|
292
|
-
for (const [lang, count] of skippedByLang.entries()) {
|
|
293
|
-
console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in import processing — ${lang} parser not available.`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
742
|
if (isDev) {
|
|
297
743
|
console.log(`📊 Import processing complete: ${totalImportsResolved}/${totalImportsFound} imports resolved to graph edges`);
|
|
298
744
|
}
|
|
@@ -300,22 +746,17 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
|
|
|
300
746
|
// ============================================================================
|
|
301
747
|
// FAST PATH: Resolve pre-extracted imports (no parsing needed)
|
|
302
748
|
// ============================================================================
|
|
303
|
-
export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx
|
|
749
|
+
export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx) => {
|
|
304
750
|
const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
|
|
305
751
|
const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = ctx;
|
|
306
752
|
let totalImportsFound = 0;
|
|
307
753
|
let totalImportsResolved = 0;
|
|
308
754
|
const effectiveRoot = repoRoot || '';
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
|
|
315
|
-
};
|
|
316
|
-
const resolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
|
|
317
|
-
// Helper: add an IMPORTS edge to the graph only (no ImportMap update)
|
|
318
|
-
const addImportGraphEdge = (filePath, resolvedPath) => {
|
|
755
|
+
const tsconfigPaths = await loadTsconfigPaths(effectiveRoot);
|
|
756
|
+
const goModule = await loadGoModulePath(effectiveRoot);
|
|
757
|
+
const composerConfig = await loadComposerConfig(effectiveRoot);
|
|
758
|
+
const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot);
|
|
759
|
+
const addImportEdge = (filePath, resolvedPath) => {
|
|
319
760
|
const sourceId = generateId('File', filePath);
|
|
320
761
|
const targetId = generateId('File', resolvedPath);
|
|
321
762
|
const relId = generateId('IMPORTS', `${filePath}->${resolvedPath}`);
|
|
@@ -328,9 +769,6 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
|
|
|
328
769
|
confidence: 1.0,
|
|
329
770
|
reason: '',
|
|
330
771
|
});
|
|
331
|
-
};
|
|
332
|
-
const addImportEdge = (filePath, resolvedPath) => {
|
|
333
|
-
addImportGraphEdge(filePath, resolvedPath);
|
|
334
772
|
if (!importMap.has(filePath)) {
|
|
335
773
|
importMap.set(filePath, new Set());
|
|
336
774
|
}
|
|
@@ -348,16 +786,103 @@ export const processImportsFromExtracted = async (graph, files, extractedImports
|
|
|
348
786
|
}
|
|
349
787
|
const totalFiles = importsByFile.size;
|
|
350
788
|
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
|
+
}
|
|
351
803
|
for (const [filePath, fileImports] of importsByFile) {
|
|
352
804
|
filesProcessed++;
|
|
353
805
|
if (filesProcessed % 100 === 0) {
|
|
354
806
|
onProgress?.(filesProcessed, totalFiles);
|
|
355
807
|
await yieldToEventLoop();
|
|
356
808
|
}
|
|
357
|
-
for (const
|
|
809
|
+
for (const { rawImportPath, language } of fileImports) {
|
|
358
810
|
totalImportsFound++;
|
|
359
|
-
|
|
360
|
-
|
|
811
|
+
// Check resolve cache first
|
|
812
|
+
const cacheKey = `${filePath}::${rawImportPath}`;
|
|
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
|
+
}
|
|
361
886
|
}
|
|
362
887
|
}
|
|
363
888
|
onProgress?.(totalFiles, totalFiles);
|