@zuvia-software-solutions/code-mapper 2.5.2 → 2.6.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/dist/cli/analyze.d.ts +0 -1
- package/dist/cli/analyze.js +1 -2
- package/dist/cli/index.js +0 -3
- package/dist/cli/refresh.js +0 -3
- package/dist/cli/tool.d.ts +0 -2
- package/dist/cli/tool.js +0 -2
- package/dist/core/embeddings/nl-embed-worker.d.ts +1 -1
- package/dist/core/embeddings/nl-embed-worker.js +1 -1
- package/dist/core/embeddings/nl-embedder.js +1 -1
- package/dist/core/incremental/refresh.d.ts +2 -4
- package/dist/core/incremental/refresh.js +20 -135
- package/dist/core/incremental/types.d.ts +0 -1
- package/dist/core/incremental/types.js +0 -1
- package/dist/core/ingestion/call-processor.d.ts +2 -3
- package/dist/core/ingestion/call-processor.js +59 -258
- package/dist/core/ingestion/pipeline.d.ts +1 -3
- package/dist/core/ingestion/pipeline.js +11 -33
- package/dist/core/ingestion/resolution-context.d.ts +1 -1
- package/dist/core/ingestion/resolution-context.js +0 -1
- package/dist/core/ingestion/utils.d.ts +5 -0
- package/dist/core/ingestion/utils.js +17 -0
- package/dist/core/ingestion/workers/parse-worker.js +7 -1
- package/dist/core/semantic/tsgo-service.d.ts +1 -1
- package/dist/core/semantic/tsgo-service.js +2 -2
- package/dist/mcp/local/local-backend.d.ts +1 -5
- package/dist/mcp/local/local-backend.js +12 -149
- package/dist/mcp/tools.js +0 -1
- package/dist/types/pipeline.d.ts +0 -2
- package/dist/types/pipeline.js +0 -1
- package/package.json +1 -1
|
@@ -6,12 +6,10 @@ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/pa
|
|
|
6
6
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
7
7
|
import { generateId } from '../../lib/utils.js';
|
|
8
8
|
import { toNodeId, toEdgeId } from '../db/schema.js';
|
|
9
|
-
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
|
|
9
|
+
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, findEnclosingClassName, SELF_RECEIVER_KEYWORDS, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
|
|
10
10
|
import { buildTypeEnv } from './type-env.js';
|
|
11
11
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
12
12
|
import { callRouters } from './call-routing.js';
|
|
13
|
-
import { TsgoService } from '../semantic/tsgo-service.js';
|
|
14
|
-
import path from 'node:path';
|
|
15
13
|
/** Walk up the AST to find the enclosing function/method, or null for top-level code */
|
|
16
14
|
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
17
15
|
let current = node.parent;
|
|
@@ -302,6 +300,12 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
302
300
|
const callForm = inferCallForm(callNode, nameNode);
|
|
303
301
|
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
|
304
302
|
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
303
|
+
// Resolve this/self/base to enclosing class name
|
|
304
|
+
if (!receiverTypeName && receiverName && SELF_RECEIVER_KEYWORDS.has(receiverName)) {
|
|
305
|
+
const className = findEnclosingClassName(callNode);
|
|
306
|
+
if (className)
|
|
307
|
+
receiverTypeName = className;
|
|
308
|
+
}
|
|
305
309
|
// Fall back to verified constructor bindings
|
|
306
310
|
if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
|
|
307
311
|
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
@@ -728,223 +732,7 @@ const lookupReceiverType = (map, funcName, varName) => {
|
|
|
728
732
|
// Fallback: file-level scope
|
|
729
733
|
return map.get(fileLevelKey);
|
|
730
734
|
};
|
|
731
|
-
/**
|
|
732
|
-
function isTypeScriptOrJavaScript(filePath) {
|
|
733
|
-
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Batch-resolve call sites via tsgo LSP before heuristic resolution.
|
|
737
|
-
*
|
|
738
|
-
* For each TS/JS call with line+column info, asks tsgo for go-to-definition.
|
|
739
|
-
* Returns a Map from callKey -> ResolveResult for calls that tsgo resolved.
|
|
740
|
-
*
|
|
741
|
-
* Call key format: "sourceId\0calledName\0callLine" — unique per call site.
|
|
742
|
-
*/
|
|
743
|
-
async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPath, onProgress) {
|
|
744
|
-
const results = new Map();
|
|
745
|
-
// Collect eligible calls (TS/JS files with line+column info)
|
|
746
|
-
const eligible = [];
|
|
747
|
-
for (const call of extractedCalls) {
|
|
748
|
-
if (isTypeScriptOrJavaScript(call.filePath) &&
|
|
749
|
-
call.callLine !== undefined &&
|
|
750
|
-
call.callColumn !== undefined) {
|
|
751
|
-
eligible.push(call);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
if (eligible.length === 0)
|
|
755
|
-
return results;
|
|
756
|
-
// Built-in receiver names that resolve to external types, not project code.
|
|
757
|
-
const BUILTIN_RECEIVERS = new Set([
|
|
758
|
-
'console', 'Math', 'JSON', 'Object', 'Array', 'String', 'Number', 'Boolean',
|
|
759
|
-
'Date', 'RegExp', 'Error', 'Promise', 'Map', 'Set', 'WeakMap', 'WeakSet',
|
|
760
|
-
'Buffer', 'process', 'globalThis', 'window', 'document', 'navigator',
|
|
761
|
-
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
|
762
|
-
'require', 'module', 'exports', '__dirname', '__filename',
|
|
763
|
-
'fs', 'path', 'os', 'url', 'util', 'crypto', 'http', 'https', 'net',
|
|
764
|
-
'child_process', 'stream', 'events', 'assert', 'zlib',
|
|
765
|
-
]);
|
|
766
|
-
// Pre-filter calls where tsgo won't add value:
|
|
767
|
-
// A. Free-form calls with unambiguous name — heuristic resolves perfectly
|
|
768
|
-
// B. Member calls on built-in receivers — tsgo always fails on these
|
|
769
|
-
const tsgoEligible = [];
|
|
770
|
-
let skippedUnambiguous = 0;
|
|
771
|
-
let skippedBuiltin = 0;
|
|
772
|
-
for (const call of eligible) {
|
|
773
|
-
if (call.callForm === 'free' || call.callForm === undefined) {
|
|
774
|
-
const resolved = ctx.resolve(call.calledName, call.filePath);
|
|
775
|
-
if (resolved && resolved.candidates.length === 1) {
|
|
776
|
-
skippedUnambiguous++;
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
if (call.callForm === 'member' && call.receiverName && BUILTIN_RECEIVERS.has(call.receiverName)) {
|
|
781
|
-
skippedBuiltin++;
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
tsgoEligible.push(call);
|
|
785
|
-
}
|
|
786
|
-
// Regroup filtered calls by file
|
|
787
|
-
const tsgoByFile = new Map();
|
|
788
|
-
for (const call of tsgoEligible) {
|
|
789
|
-
let list = tsgoByFile.get(call.filePath);
|
|
790
|
-
if (!list) {
|
|
791
|
-
list = [];
|
|
792
|
-
tsgoByFile.set(call.filePath, list);
|
|
793
|
-
}
|
|
794
|
-
list.push(call);
|
|
795
|
-
}
|
|
796
|
-
const t0 = Date.now();
|
|
797
|
-
const skippedTotal = skippedUnambiguous + skippedBuiltin;
|
|
798
|
-
// Adaptive parallelism — conservative to avoid freezing the machine.
|
|
799
|
-
// Each tsgo LSP process loads the full project into memory (~1.5-3GB for
|
|
800
|
-
// large codebases) and pins a CPU core at 100%, so we cap aggressively.
|
|
801
|
-
const osModule = await import('os');
|
|
802
|
-
const cpuCount = osModule.cpus().length;
|
|
803
|
-
const freeMemGB = osModule.freemem() / (1024 * 1024 * 1024);
|
|
804
|
-
const maxByCpu = Math.max(1, Math.floor(cpuCount * 0.5));
|
|
805
|
-
const maxByMemory = Math.max(1, Math.floor(freeMemGB / 2)); // ~2GB per process
|
|
806
|
-
const maxByWorkload = Math.max(1, Math.floor(tsgoByFile.size / 100));
|
|
807
|
-
const HARD_CAP = 4; // never more than 4 tsgo processes regardless of hardware
|
|
808
|
-
const actualWorkers = Math.min(maxByCpu, maxByMemory, maxByWorkload, HARD_CAP);
|
|
809
|
-
if (process.env['CODE_MAPPER_VERBOSE']) {
|
|
810
|
-
console.error(`Code Mapper: tsgo resolving ${tsgoEligible.length} calls across ${tsgoByFile.size} files with ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''} (skipped ${skippedTotal}: ${skippedUnambiguous} unambiguous, ${skippedBuiltin} builtin)...`);
|
|
811
|
-
}
|
|
812
|
-
// Dynamic dispatch: shared queue sorted by call count descending
|
|
813
|
-
const fileEntries = [...tsgoByFile.entries()];
|
|
814
|
-
fileEntries.sort((a, b) => b[1].length - a[1].length);
|
|
815
|
-
let totalFilesProcessed = 0;
|
|
816
|
-
let nextFileIdx = 0;
|
|
817
|
-
const tsgoTotalFiles = tsgoByFile.size;
|
|
818
|
-
const getNextFile = () => {
|
|
819
|
-
if (nextFileIdx >= fileEntries.length)
|
|
820
|
-
return null;
|
|
821
|
-
return fileEntries[nextFileIdx++];
|
|
822
|
-
};
|
|
823
|
-
const resolveWorker = async (service) => {
|
|
824
|
-
const sliceResults = new Map();
|
|
825
|
-
let sliceResolved = 0;
|
|
826
|
-
let sliceFailed = 0;
|
|
827
|
-
let entry;
|
|
828
|
-
while ((entry = getNextFile()) !== null) {
|
|
829
|
-
// Bail out early if tsgo process died — no point sending more requests
|
|
830
|
-
if (!service.isReady())
|
|
831
|
-
break;
|
|
832
|
-
const [filePath, calls] = entry;
|
|
833
|
-
totalFilesProcessed++;
|
|
834
|
-
if (totalFilesProcessed % 25 === 0) {
|
|
835
|
-
onProgress?.(totalFilesProcessed, tsgoTotalFiles, actualWorkers);
|
|
836
|
-
}
|
|
837
|
-
const absFilePath = path.resolve(repoPath, filePath);
|
|
838
|
-
// Pipeline: fire all definition requests for this file concurrently.
|
|
839
|
-
// The LSP server processes them serially, but we eliminate round-trip
|
|
840
|
-
// latency between requests — major speedup on large files.
|
|
841
|
-
const BATCH = 50;
|
|
842
|
-
for (let i = 0; i < calls.length; i += BATCH) {
|
|
843
|
-
const batch = calls.slice(i, i + BATCH);
|
|
844
|
-
const defs = await Promise.all(batch.map(call => service.resolveDefinition(absFilePath, call.callLine - 1, call.callColumn)
|
|
845
|
-
.catch(() => null)));
|
|
846
|
-
for (let j = 0; j < batch.length; j++) {
|
|
847
|
-
const call = batch[j];
|
|
848
|
-
const def = defs[j];
|
|
849
|
-
if (!def) {
|
|
850
|
-
sliceFailed++;
|
|
851
|
-
continue;
|
|
852
|
-
}
|
|
853
|
-
const targetSymbols = ctx.symbols.lookupAllInFile(def.filePath);
|
|
854
|
-
if (targetSymbols.length === 0) {
|
|
855
|
-
sliceFailed++;
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
let bestMatch;
|
|
859
|
-
for (const sym of targetSymbols) {
|
|
860
|
-
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
861
|
-
if (node && node.properties.startLine === def.line) {
|
|
862
|
-
bestMatch = sym;
|
|
863
|
-
break;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (!bestMatch) {
|
|
867
|
-
for (const sym of targetSymbols) {
|
|
868
|
-
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
869
|
-
if (node) {
|
|
870
|
-
const sl = node.properties.startLine;
|
|
871
|
-
const el = node.properties.endLine;
|
|
872
|
-
if (sl !== undefined && el !== undefined && def.line >= sl && def.line <= el) {
|
|
873
|
-
bestMatch = sym;
|
|
874
|
-
break;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
if (bestMatch) {
|
|
880
|
-
if (bestMatch.nodeId === call.sourceId) {
|
|
881
|
-
sliceFailed++;
|
|
882
|
-
continue;
|
|
883
|
-
}
|
|
884
|
-
const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
|
|
885
|
-
sliceResults.set(callKey, {
|
|
886
|
-
nodeId: bestMatch.nodeId,
|
|
887
|
-
confidence: TIER_CONFIDENCE['tsgo-resolved'],
|
|
888
|
-
reason: 'tsgo-lsp',
|
|
889
|
-
});
|
|
890
|
-
sliceResolved++;
|
|
891
|
-
}
|
|
892
|
-
else {
|
|
893
|
-
sliceFailed++;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
service.notifyFileDeleted(absFilePath);
|
|
898
|
-
}
|
|
899
|
-
return { resolved: sliceResolved, failed: sliceFailed, results: sliceResults };
|
|
900
|
-
};
|
|
901
|
-
let resolved = 0;
|
|
902
|
-
let failed = 0;
|
|
903
|
-
if (actualWorkers === 1) {
|
|
904
|
-
const outcome = await resolveWorker(tsgoService);
|
|
905
|
-
resolved = outcome.resolved;
|
|
906
|
-
failed = outcome.failed;
|
|
907
|
-
for (const [k, v] of outcome.results)
|
|
908
|
-
results.set(k, v);
|
|
909
|
-
}
|
|
910
|
-
else {
|
|
911
|
-
const extraServices = [];
|
|
912
|
-
try {
|
|
913
|
-
const startPromises = [];
|
|
914
|
-
for (let i = 1; i < actualWorkers; i++) {
|
|
915
|
-
startPromises.push((async () => {
|
|
916
|
-
const svc = new TsgoService(repoPath);
|
|
917
|
-
if (await svc.start())
|
|
918
|
-
return svc;
|
|
919
|
-
return null;
|
|
920
|
-
})());
|
|
921
|
-
}
|
|
922
|
-
const started = await Promise.all(startPromises);
|
|
923
|
-
for (const svc of started) {
|
|
924
|
-
if (svc)
|
|
925
|
-
extraServices.push(svc);
|
|
926
|
-
}
|
|
927
|
-
const services = [tsgoService, ...extraServices];
|
|
928
|
-
if (process.env['CODE_MAPPER_VERBOSE'])
|
|
929
|
-
console.error(`Code Mapper: ${services.length} tsgo processes ready, resolving with dynamic dispatch...`);
|
|
930
|
-
const outcomes = await Promise.all(services.map(svc => resolveWorker(svc)));
|
|
931
|
-
for (const outcome of outcomes) {
|
|
932
|
-
resolved += outcome.resolved;
|
|
933
|
-
failed += outcome.failed;
|
|
934
|
-
for (const [k, v] of outcome.results)
|
|
935
|
-
results.set(k, v);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
finally {
|
|
939
|
-
for (const svc of extraServices)
|
|
940
|
-
svc.stop();
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
const elapsed = Date.now() - t0;
|
|
944
|
-
if (process.env['CODE_MAPPER_VERBOSE'])
|
|
945
|
-
console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable, ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''})`);
|
|
946
|
-
return results;
|
|
947
|
-
}
|
|
735
|
+
/** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
|
|
948
736
|
/** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
|
|
949
737
|
const GENERIC_MEMBER_METHODS_WORKER = new Set([
|
|
950
738
|
'has', 'get', 'set', 'add', 'remove', 'delete', 'close', 'stop', 'clear', 'reset',
|
|
@@ -958,7 +746,7 @@ const GENERIC_MEMBER_METHODS_WORKER = new Set([
|
|
|
958
746
|
'json', 'text', 'blob', 'status', 'send', 'end', // HTTP response
|
|
959
747
|
]);
|
|
960
748
|
/** Resolve pre-extracted call sites from workers (no AST parsing needed) */
|
|
961
|
-
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings
|
|
749
|
+
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
|
|
962
750
|
// Scope-aware receiver types keyed by filePath -> scope\0varName -> typeName
|
|
963
751
|
const fileReceiverTypes = new Map();
|
|
964
752
|
if (constructorBindings) {
|
|
@@ -978,10 +766,41 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
978
766
|
}
|
|
979
767
|
list.push(call);
|
|
980
768
|
}
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
769
|
+
// Pre-build indexes for O(1) lookups in the hot loop.
|
|
770
|
+
// Without these, interface dispatch does graph.relationships.filter() per call = O(calls × edges).
|
|
771
|
+
const implementsIndex = new Map(); // interfaceNodeId → [classNodeId, ...]
|
|
772
|
+
const hasMethodIndex = new Map(); // classNodeId → methods
|
|
773
|
+
const reverseImportIndex = new Map(); // importedFile → [importingFile, ...]
|
|
774
|
+
for (const rel of graph.iterRelationships()) {
|
|
775
|
+
if (rel.type === 'IMPLEMENTS') {
|
|
776
|
+
let arr = implementsIndex.get(rel.targetId);
|
|
777
|
+
if (!arr) {
|
|
778
|
+
arr = [];
|
|
779
|
+
implementsIndex.set(rel.targetId, arr);
|
|
780
|
+
}
|
|
781
|
+
arr.push(rel.sourceId);
|
|
782
|
+
}
|
|
783
|
+
else if (rel.type === 'HAS_METHOD') {
|
|
784
|
+
const methodNode = graph.getNode(rel.targetId);
|
|
785
|
+
if (methodNode) {
|
|
786
|
+
let arr = hasMethodIndex.get(rel.sourceId);
|
|
787
|
+
if (!arr) {
|
|
788
|
+
arr = [];
|
|
789
|
+
hasMethodIndex.set(rel.sourceId, arr);
|
|
790
|
+
}
|
|
791
|
+
arr.push({ targetId: rel.targetId, name: methodNode.properties.name });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
for (const [file, importedFiles] of ctx.importMap) {
|
|
796
|
+
for (const imported of importedFiles) {
|
|
797
|
+
let arr = reverseImportIndex.get(imported);
|
|
798
|
+
if (!arr) {
|
|
799
|
+
arr = [];
|
|
800
|
+
reverseImportIndex.set(imported, arr);
|
|
801
|
+
}
|
|
802
|
+
arr.push(file);
|
|
803
|
+
}
|
|
985
804
|
}
|
|
986
805
|
const totalFiles = byFile.size;
|
|
987
806
|
let filesProcessed = 0;
|
|
@@ -1031,16 +850,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1031
850
|
if (effectiveCall.callForm === 'member' && !effectiveCall.receiverTypeName && GENERIC_MEMBER_METHODS_WORKER.has(effectiveCall.calledName)) {
|
|
1032
851
|
continue;
|
|
1033
852
|
}
|
|
1034
|
-
|
|
1035
|
-
let resolved;
|
|
1036
|
-
if (tsgoResolved && effectiveCall.callLine !== undefined) {
|
|
1037
|
-
const callKey = `${effectiveCall.sourceId}\0${effectiveCall.calledName}\0${effectiveCall.callLine}`;
|
|
1038
|
-
resolved = tsgoResolved.get(callKey);
|
|
1039
|
-
}
|
|
1040
|
-
// Fall through to heuristic resolution if tsgo didn't resolve
|
|
1041
|
-
if (!resolved) {
|
|
1042
|
-
resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
|
|
1043
|
-
}
|
|
853
|
+
let resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
|
|
1044
854
|
// RC-I: Interface dispatch — when receiver type is an Interface, find implementations.
|
|
1045
855
|
// Strategy 1: Classes with IMPLEMENTS edges to the interface.
|
|
1046
856
|
// Strategy 2: Factory functions whose return type matches the interface name.
|
|
@@ -1051,38 +861,29 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
1051
861
|
const interfaceDefs = receiverResolved.candidates.filter(d => d.type === 'Interface' || d.type === 'Trait');
|
|
1052
862
|
if (interfaceDefs.length > 0) {
|
|
1053
863
|
for (const ifaceDef of interfaceDefs) {
|
|
1054
|
-
// Strategy 1: Class-based — find IMPLEMENTS edges
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
864
|
+
// Strategy 1: Class-based — find IMPLEMENTS edges via pre-built index
|
|
865
|
+
const implClassIds = implementsIndex.get(toNodeId(ifaceDef.nodeId));
|
|
866
|
+
if (implClassIds) {
|
|
867
|
+
for (const classId of implClassIds) {
|
|
868
|
+
const methods = hasMethodIndex.get(classId);
|
|
869
|
+
if (methods) {
|
|
870
|
+
const match = methods.find(m => m.name === effectiveCall.calledName);
|
|
871
|
+
if (match) {
|
|
872
|
+
resolved = { nodeId: match.targetId, confidence: 0.85, reason: 'interface-dispatch' };
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
1063
875
|
}
|
|
1064
876
|
}
|
|
1065
|
-
if (resolved)
|
|
1066
|
-
break;
|
|
1067
877
|
}
|
|
1068
878
|
if (resolved)
|
|
1069
879
|
break;
|
|
1070
880
|
// Strategy 2: Factory/closure pattern — find functions returning this interface
|
|
1071
|
-
// e.g. createEventBus(): EventBus → the closure has emit/subscribe as inner functions
|
|
1072
|
-
// Look for the method name in files that import the interface's file
|
|
1073
881
|
if (!resolved) {
|
|
1074
882
|
const methodName = effectiveCall.calledName;
|
|
1075
883
|
const ifaceFile = ifaceDef.filePath;
|
|
1076
|
-
|
|
1077
|
-
const importingFiles = [];
|
|
1078
|
-
for (const [file, importedFiles] of ctx.importMap) {
|
|
1079
|
-
if (importedFiles.has(ifaceFile)) {
|
|
1080
|
-
importingFiles.push(file);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
884
|
+
const importingFiles = reverseImportIndex.get(ifaceFile) ?? [];
|
|
1083
885
|
// Also check the interface's own file
|
|
1084
|
-
importingFiles
|
|
1085
|
-
for (const file of importingFiles) {
|
|
886
|
+
for (const file of [...importingFiles, ifaceFile]) {
|
|
1086
887
|
const method = ctx.symbols.lookupExactFull(file, methodName);
|
|
1087
888
|
if (method && (method.type === 'Function' || method.type === 'Method')) {
|
|
1088
889
|
resolved = { nodeId: method.nodeId, confidence: 0.75, reason: 'interface-factory-dispatch' };
|
|
@@ -1371,7 +1172,7 @@ export const createProvidesEdges = async (graph, ctx) => {
|
|
|
1371
1172
|
* Creates CALLS edges: InterfaceMethod → ImplementationFunction.
|
|
1372
1173
|
*
|
|
1373
1174
|
* This makes the call chain traversable:
|
|
1374
|
-
* register() →
|
|
1175
|
+
* register() → EventBus.emit (Method) →[dispatch]→ emit (Function in event-bus.ts)
|
|
1375
1176
|
*/
|
|
1376
1177
|
export const resolveInterfaceDispatches = async (graph, ctx) => {
|
|
1377
1178
|
let edgesCreated = 0;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
/** @file pipeline.ts @description Main ingestion pipeline that orchestrates scanning, parsing, resolution, community detection, and process detection across chunked file batches */
|
|
2
2
|
import type { PipelineProgress, PipelineResult } from '../../types/pipeline.js';
|
|
3
|
-
export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void
|
|
4
|
-
tsgo?: boolean;
|
|
5
|
-
}) => Promise<PipelineResult>;
|
|
3
|
+
export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void) => Promise<PipelineResult>;
|
|
@@ -20,7 +20,6 @@ import path from 'node:path';
|
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
21
|
import { memoryGuard } from '../../lib/memory-guard.js';
|
|
22
22
|
import { toNodeId, toEdgeId } from '../db/schema.js';
|
|
23
|
-
import { getTsgoService } from '../semantic/tsgo-service.js';
|
|
24
23
|
const verbose = (...args) => {
|
|
25
24
|
if (process.env['CODE_MAPPER_VERBOSE'])
|
|
26
25
|
console.error(...args);
|
|
@@ -33,7 +32,7 @@ const DEFAULT_CHUNK_BYTE_BUDGET = 50 * 1024 * 1024;
|
|
|
33
32
|
const WORKING_MEMORY_MULTIPLIER = 20;
|
|
34
33
|
// Max AST trees to keep in LRU cache
|
|
35
34
|
const AST_CACHE_CAP = 50;
|
|
36
|
-
export const runPipelineFromRepo = async (repoPath, onProgress
|
|
35
|
+
export const runPipelineFromRepo = async (repoPath, onProgress) => {
|
|
37
36
|
const graph = createKnowledgeGraph();
|
|
38
37
|
const ctx = createResolutionContext();
|
|
39
38
|
const symbolTable = ctx.symbols;
|
|
@@ -272,7 +271,6 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
|
|
|
272
271
|
}
|
|
273
272
|
astCache.clear();
|
|
274
273
|
}
|
|
275
|
-
let tsgoWasUsed = false;
|
|
276
274
|
// Phase B: Resolve ALL deferred calls now that every symbol is registered
|
|
277
275
|
// Progress range: 70-82% (advancing, not fixed)
|
|
278
276
|
if (allExtractedCalls.length > 0) {
|
|
@@ -282,35 +280,15 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
|
|
|
282
280
|
message: `Resolving ${allExtractedCalls.length} calls across all files...`,
|
|
283
281
|
stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
|
|
284
282
|
});
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
// tsgo is optional — if @typescript/native-preview isn't installed, skip silently
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
try {
|
|
300
|
-
await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total, workerCount) => {
|
|
301
|
-
const callPercent = 70 + Math.round((current / Math.max(total, 1)) * 12);
|
|
302
|
-
onProgress({
|
|
303
|
-
phase: 'calls',
|
|
304
|
-
percent: callPercent,
|
|
305
|
-
message: `Resolving calls: ${current}/${total} files...`,
|
|
306
|
-
stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount, ...(workerCount ? { workerCount } : {}) },
|
|
307
|
-
});
|
|
308
|
-
}, allConstructorBindings.length > 0 ? allConstructorBindings : undefined, tsgoService, repoPath);
|
|
309
|
-
}
|
|
310
|
-
finally {
|
|
311
|
-
// Stop tsgo after call resolution completes
|
|
312
|
-
tsgoService?.stop();
|
|
313
|
-
}
|
|
283
|
+
await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total, workerCount) => {
|
|
284
|
+
const callPercent = 70 + Math.round((current / Math.max(total, 1)) * 12);
|
|
285
|
+
onProgress({
|
|
286
|
+
phase: 'calls',
|
|
287
|
+
percent: callPercent,
|
|
288
|
+
message: `Resolving calls: ${current}/${total} files...`,
|
|
289
|
+
stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount, ...(workerCount ? { workerCount } : {}) },
|
|
290
|
+
});
|
|
291
|
+
}, allConstructorBindings.length > 0 ? allConstructorBindings : undefined);
|
|
314
292
|
}
|
|
315
293
|
{
|
|
316
294
|
const rcStats = ctx.getStats();
|
|
@@ -446,7 +424,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
|
|
|
446
424
|
},
|
|
447
425
|
});
|
|
448
426
|
astCache.clear();
|
|
449
|
-
return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult
|
|
427
|
+
return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult };
|
|
450
428
|
}
|
|
451
429
|
catch (error) {
|
|
452
430
|
cleanup();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
|
|
13
13
|
import type { NamedImportBinding } from './import-processor.js';
|
|
14
|
-
export type ResolutionTier = '
|
|
14
|
+
export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
|
|
15
15
|
export interface TieredCandidates {
|
|
16
16
|
readonly candidates: readonly SymbolDefinition[];
|
|
17
17
|
readonly tier: ResolutionTier;
|
|
@@ -15,7 +15,6 @@ import { isFileInPackageDir } from './import-processor.js';
|
|
|
15
15
|
import { walkBindingChain } from './named-binding-extraction.js';
|
|
16
16
|
// Confidence scores per resolution tier
|
|
17
17
|
export const TIER_CONFIDENCE = {
|
|
18
|
-
'tsgo-resolved': 0.99,
|
|
19
18
|
'same-file': 0.95,
|
|
20
19
|
'import-scoped': 0.9,
|
|
21
20
|
'global': 0.5,
|
|
@@ -46,6 +46,11 @@ export declare const CLASS_CONTAINER_TYPES: Set<string>;
|
|
|
46
46
|
export declare const CONTAINER_TYPE_TO_LABEL: Record<string, string>;
|
|
47
47
|
/** Walk up AST to find enclosing class/struct/impl and return its generateId (handles Go receivers) */
|
|
48
48
|
export declare const findEnclosingClassId: (node: any, filePath: string) => string | null;
|
|
49
|
+
/** Self-receiver keywords that should resolve to the enclosing class */
|
|
50
|
+
export declare const SELF_RECEIVER_KEYWORDS: Set<string>;
|
|
51
|
+
/** Walk up AST to find enclosing class/struct/impl and return its NAME (not ID).
|
|
52
|
+
* Used to resolve `this.method()` / `self.method()` receiver type. */
|
|
53
|
+
export declare const findEnclosingClassName: (node: any) => string | null;
|
|
49
54
|
/** Extract function name and label from a function/method AST node (handles C/C++ qualified_identifier) */
|
|
50
55
|
export declare const extractFunctionName: (node: any) => {
|
|
51
56
|
funcName: string | null;
|
|
@@ -369,6 +369,23 @@ export const findEnclosingClassId = (node, filePath) => {
|
|
|
369
369
|
}
|
|
370
370
|
return null;
|
|
371
371
|
};
|
|
372
|
+
/** Self-receiver keywords that should resolve to the enclosing class */
|
|
373
|
+
export const SELF_RECEIVER_KEYWORDS = new Set(['this', 'self', 'base', 'parent']);
|
|
374
|
+
/** Walk up AST to find enclosing class/struct/impl and return its NAME (not ID).
|
|
375
|
+
* Used to resolve `this.method()` / `self.method()` receiver type. */
|
|
376
|
+
export const findEnclosingClassName = (node) => {
|
|
377
|
+
let current = node.parent;
|
|
378
|
+
while (current) {
|
|
379
|
+
if (CLASS_CONTAINER_TYPES.has(current.type)) {
|
|
380
|
+
const nameNode = current.childForFieldName?.('name')
|
|
381
|
+
?? current.children?.find((c) => c.type === 'type_identifier' || c.type === 'identifier' || c.type === 'name' || c.type === 'constant');
|
|
382
|
+
if (nameNode)
|
|
383
|
+
return nameNode.text;
|
|
384
|
+
}
|
|
385
|
+
current = current.parent;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
};
|
|
372
389
|
/** Extract function name and label from a function/method AST node (handles C/C++ qualified_identifier) */
|
|
373
390
|
export const extractFunctionName = (node) => {
|
|
374
391
|
let funcName = null;
|
|
@@ -34,7 +34,7 @@ try {
|
|
|
34
34
|
Kotlin = _require('tree-sitter-kotlin');
|
|
35
35
|
}
|
|
36
36
|
catch { }
|
|
37
|
-
import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, CALL_EXPRESSION_TYPES, extractCallChain, } from '../utils.js';
|
|
37
|
+
import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, findEnclosingClassName, SELF_RECEIVER_KEYWORDS, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, CALL_EXPRESSION_TYPES, extractCallChain, } from '../utils.js';
|
|
38
38
|
import { buildTypeEnv } from '../type-env.js';
|
|
39
39
|
import { isNodeExported } from '../export-detection.js';
|
|
40
40
|
import { detectFrameworkFromAST } from '../framework-detection.js';
|
|
@@ -851,6 +851,12 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
851
851
|
const callForm = callForm_pre;
|
|
852
852
|
let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
|
|
853
853
|
let receiverTypeName = receiverName ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
854
|
+
// Resolve this/self/base to enclosing class name
|
|
855
|
+
if (!receiverTypeName && receiverName && SELF_RECEIVER_KEYWORDS.has(receiverName)) {
|
|
856
|
+
const className = findEnclosingClassName(callNode);
|
|
857
|
+
if (className)
|
|
858
|
+
receiverTypeName = className;
|
|
859
|
+
}
|
|
854
860
|
let receiverCallChain;
|
|
855
861
|
// When the receiver is complex (call chain or member chain),
|
|
856
862
|
// extractReceiverName returns undefined. Walk the AST to extract
|
|
@@ -44,7 +44,7 @@ export declare class TsgoService {
|
|
|
44
44
|
/** Whether the server is running and ready for queries */
|
|
45
45
|
isReady(): boolean;
|
|
46
46
|
/** Resolve what a symbol at a given position points to (go-to-definition) */
|
|
47
|
-
resolveDefinition(absFilePath: string, line: number, character: number): Promise<TsgoDefinition | null>;
|
|
47
|
+
resolveDefinition(absFilePath: string, line: number, character: number, timeoutMs?: number): Promise<TsgoDefinition | null>;
|
|
48
48
|
/** Find all references to the symbol at the given position */
|
|
49
49
|
findReferences(absFilePath: string, line: number, character: number): Promise<TsgoReference[]>;
|
|
50
50
|
/** Get the type signature at a position (hover) */
|
|
@@ -68,7 +68,7 @@ export class TsgoService {
|
|
|
68
68
|
return this.ready;
|
|
69
69
|
}
|
|
70
70
|
/** Resolve what a symbol at a given position points to (go-to-definition) */
|
|
71
|
-
async resolveDefinition(absFilePath, line, character) {
|
|
71
|
+
async resolveDefinition(absFilePath, line, character, timeoutMs = 3000) {
|
|
72
72
|
if (!this.ready) {
|
|
73
73
|
console.error('[tsgo-service] resolveDefinition called but not ready');
|
|
74
74
|
return null;
|
|
@@ -77,7 +77,7 @@ export class TsgoService {
|
|
|
77
77
|
const resp = await this.request('textDocument/definition', {
|
|
78
78
|
textDocument: { uri: this.fileUri(absFilePath) },
|
|
79
79
|
position: { line, character },
|
|
80
|
-
},
|
|
80
|
+
}, timeoutMs);
|
|
81
81
|
if (!resp) {
|
|
82
82
|
verbose('definition timeout', absFilePath, line, character);
|
|
83
83
|
return null;
|
|
@@ -38,14 +38,10 @@ export declare class LocalBackend {
|
|
|
38
38
|
/** Per-repo promise chain that serializes ensureFresh calls.
|
|
39
39
|
* Prevents race: Call 2 skipping refresh while Call 1 is still writing. */
|
|
40
40
|
private refreshLocks;
|
|
41
|
-
/** Per-repo tsgo LSP service instances for live semantic enrichment */
|
|
42
|
-
private tsgoServices;
|
|
43
41
|
/** Per-repo in-memory embedding cache: nodeId → Float32Array (256-dim) */
|
|
44
42
|
private embeddingCaches;
|
|
45
43
|
/** Per-repo in-memory NL embedding cache: includes source text for match_reason */
|
|
46
44
|
private nlEmbeddingCaches;
|
|
47
|
-
/** Get (or lazily start) a tsgo LSP service for a repo. Returns null if unavailable. */
|
|
48
|
-
private getTsgo;
|
|
49
45
|
/** Get (or lazily open) the SQLite database for a repo. */
|
|
50
46
|
private getDb;
|
|
51
47
|
/** Load all embeddings into memory for fast vector search */
|
|
@@ -104,7 +100,7 @@ export declare class LocalBackend {
|
|
|
104
100
|
lastCommit: string;
|
|
105
101
|
stats?: any;
|
|
106
102
|
}>>;
|
|
107
|
-
/** Find the narrowest symbol node enclosing a given file position
|
|
103
|
+
/** Find the narrowest symbol node enclosing a given file position */
|
|
108
104
|
private findNodeAtPosition;
|
|
109
105
|
/** Extract signature from content. For interfaces/types, returns the full body (fields ARE the signature). */
|
|
110
106
|
private extractSignature;
|