gitnexus 1.4.6 → 1.4.8
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 +22 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +54 -21
- package/dist/cli/index.js +2 -1
- package/dist/cli/setup.js +78 -1
- package/dist/config/supported-languages.d.ts +30 -0
- package/dist/config/supported-languages.js +30 -0
- package/dist/core/embeddings/embedder.d.ts +6 -1
- package/dist/core/embeddings/embedder.js +65 -5
- package/dist/core/embeddings/embedding-pipeline.js +11 -9
- package/dist/core/embeddings/http-client.d.ts +31 -0
- package/dist/core/embeddings/http-client.js +179 -0
- package/dist/core/embeddings/index.d.ts +1 -0
- package/dist/core/embeddings/index.js +1 -0
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +4 -3
- package/dist/core/ingestion/ast-helpers.d.ts +80 -0
- package/dist/core/ingestion/ast-helpers.js +738 -0
- package/dist/core/ingestion/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/call-analysis.js +490 -0
- package/dist/core/ingestion/call-processor.d.ts +55 -2
- package/dist/core/ingestion/call-processor.js +673 -108
- package/dist/core/ingestion/call-routing.d.ts +23 -2
- package/dist/core/ingestion/call-routing.js +21 -0
- package/dist/core/ingestion/entry-point-scoring.js +36 -26
- package/dist/core/ingestion/framework-detection.d.ts +10 -2
- package/dist/core/ingestion/framework-detection.js +49 -12
- package/dist/core/ingestion/heritage-processor.js +47 -49
- package/dist/core/ingestion/import-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +103 -194
- package/dist/core/ingestion/import-resolution.d.ts +101 -0
- package/dist/core/ingestion/import-resolution.js +251 -0
- package/dist/core/ingestion/language-config.d.ts +3 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/mro-processor.js +8 -3
- package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
- package/dist/core/ingestion/named-binding-extraction.js +89 -79
- package/dist/core/ingestion/parsing-processor.d.ts +3 -2
- package/dist/core/ingestion/parsing-processor.js +27 -60
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +425 -4
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +7 -4
- package/dist/core/ingestion/resolvers/index.d.ts +1 -1
- package/dist/core/ingestion/resolvers/index.js +1 -1
- package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
- package/dist/core/ingestion/resolvers/jvm.js +25 -9
- package/dist/core/ingestion/resolvers/php.d.ts +14 -0
- package/dist/core/ingestion/resolvers/php.js +43 -3
- package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
- package/dist/core/ingestion/resolvers/utils.js +16 -0
- package/dist/core/ingestion/symbol-table.d.ts +29 -3
- package/dist/core/ingestion/symbol-table.js +42 -9
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +243 -2
- package/dist/core/ingestion/type-env.d.ts +28 -1
- package/dist/core/ingestion/type-env.js +451 -72
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
- package/dist/core/ingestion/type-extractors/csharp.js +189 -16
- package/dist/core/ingestion/type-extractors/go.js +45 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/index.js +1 -1
- package/dist/core/ingestion/type-extractors/jvm.js +244 -69
- package/dist/core/ingestion/type-extractors/php.js +31 -4
- package/dist/core/ingestion/type-extractors/python.js +89 -17
- package/dist/core/ingestion/type-extractors/ruby.js +17 -2
- package/dist/core/ingestion/type-extractors/rust.js +72 -4
- package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
- package/dist/core/ingestion/type-extractors/shared.js +115 -13
- package/dist/core/ingestion/type-extractors/swift.js +7 -6
- package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
- package/dist/core/ingestion/type-extractors/typescript.js +171 -9
- package/dist/core/ingestion/utils.d.ts +2 -95
- package/dist/core/ingestion/utils.js +3 -892
- package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
- package/dist/core/ingestion/workers/parse-worker.js +116 -95
- package/dist/core/lbug/csv-generator.js +18 -1
- package/dist/core/lbug/lbug-adapter.d.ts +12 -0
- package/dist/core/lbug/lbug-adapter.js +71 -4
- package/dist/core/lbug/schema.d.ts +6 -4
- package/dist/core/lbug/schema.js +27 -3
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +178 -23
- package/dist/mcp/local/local-backend.d.ts +22 -0
- package/dist/mcp/local/local-backend.js +136 -32
- package/dist/mcp/resources.js +13 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +17 -7
- package/dist/server/api.d.ts +19 -1
- package/dist/server/api.js +66 -6
- package/dist/storage/git.d.ts +12 -0
- package/dist/storage/git.js +21 -0
- package/package.json +12 -4
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createKnowledgeGraph } from '../graph/graph.js';
|
|
2
2
|
import { processStructure } from './structure-processor.js';
|
|
3
|
+
import { processMarkdown } from './markdown-processor.js';
|
|
3
4
|
import { processParsing } from './parsing-processor.js';
|
|
4
5
|
import { processImports, processImportsFromExtracted, buildImportResolutionContext } from './import-processor.js';
|
|
5
|
-
import {
|
|
6
|
+
import { EMPTY_INDEX } from './resolvers/index.js';
|
|
7
|
+
import { processCalls, processCallsFromExtracted, processAssignmentsFromExtracted, processRoutesFromExtracted, seedCrossFileReceiverTypes, buildImportedReturnTypes, buildImportedRawReturnTypes, buildExportedTypeMapFromGraph } from './call-processor.js';
|
|
6
8
|
import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
|
|
7
9
|
import { computeMRO } from './mro-processor.js';
|
|
8
10
|
import { processCommunities } from './community-processor.js';
|
|
@@ -12,11 +14,63 @@ import { createASTCache } from './ast-cache.js';
|
|
|
12
14
|
import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
|
|
13
15
|
import { getLanguageFromFilename } from './utils.js';
|
|
14
16
|
import { isLanguageAvailable } from '../tree-sitter/parser-loader.js';
|
|
17
|
+
import { SupportedLanguages } from '../../config/supported-languages.js';
|
|
15
18
|
import { createWorkerPool } from './workers/worker-pool.js';
|
|
16
19
|
import fs from 'node:fs';
|
|
17
20
|
import path from 'node:path';
|
|
18
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
22
|
const isDev = process.env.NODE_ENV === 'development';
|
|
23
|
+
/** Kahn's algorithm: returns files grouped by topological level.
|
|
24
|
+
* Files in the same level have no mutual dependencies — safe to process in parallel.
|
|
25
|
+
* Files in cycles are returned as a final group (no cross-cycle propagation). */
|
|
26
|
+
export function topologicalLevelSort(importMap) {
|
|
27
|
+
// Build in-degree map and reverse dependency map
|
|
28
|
+
const inDegree = new Map();
|
|
29
|
+
const reverseDeps = new Map();
|
|
30
|
+
for (const [file, deps] of importMap) {
|
|
31
|
+
if (!inDegree.has(file))
|
|
32
|
+
inDegree.set(file, 0);
|
|
33
|
+
for (const dep of deps) {
|
|
34
|
+
if (!inDegree.has(dep))
|
|
35
|
+
inDegree.set(dep, 0);
|
|
36
|
+
// file imports dep, so dep must be processed before file
|
|
37
|
+
// In Kahn's terms: dep → file (dep is a prerequisite of file)
|
|
38
|
+
inDegree.set(file, (inDegree.get(file) ?? 0) + 1);
|
|
39
|
+
let rev = reverseDeps.get(dep);
|
|
40
|
+
if (!rev) {
|
|
41
|
+
rev = [];
|
|
42
|
+
reverseDeps.set(dep, rev);
|
|
43
|
+
}
|
|
44
|
+
rev.push(file);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// BFS from zero-in-degree nodes, grouping by level
|
|
48
|
+
const levels = [];
|
|
49
|
+
let currentLevel = [...inDegree.entries()]
|
|
50
|
+
.filter(([, d]) => d === 0)
|
|
51
|
+
.map(([f]) => f);
|
|
52
|
+
while (currentLevel.length > 0) {
|
|
53
|
+
levels.push(currentLevel);
|
|
54
|
+
const nextLevel = [];
|
|
55
|
+
for (const file of currentLevel) {
|
|
56
|
+
for (const dependent of reverseDeps.get(file) ?? []) {
|
|
57
|
+
const newDeg = (inDegree.get(dependent) ?? 1) - 1;
|
|
58
|
+
inDegree.set(dependent, newDeg);
|
|
59
|
+
if (newDeg === 0)
|
|
60
|
+
nextLevel.push(dependent);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
currentLevel = nextLevel;
|
|
64
|
+
}
|
|
65
|
+
// Files still with positive in-degree are in cycles — add as final group
|
|
66
|
+
const cycleFiles = [...inDegree.entries()]
|
|
67
|
+
.filter(([, d]) => d > 0)
|
|
68
|
+
.map(([f]) => f);
|
|
69
|
+
if (cycleFiles.length > 0) {
|
|
70
|
+
levels.push(cycleFiles);
|
|
71
|
+
}
|
|
72
|
+
return { levels, cycleCount: cycleFiles.length };
|
|
73
|
+
}
|
|
20
74
|
/** Max bytes of source content to load per parse chunk. Each chunk's source +
|
|
21
75
|
* parsed ASTs + extracted records + worker serialization overhead all live in
|
|
22
76
|
* memory simultaneously, so this must be conservative. 20MB source ≈ 200-400MB
|
|
@@ -24,11 +78,286 @@ const isDev = process.env.NODE_ENV === 'development';
|
|
|
24
78
|
const CHUNK_BYTE_BUDGET = 20 * 1024 * 1024; // 20MB
|
|
25
79
|
/** Max AST trees to keep in LRU cache */
|
|
26
80
|
const AST_CACHE_CAP = 50;
|
|
81
|
+
/** Minimum percentage of files that must benefit from cross-file seeding to justify the re-resolution pass. */
|
|
82
|
+
const CROSS_FILE_SKIP_THRESHOLD = 0.03;
|
|
83
|
+
/** Hard cap on files re-processed during cross-file propagation. */
|
|
84
|
+
const MAX_CROSS_FILE_REPROCESS = 2000;
|
|
85
|
+
/** Node labels that represent top-level importable symbols.
|
|
86
|
+
* Excludes Method, Property, Constructor (accessed via receiver, not directly imported),
|
|
87
|
+
* and structural labels (File, Folder, Package, Module, Project, etc.). */
|
|
88
|
+
const IMPORTABLE_SYMBOL_LABELS = new Set([
|
|
89
|
+
'Function', 'Class', 'Interface', 'Struct', 'Enum', 'Trait',
|
|
90
|
+
'TypeAlias', 'Const', 'Static', 'Record', 'Union', 'Typedef', 'Macro',
|
|
91
|
+
]);
|
|
92
|
+
/** Max synthetic bindings per importing file — prevents memory bloat for
|
|
93
|
+
* C/C++ files that include many large headers. */
|
|
94
|
+
const MAX_SYNTHETIC_BINDINGS_PER_FILE = 1000;
|
|
95
|
+
/** Languages with whole-module import semantics (no per-symbol named imports).
|
|
96
|
+
* For these languages, namedImportMap entries are synthesized from graph-exported
|
|
97
|
+
* symbols after parsing, enabling Phase 14 cross-file binding propagation.
|
|
98
|
+
*
|
|
99
|
+
* Note: Python is intentionally excluded here. `import models` is a namespace import
|
|
100
|
+
* (not wildcard symbol expansion) — expanding all exported symbols produces ambiguous
|
|
101
|
+
* bindings when multiple modules export the same name (e.g. models.User vs auth.User).
|
|
102
|
+
* Python module aliases are built in synthesizeWildcardImportBindings via moduleAliasMap. */
|
|
103
|
+
const WILDCARD_IMPORT_LANGUAGES = new Set([
|
|
104
|
+
SupportedLanguages.Go,
|
|
105
|
+
SupportedLanguages.Ruby,
|
|
106
|
+
SupportedLanguages.C,
|
|
107
|
+
SupportedLanguages.CPlusPlus,
|
|
108
|
+
SupportedLanguages.Swift,
|
|
109
|
+
]);
|
|
110
|
+
/** Languages that require synthesizeWildcardImportBindings to run before call resolution.
|
|
111
|
+
* Superset of WILDCARD_IMPORT_LANGUAGES — includes Python for moduleAliasMap building. */
|
|
112
|
+
const SYNTHESIS_LANGUAGES = new Set([...WILDCARD_IMPORT_LANGUAGES, SupportedLanguages.Python]);
|
|
113
|
+
/** Synthesize namedImportMap entries for languages with whole-module imports.
|
|
114
|
+
* These languages (Go, Ruby, C/C++, Swift, Python) import all exported symbols from a
|
|
115
|
+
* file, not specific named symbols. After parsing, we know which symbols each file
|
|
116
|
+
* exports (via graph isExported), so we can expand ImportMap edges into per-symbol
|
|
117
|
+
* bindings that Phase 14 can use for cross-file type propagation. */
|
|
118
|
+
function synthesizeWildcardImportBindings(graph, ctx) {
|
|
119
|
+
// Pre-compute exported symbols per file from graph (single pass)
|
|
120
|
+
const exportedSymbolsByFile = new Map();
|
|
121
|
+
graph.forEachNode(node => {
|
|
122
|
+
if (!node.properties?.isExported)
|
|
123
|
+
return;
|
|
124
|
+
if (!IMPORTABLE_SYMBOL_LABELS.has(node.label))
|
|
125
|
+
return;
|
|
126
|
+
const fp = node.properties.filePath;
|
|
127
|
+
const name = node.properties.name;
|
|
128
|
+
if (!fp || !name)
|
|
129
|
+
return;
|
|
130
|
+
let symbols = exportedSymbolsByFile.get(fp);
|
|
131
|
+
if (!symbols) {
|
|
132
|
+
symbols = [];
|
|
133
|
+
exportedSymbolsByFile.set(fp, symbols);
|
|
134
|
+
}
|
|
135
|
+
symbols.push({ name, filePath: fp });
|
|
136
|
+
});
|
|
137
|
+
if (exportedSymbolsByFile.size === 0)
|
|
138
|
+
return 0;
|
|
139
|
+
// Build a merged import map: ctx.importMap has file-based imports (Ruby, C/C++),
|
|
140
|
+
// but Go/C# package imports use graph IMPORTS edges + PackageMap instead.
|
|
141
|
+
// Collect graph-level IMPORTS edges for wildcard languages missing from ctx.importMap.
|
|
142
|
+
const FILE_PREFIX = 'File:';
|
|
143
|
+
const graphImports = new Map();
|
|
144
|
+
graph.forEachRelationship(rel => {
|
|
145
|
+
if (rel.type !== 'IMPORTS')
|
|
146
|
+
return;
|
|
147
|
+
if (!rel.sourceId.startsWith(FILE_PREFIX) || !rel.targetId.startsWith(FILE_PREFIX))
|
|
148
|
+
return;
|
|
149
|
+
const srcFile = rel.sourceId.slice(FILE_PREFIX.length);
|
|
150
|
+
const tgtFile = rel.targetId.slice(FILE_PREFIX.length);
|
|
151
|
+
const lang = getLanguageFromFilename(srcFile);
|
|
152
|
+
if (!lang || !WILDCARD_IMPORT_LANGUAGES.has(lang))
|
|
153
|
+
return;
|
|
154
|
+
// Only add if not already in ctx.importMap (avoid duplicates)
|
|
155
|
+
if (ctx.importMap.get(srcFile)?.has(tgtFile))
|
|
156
|
+
return;
|
|
157
|
+
let set = graphImports.get(srcFile);
|
|
158
|
+
if (!set) {
|
|
159
|
+
set = new Set();
|
|
160
|
+
graphImports.set(srcFile, set);
|
|
161
|
+
}
|
|
162
|
+
set.add(tgtFile);
|
|
163
|
+
});
|
|
164
|
+
let totalSynthesized = 0;
|
|
165
|
+
// Helper: synthesize bindings for a file given its imported files
|
|
166
|
+
const synthesizeForFile = (filePath, importedFiles) => {
|
|
167
|
+
let fileBindings = ctx.namedImportMap.get(filePath);
|
|
168
|
+
let fileCount = fileBindings?.size ?? 0;
|
|
169
|
+
for (const importedFile of importedFiles) {
|
|
170
|
+
const exportedSymbols = exportedSymbolsByFile.get(importedFile);
|
|
171
|
+
if (!exportedSymbols)
|
|
172
|
+
continue;
|
|
173
|
+
for (const sym of exportedSymbols) {
|
|
174
|
+
if (fileCount >= MAX_SYNTHETIC_BINDINGS_PER_FILE)
|
|
175
|
+
return;
|
|
176
|
+
if (fileBindings?.has(sym.name))
|
|
177
|
+
continue;
|
|
178
|
+
if (!fileBindings) {
|
|
179
|
+
fileBindings = new Map();
|
|
180
|
+
ctx.namedImportMap.set(filePath, fileBindings);
|
|
181
|
+
}
|
|
182
|
+
fileBindings.set(sym.name, {
|
|
183
|
+
sourcePath: importedFile,
|
|
184
|
+
exportedName: sym.name,
|
|
185
|
+
});
|
|
186
|
+
fileCount++;
|
|
187
|
+
totalSynthesized++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
// Process files from ctx.importMap (Ruby, C/C++, Swift file-based imports)
|
|
192
|
+
for (const [filePath, importedFiles] of ctx.importMap) {
|
|
193
|
+
const lang = getLanguageFromFilename(filePath);
|
|
194
|
+
if (!lang || !WILDCARD_IMPORT_LANGUAGES.has(lang))
|
|
195
|
+
continue;
|
|
196
|
+
synthesizeForFile(filePath, importedFiles);
|
|
197
|
+
}
|
|
198
|
+
// Process files from graph IMPORTS edges (Go and other wildcard-import languages)
|
|
199
|
+
for (const [filePath, importedFiles] of graphImports) {
|
|
200
|
+
synthesizeForFile(filePath, importedFiles);
|
|
201
|
+
}
|
|
202
|
+
// Build module alias map for Python namespace imports.
|
|
203
|
+
// `import models` in app.py → ctx.moduleAliasMap['app.py']['models'] = 'models.py'
|
|
204
|
+
// Enables `models.User()` to resolve to models.py:User without ambiguous symbol expansion.
|
|
205
|
+
const buildPythonModuleAliasForFile = (callerFile, importedFiles) => {
|
|
206
|
+
let aliasMap = ctx.moduleAliasMap.get(callerFile);
|
|
207
|
+
for (const importedFile of importedFiles) {
|
|
208
|
+
// Derive the module alias from the imported filename stem (e.g. "models.py" → "models")
|
|
209
|
+
const lastSlash = importedFile.lastIndexOf('/');
|
|
210
|
+
const base = lastSlash >= 0 ? importedFile.slice(lastSlash + 1) : importedFile;
|
|
211
|
+
const dot = base.lastIndexOf('.');
|
|
212
|
+
const stem = dot >= 0 ? base.slice(0, dot) : base;
|
|
213
|
+
if (!stem)
|
|
214
|
+
continue;
|
|
215
|
+
if (!aliasMap) {
|
|
216
|
+
aliasMap = new Map();
|
|
217
|
+
ctx.moduleAliasMap.set(callerFile, aliasMap);
|
|
218
|
+
}
|
|
219
|
+
aliasMap.set(stem, importedFile);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
for (const [filePath, importedFiles] of ctx.importMap) {
|
|
223
|
+
if (getLanguageFromFilename(filePath) !== SupportedLanguages.Python)
|
|
224
|
+
continue;
|
|
225
|
+
buildPythonModuleAliasForFile(filePath, importedFiles);
|
|
226
|
+
}
|
|
227
|
+
return totalSynthesized;
|
|
228
|
+
}
|
|
229
|
+
/** Phase 14: Cross-file binding propagation.
|
|
230
|
+
* Seeds downstream files with resolved type bindings from upstream exports.
|
|
231
|
+
* Files are processed in topological import order so upstream bindings are
|
|
232
|
+
* available when downstream files are re-resolved. */
|
|
233
|
+
async function runCrossFileBindingPropagation(graph, ctx, exportedTypeMap, allPaths, totalFiles, repoPath, pipelineStart, onProgress) {
|
|
234
|
+
// For the worker path, buildTypeEnv runs inside workers without SymbolTable,
|
|
235
|
+
// so exported bindings must be collected from graph + SymbolTable in main thread.
|
|
236
|
+
if (exportedTypeMap.size === 0 && graph.nodeCount > 0) {
|
|
237
|
+
const graphExports = buildExportedTypeMapFromGraph(graph, ctx.symbols);
|
|
238
|
+
for (const [fp, exports] of graphExports)
|
|
239
|
+
exportedTypeMap.set(fp, exports);
|
|
240
|
+
}
|
|
241
|
+
if (exportedTypeMap.size === 0 || ctx.namedImportMap.size === 0)
|
|
242
|
+
return;
|
|
243
|
+
const allPathSet = new Set(allPaths);
|
|
244
|
+
const { levels, cycleCount } = topologicalLevelSort(ctx.importMap);
|
|
245
|
+
// Cycle diagnostic: only log when actual cycles detected (cycleCount from Kahn's BFS)
|
|
246
|
+
if (isDev && cycleCount > 0) {
|
|
247
|
+
console.log(`🔄 ${cycleCount} files in import cycles (skipped for cross-file propagation)`);
|
|
248
|
+
}
|
|
249
|
+
// Quick count of files with cross-file binding gaps (early exit once threshold exceeded)
|
|
250
|
+
let filesWithGaps = 0;
|
|
251
|
+
const gapThreshold = Math.max(1, Math.ceil(totalFiles * CROSS_FILE_SKIP_THRESHOLD));
|
|
252
|
+
outer: for (const level of levels) {
|
|
253
|
+
for (const filePath of level) {
|
|
254
|
+
const imports = ctx.namedImportMap.get(filePath);
|
|
255
|
+
if (!imports)
|
|
256
|
+
continue;
|
|
257
|
+
for (const [, binding] of imports) {
|
|
258
|
+
const upstream = exportedTypeMap.get(binding.sourcePath);
|
|
259
|
+
if (upstream?.has(binding.exportedName)) {
|
|
260
|
+
filesWithGaps++;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
const def = ctx.symbols.lookupExactFull(binding.sourcePath, binding.exportedName);
|
|
264
|
+
if (def?.returnType) {
|
|
265
|
+
filesWithGaps++;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (filesWithGaps >= gapThreshold)
|
|
270
|
+
break outer;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const gapRatio = totalFiles > 0 ? filesWithGaps / totalFiles : 0;
|
|
274
|
+
if (gapRatio < CROSS_FILE_SKIP_THRESHOLD && filesWithGaps < gapThreshold) {
|
|
275
|
+
if (isDev) {
|
|
276
|
+
console.log(`⏭️ Cross-file re-resolution skipped (${filesWithGaps}/${totalFiles} files, ${(gapRatio * 100).toFixed(1)}% < ${CROSS_FILE_SKIP_THRESHOLD * 100}% threshold)`);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
onProgress({
|
|
281
|
+
phase: 'parsing',
|
|
282
|
+
percent: 82,
|
|
283
|
+
message: `Cross-file type propagation (${filesWithGaps}+ files)...`,
|
|
284
|
+
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
285
|
+
});
|
|
286
|
+
let crossFileResolved = 0;
|
|
287
|
+
const crossFileStart = Date.now();
|
|
288
|
+
let astCache = createASTCache(AST_CACHE_CAP);
|
|
289
|
+
for (const level of levels) {
|
|
290
|
+
const levelCandidates = [];
|
|
291
|
+
for (const filePath of level) {
|
|
292
|
+
if (crossFileResolved + levelCandidates.length >= MAX_CROSS_FILE_REPROCESS)
|
|
293
|
+
break;
|
|
294
|
+
const imports = ctx.namedImportMap.get(filePath);
|
|
295
|
+
if (!imports)
|
|
296
|
+
continue;
|
|
297
|
+
const seeded = new Map();
|
|
298
|
+
for (const [localName, binding] of imports) {
|
|
299
|
+
const upstream = exportedTypeMap.get(binding.sourcePath);
|
|
300
|
+
if (upstream) {
|
|
301
|
+
const type = upstream.get(binding.exportedName);
|
|
302
|
+
if (type)
|
|
303
|
+
seeded.set(localName, type);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const importedReturns = buildImportedReturnTypes(filePath, ctx.namedImportMap, ctx.symbols);
|
|
307
|
+
const importedRawReturns = buildImportedRawReturnTypes(filePath, ctx.namedImportMap, ctx.symbols);
|
|
308
|
+
if (seeded.size === 0 && importedReturns.size === 0)
|
|
309
|
+
continue;
|
|
310
|
+
if (!allPathSet.has(filePath))
|
|
311
|
+
continue;
|
|
312
|
+
const lang = getLanguageFromFilename(filePath);
|
|
313
|
+
if (!lang || !isLanguageAvailable(lang))
|
|
314
|
+
continue;
|
|
315
|
+
levelCandidates.push({ filePath, seeded, importedReturns, importedRawReturns });
|
|
316
|
+
}
|
|
317
|
+
if (levelCandidates.length === 0)
|
|
318
|
+
continue;
|
|
319
|
+
const levelPaths = levelCandidates.map(c => c.filePath);
|
|
320
|
+
const contentMap = await readFileContents(repoPath, levelPaths);
|
|
321
|
+
for (const { filePath, seeded, importedReturns, importedRawReturns } of levelCandidates) {
|
|
322
|
+
const content = contentMap.get(filePath);
|
|
323
|
+
if (!content)
|
|
324
|
+
continue;
|
|
325
|
+
const reFile = [{ path: filePath, content }];
|
|
326
|
+
const bindings = new Map();
|
|
327
|
+
if (seeded.size > 0)
|
|
328
|
+
bindings.set(filePath, seeded);
|
|
329
|
+
const importedReturnTypesMap = new Map();
|
|
330
|
+
if (importedReturns.size > 0) {
|
|
331
|
+
importedReturnTypesMap.set(filePath, importedReturns);
|
|
332
|
+
}
|
|
333
|
+
const importedRawReturnTypesMap = new Map();
|
|
334
|
+
if (importedRawReturns.size > 0) {
|
|
335
|
+
importedRawReturnTypesMap.set(filePath, importedRawReturns);
|
|
336
|
+
}
|
|
337
|
+
await processCalls(graph, reFile, astCache, ctx, undefined, exportedTypeMap, bindings.size > 0 ? bindings : undefined, importedReturnTypesMap.size > 0 ? importedReturnTypesMap : undefined, importedRawReturnTypesMap.size > 0 ? importedRawReturnTypesMap : undefined);
|
|
338
|
+
crossFileResolved++;
|
|
339
|
+
}
|
|
340
|
+
if (crossFileResolved >= MAX_CROSS_FILE_REPROCESS) {
|
|
341
|
+
if (isDev)
|
|
342
|
+
console.log(`⚠️ Cross-file re-resolution capped at ${MAX_CROSS_FILE_REPROCESS} files`);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
astCache.clear();
|
|
347
|
+
if (isDev) {
|
|
348
|
+
const elapsed = Date.now() - crossFileStart;
|
|
349
|
+
const totalElapsed = Date.now() - pipelineStart;
|
|
350
|
+
const reResolutionPct = totalElapsed > 0 ? ((elapsed / totalElapsed) * 100).toFixed(1) : '0';
|
|
351
|
+
console.log(`🔗 Cross-file re-resolution: ${crossFileResolved} candidates re-processed` +
|
|
352
|
+
` in ${elapsed}ms (${reResolutionPct}% of total ingestion time so far)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
27
355
|
export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
28
356
|
const graph = createKnowledgeGraph();
|
|
29
357
|
const ctx = createResolutionContext();
|
|
30
358
|
const symbolTable = ctx.symbols;
|
|
31
359
|
let astCache = createASTCache(AST_CACHE_CAP);
|
|
360
|
+
const pipelineStart = Date.now();
|
|
32
361
|
const cleanup = () => {
|
|
33
362
|
astCache.clear();
|
|
34
363
|
ctx.clear();
|
|
@@ -72,6 +401,19 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
72
401
|
message: 'Project structure analyzed',
|
|
73
402
|
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
74
403
|
});
|
|
404
|
+
// ── Phase 2.5: Markdown processing (headings + cross-links) ────────
|
|
405
|
+
const mdScanned = scannedFiles.filter(f => f.path.endsWith('.md') || f.path.endsWith('.mdx'));
|
|
406
|
+
if (mdScanned.length > 0) {
|
|
407
|
+
const mdContents = await readFileContents(repoPath, mdScanned.map(f => f.path));
|
|
408
|
+
const mdFiles = mdScanned
|
|
409
|
+
.filter(f => mdContents.has(f.path))
|
|
410
|
+
.map(f => ({ path: f.path, content: mdContents.get(f.path) }));
|
|
411
|
+
const allPathSet = new Set(allPaths);
|
|
412
|
+
const mdResult = processMarkdown(graph, mdFiles, allPathSet);
|
|
413
|
+
if (isDev) {
|
|
414
|
+
console.log(` Markdown: ${mdResult.sections} sections, ${mdResult.links} cross-links from ${mdFiles.length} files`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
75
417
|
// ── Phase 3+4: Chunked read + parse ────────────────────────────────
|
|
76
418
|
// Group parseable files into byte-budget chunks so only ~20MB of source
|
|
77
419
|
// is in memory at a time. Each chunk is: read → parse → extract → free.
|
|
@@ -163,6 +505,15 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
163
505
|
// are already registered). This trades ~5% cross-chunk resolution accuracy for
|
|
164
506
|
// 200-400MB less memory — critical for Linux-kernel-scale repos.
|
|
165
507
|
const sequentialChunkPaths = [];
|
|
508
|
+
// Pre-compute which chunks need synthesis — O(1) lookup per chunk.
|
|
509
|
+
const chunkNeedsSynthesis = chunks.map(paths => paths.some(p => {
|
|
510
|
+
const lang = getLanguageFromFilename(p);
|
|
511
|
+
return lang != null && SYNTHESIS_LANGUAGES.has(lang);
|
|
512
|
+
}));
|
|
513
|
+
// Phase 14: Collect exported type bindings for cross-file propagation
|
|
514
|
+
const exportedTypeMap = new Map();
|
|
515
|
+
// Accumulate file-scope TypeEnv bindings from workers (closes worker/sequential quality gap)
|
|
516
|
+
const workerTypeEnvBindings = [];
|
|
166
517
|
try {
|
|
167
518
|
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
|
|
168
519
|
const chunkPaths = chunks[chunkIdx];
|
|
@@ -195,6 +546,23 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
195
546
|
stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
|
|
196
547
|
});
|
|
197
548
|
}, repoPath, importCtx);
|
|
549
|
+
// ── Wildcard-import synthesis (Ruby / C/C++ / Swift / Go) + Python module aliases ─
|
|
550
|
+
// Synthesize namedImportMap entries for wildcard-import languages and build
|
|
551
|
+
// moduleAliasMap for Python namespace imports. Must run after imports are resolved
|
|
552
|
+
// (importMap is populated) but BEFORE call resolution.
|
|
553
|
+
if (chunkNeedsSynthesis[chunkIdx])
|
|
554
|
+
synthesizeWildcardImportBindings(graph, ctx);
|
|
555
|
+
// Phase 14 E1: Seed cross-file receiver types from ExportedTypeMap
|
|
556
|
+
// before call resolution — eliminates re-parse for single-hop imported receivers.
|
|
557
|
+
// NOTE: In the worker path, exportedTypeMap is empty during chunk processing
|
|
558
|
+
// (populated later in runCrossFileBindingPropagation). This block is latent —
|
|
559
|
+
// it activates only if incremental export collection is added per-chunk.
|
|
560
|
+
if (exportedTypeMap.size > 0 && ctx.namedImportMap.size > 0) {
|
|
561
|
+
const { enrichedCount } = seedCrossFileReceiverTypes(chunkWorkerData.calls, ctx.namedImportMap, exportedTypeMap);
|
|
562
|
+
if (isDev && enrichedCount > 0) {
|
|
563
|
+
console.log(`🔗 E1: Seeded ${enrichedCount} cross-file receiver types (chunk ${chunkIdx + 1})`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
198
566
|
// Calls + Heritage + Routes — resolve in parallel (no shared mutable state between them)
|
|
199
567
|
// This is safe because each writes disjoint relationship types into idempotent id-keyed Maps,
|
|
200
568
|
// and the single-threaded event loop prevents races between synchronous addRelationship calls.
|
|
@@ -227,6 +595,14 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
227
595
|
});
|
|
228
596
|
}),
|
|
229
597
|
]);
|
|
598
|
+
// Process field write assignments (synchronous, runs after calls resolve)
|
|
599
|
+
if (chunkWorkerData.assignments?.length) {
|
|
600
|
+
processAssignmentsFromExtracted(graph, chunkWorkerData.assignments, ctx, chunkWorkerData.constructorBindings);
|
|
601
|
+
}
|
|
602
|
+
// Collect TypeEnv file-scope bindings for exported type enrichment
|
|
603
|
+
if (chunkWorkerData.typeEnvBindings?.length) {
|
|
604
|
+
workerTypeEnvBindings.push(...chunkWorkerData.typeEnvBindings);
|
|
605
|
+
}
|
|
230
606
|
}
|
|
231
607
|
else {
|
|
232
608
|
await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
|
|
@@ -242,13 +618,17 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
242
618
|
await workerPool?.terminate();
|
|
243
619
|
}
|
|
244
620
|
// Sequential fallback chunks: re-read source for call/heritage resolution
|
|
621
|
+
// Synthesize wildcard import bindings once after ALL imports are processed,
|
|
622
|
+
// before any call resolution — same rationale as the worker-path inline synthesis.
|
|
623
|
+
if (sequentialChunkPaths.length > 0)
|
|
624
|
+
synthesizeWildcardImportBindings(graph, ctx);
|
|
245
625
|
for (const chunkPaths of sequentialChunkPaths) {
|
|
246
626
|
const chunkContents = await readFileContents(repoPath, chunkPaths);
|
|
247
627
|
const chunkFiles = chunkPaths
|
|
248
628
|
.filter(p => chunkContents.has(p))
|
|
249
629
|
.map(p => ({ path: p, content: chunkContents.get(p) }));
|
|
250
630
|
astCache = createASTCache(chunkFiles.length);
|
|
251
|
-
const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx);
|
|
631
|
+
const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx, undefined, exportedTypeMap);
|
|
252
632
|
await processHeritage(graph, chunkFiles, astCache, ctx);
|
|
253
633
|
if (rubyHeritage.length > 0) {
|
|
254
634
|
await processHeritageFromExtracted(graph, rubyHeritage, ctx);
|
|
@@ -262,12 +642,53 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
262
642
|
const hitRate = total > 0 ? ((rcStats.cacheHits / total) * 100).toFixed(1) : '0';
|
|
263
643
|
console.log(`🔍 Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`);
|
|
264
644
|
}
|
|
645
|
+
// ── Worker path quality enrichment: merge TypeEnv file-scope bindings into ExportedTypeMap ──
|
|
646
|
+
// Workers return file-scope bindings from their TypeEnv fixpoint (includes inferred types
|
|
647
|
+
// like `const config = getConfig()` → Config). Filter by graph isExported to match
|
|
648
|
+
// the sequential path's collectExportedBindings behavior.
|
|
649
|
+
if (workerTypeEnvBindings.length > 0) {
|
|
650
|
+
let enriched = 0;
|
|
651
|
+
for (const { filePath, bindings } of workerTypeEnvBindings) {
|
|
652
|
+
for (const [name, type] of bindings) {
|
|
653
|
+
// Verify the symbol is exported via graph node
|
|
654
|
+
const nodeId = `Function:${filePath}:${name}`;
|
|
655
|
+
const varNodeId = `Variable:${filePath}:${name}`;
|
|
656
|
+
const constNodeId = `Const:${filePath}:${name}`;
|
|
657
|
+
const node = graph.getNode(nodeId) ?? graph.getNode(varNodeId) ?? graph.getNode(constNodeId);
|
|
658
|
+
if (!node?.properties?.isExported)
|
|
659
|
+
continue;
|
|
660
|
+
let fileExports = exportedTypeMap.get(filePath);
|
|
661
|
+
if (!fileExports) {
|
|
662
|
+
fileExports = new Map();
|
|
663
|
+
exportedTypeMap.set(filePath, fileExports);
|
|
664
|
+
}
|
|
665
|
+
// Don't overwrite existing entries (Tier 0 from SymbolTable is authoritative)
|
|
666
|
+
if (!fileExports.has(name)) {
|
|
667
|
+
fileExports.set(name, type);
|
|
668
|
+
enriched++;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (isDev && enriched > 0) {
|
|
673
|
+
console.log(`🔗 Worker TypeEnv enrichment: ${enriched} fixpoint-inferred exports added to ExportedTypeMap`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// ── Phase 14 pre-pass: Final synthesis pass for whole-module-import languages ──
|
|
677
|
+
// Per-chunk synthesis (above) already ran incrementally. This final pass ensures
|
|
678
|
+
// any remaining files whose imports were not covered inline are also synthesized,
|
|
679
|
+
// and that Phase 14 type propagation has complete namedImportMap data.
|
|
680
|
+
const synthesized = synthesizeWildcardImportBindings(graph, ctx);
|
|
681
|
+
if (isDev && synthesized > 0) {
|
|
682
|
+
console.log(`🔗 Synthesized ${synthesized} additional wildcard import bindings (Go/Ruby/C++/Swift/Python)`);
|
|
683
|
+
}
|
|
684
|
+
// ── Phase 14: Cross-file binding propagation ──────────────────────
|
|
685
|
+
await runCrossFileBindingPropagation(graph, ctx, exportedTypeMap, allPaths, totalFiles, repoPath, pipelineStart, onProgress);
|
|
265
686
|
// Free import resolution context — suffix index + resolve cache no longer needed
|
|
266
687
|
// (allPathObjects and importCtx hold ~94MB+ for large repos)
|
|
267
688
|
allPathObjects.length = 0;
|
|
268
689
|
importCtx.resolveCache.clear();
|
|
269
|
-
importCtx.
|
|
270
|
-
importCtx.normalizedFileList =
|
|
690
|
+
importCtx.index = EMPTY_INDEX; // Release suffix index memory (~30MB for large repos)
|
|
691
|
+
importCtx.normalizedFileList = [];
|
|
271
692
|
let communityResult;
|
|
272
693
|
let processResult;
|
|
273
694
|
if (!options?.skipGraphPhases) {
|
|
@@ -26,6 +26,9 @@ export declare const TIER_CONFIDENCE: Record<ResolutionTier, number>;
|
|
|
26
26
|
export type ImportMap = Map<string, Set<string>>;
|
|
27
27
|
export type PackageMap = Map<string, Set<string>>;
|
|
28
28
|
export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
|
|
29
|
+
/** Maps callerFile → (moduleAlias → sourceFilePath) for Python namespace imports.
|
|
30
|
+
* e.g. `import models` in app.py → moduleAliasMap.get('app.py')?.get('models') === 'models.py' */
|
|
31
|
+
export type ModuleAliasMap = Map<string, Map<string, string>>;
|
|
29
32
|
export interface ResolutionContext {
|
|
30
33
|
/**
|
|
31
34
|
* The only resolution API. Returns all candidates at the winning tier.
|
|
@@ -40,6 +43,8 @@ export interface ResolutionContext {
|
|
|
40
43
|
readonly importMap: ImportMap;
|
|
41
44
|
readonly packageMap: PackageMap;
|
|
42
45
|
readonly namedImportMap: NamedImportMap;
|
|
46
|
+
/** Module-alias map for Python namespace imports: callerFile → (alias → sourceFile). */
|
|
47
|
+
readonly moduleAliasMap: ModuleAliasMap;
|
|
43
48
|
enableCache(filePath: string): void;
|
|
44
49
|
clearCache(): void;
|
|
45
50
|
getStats(): {
|
|
@@ -26,6 +26,7 @@ export const createResolutionContext = () => {
|
|
|
26
26
|
const importMap = new Map();
|
|
27
27
|
const packageMap = new Map();
|
|
28
28
|
const namedImportMap = new Map();
|
|
29
|
+
const moduleAliasMap = new Map();
|
|
29
30
|
// Per-file cache state
|
|
30
31
|
let cacheFile = null;
|
|
31
32
|
let cache = null;
|
|
@@ -33,10 +34,10 @@ export const createResolutionContext = () => {
|
|
|
33
34
|
let cacheMisses = 0;
|
|
34
35
|
// --- Core resolution (single implementation of tier logic) ---
|
|
35
36
|
const resolveUncached = (name, fromFile) => {
|
|
36
|
-
// Tier 1: Same file — authoritative match
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
return { candidates:
|
|
37
|
+
// Tier 1: Same file — authoritative match (returns all overloads)
|
|
38
|
+
const localDefs = symbols.lookupExactAll(fromFile, name);
|
|
39
|
+
if (localDefs.length > 0) {
|
|
40
|
+
return { candidates: localDefs, tier: 'same-file' };
|
|
40
41
|
}
|
|
41
42
|
// Get all global definitions for subsequent tiers
|
|
42
43
|
const allDefs = symbols.lookupFuzzy(name);
|
|
@@ -114,6 +115,7 @@ export const createResolutionContext = () => {
|
|
|
114
115
|
importMap.clear();
|
|
115
116
|
packageMap.clear();
|
|
116
117
|
namedImportMap.clear();
|
|
118
|
+
moduleAliasMap.clear();
|
|
117
119
|
clearCache();
|
|
118
120
|
cacheHits = 0;
|
|
119
121
|
cacheMisses = 0;
|
|
@@ -124,6 +126,7 @@ export const createResolutionContext = () => {
|
|
|
124
126
|
importMap,
|
|
125
127
|
packageMap,
|
|
126
128
|
namedImportMap,
|
|
129
|
+
moduleAliasMap,
|
|
127
130
|
enableCache,
|
|
128
131
|
clearCache,
|
|
129
132
|
getStats,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Language-specific import resolvers.
|
|
3
3
|
* Extracted from import-processor.ts for maintainability.
|
|
4
4
|
*/
|
|
5
|
-
export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve } from './utils.js';
|
|
5
|
+
export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve, EMPTY_INDEX } from './utils.js';
|
|
6
6
|
export type { SuffixIndex } from './utils.js';
|
|
7
7
|
export { KOTLIN_EXTENSIONS, appendKotlinWildcard, resolveJvmWildcard, resolveJvmMemberImport } from './jvm.js';
|
|
8
8
|
export { resolveGoPackageDir, resolveGoPackage } from './go.js';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Language-specific import resolvers.
|
|
3
3
|
* Extracted from import-processor.ts for maintainability.
|
|
4
4
|
*/
|
|
5
|
-
export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve } from './utils.js';
|
|
5
|
+
export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve, EMPTY_INDEX } from './utils.js';
|
|
6
6
|
export { KOTLIN_EXTENSIONS, appendKotlinWildcard, resolveJvmWildcard, resolveJvmMemberImport } from './jvm.js';
|
|
7
7
|
export { resolveGoPackageDir, resolveGoPackage } from './go.js';
|
|
8
8
|
export { resolveCSharpImport, resolveCSharpNamespaceDir } from './csharp.js';
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
* Handles wildcard imports, member/static imports, and Kotlin-specific patterns.
|
|
4
4
|
*/
|
|
5
5
|
import type { SuffixIndex } from './utils.js';
|
|
6
|
+
import type { SyntaxNode } from '../utils.js';
|
|
6
7
|
/** Kotlin file extensions for JVM resolver reuse */
|
|
7
8
|
export declare const KOTLIN_EXTENSIONS: readonly string[];
|
|
8
9
|
/**
|
|
9
10
|
* Append .* to a Kotlin import path if the AST has a wildcard_import sibling node.
|
|
10
11
|
* Pure function — returns a new string without mutating the input.
|
|
11
12
|
*/
|
|
12
|
-
export declare const appendKotlinWildcard: (importPath: string, importNode:
|
|
13
|
+
export declare const appendKotlinWildcard: (importPath: string, importNode: SyntaxNode) => string;
|
|
13
14
|
/**
|
|
14
15
|
* Resolve a JVM wildcard import (com.example.*) to all matching files.
|
|
15
16
|
* Works for both Java (.java) and Kotlin (.kt, .kts).
|
|
@@ -27,26 +27,42 @@ export function resolveJvmWildcard(importPath, normalizedFileList, allFileList,
|
|
|
27
27
|
const candidates = extensions.flatMap(ext => index.getFilesInDir(packagePath, ext));
|
|
28
28
|
// Filter to only direct children (no subdirectories)
|
|
29
29
|
const packageSuffix = '/' + packagePath + '/';
|
|
30
|
+
const packagePrefix = packagePath + '/';
|
|
30
31
|
return candidates.filter(f => {
|
|
31
32
|
const normalized = f.replace(/\\/g, '/');
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// Match both nested (src/models/User.kt) and root-level (models/User.kt) packages
|
|
34
|
+
let afterPkg;
|
|
35
|
+
const idx = normalized.lastIndexOf(packageSuffix);
|
|
36
|
+
if (idx >= 0) {
|
|
37
|
+
afterPkg = normalized.substring(idx + packageSuffix.length);
|
|
38
|
+
}
|
|
39
|
+
else if (normalized.startsWith(packagePrefix)) {
|
|
40
|
+
afterPkg = normalized.substring(packagePrefix.length);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
34
43
|
return false;
|
|
35
|
-
|
|
44
|
+
}
|
|
36
45
|
return !afterPkg.includes('/');
|
|
37
46
|
});
|
|
38
47
|
}
|
|
39
48
|
// Fallback: linear scan
|
|
40
49
|
const packageSuffix = '/' + packagePath + '/';
|
|
50
|
+
const packagePrefix = packagePath + '/';
|
|
41
51
|
const matches = [];
|
|
42
52
|
for (let i = 0; i < normalizedFileList.length; i++) {
|
|
43
53
|
const normalized = normalizedFileList[i];
|
|
44
|
-
if (normalized.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
if (!extensions.some(ext => normalized.endsWith(ext)))
|
|
55
|
+
continue;
|
|
56
|
+
// Match both nested (src/models/User.kt) and root-level (models/User.kt) packages
|
|
57
|
+
let afterPackage = null;
|
|
58
|
+
if (normalized.includes(packageSuffix)) {
|
|
59
|
+
afterPackage = normalized.substring(normalized.lastIndexOf(packageSuffix) + packageSuffix.length);
|
|
60
|
+
}
|
|
61
|
+
else if (normalized.startsWith(packagePrefix)) {
|
|
62
|
+
afterPackage = normalized.substring(packagePrefix.length);
|
|
63
|
+
}
|
|
64
|
+
if (afterPackage !== null && !afterPackage.includes('/')) {
|
|
65
|
+
matches.push(allFileList[i]);
|
|
50
66
|
}
|
|
51
67
|
}
|
|
52
68
|
return matches;
|