Skip to content

Commit 377a1ff

Browse files
committed
fix: resolve merge conflicts with main
2 parents 8b74716 + 0754216 commit 377a1ff

25 files changed

Lines changed: 1154 additions & 146 deletions

src/domain/graph/builder/call-resolver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,11 @@ export function resolveByMethodOrGlobal(
198198
if (call.receiver && callerName) {
199199
const dotIdx = callerName.lastIndexOf('.');
200200
if (dotIdx > -1) {
201-
const callerClass = callerName.slice(0, dotIdx);
201+
// Extract only the segment immediately before the method name so that
202+
// 'Namespace.ClassName.method' yields 'ClassName', not 'Namespace.ClassName'.
203+
// Symbols are stored under their bare class name, not their qualified path.
204+
const prevDot = callerName.lastIndexOf('.', dotIdx - 1);
205+
const callerClass = callerName.slice(prevDot + 1, dotIdx);
202206
const qualifiedName = `${callerClass}.${call.name}`;
203207
const sameClass = lookup
204208
.byName(qualifiedName)

src/domain/graph/builder/incremental.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,14 +538,15 @@ function buildCallEdges(
538538
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
539539

540540
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
541-
let { targets, importedFrom } = resolveCallTargets(
541+
const { targets: initialTargets, importedFrom } = resolveCallTargets(
542542
lookup,
543543
call,
544544
relPath,
545545
importedNames,
546546
typeMap,
547547
caller.callerName,
548548
);
549+
let targets = initialTargets;
549550

550551
if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
551552
const dotIdx = caller.callerName.indexOf('.');
@@ -583,7 +584,11 @@ function buildCallEdges(
583584
}
584585
}
585586
if (targets.length === 0) {
586-
const sameFile = lookup.byNameAndFile(call.name, relPath);
587+
// Narrow to function/method kinds only to avoid matching unrelated
588+
// variables or classes that share a name in the same file.
589+
const sameFile = lookup
590+
.byNameAndFile(call.name, relPath)
591+
.filter((n) => n.kind === 'function' || n.kind === 'method');
587592
if (sameFile.length > 0) {
588593
targets = [...sameFile];
589594
}

src/domain/graph/builder/stages/native-orchestrator.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,51 @@ async function runPostNativePrototypeMethods(
669669

670670
db.transaction(() => batchInsertNodes(db, newNodeRows))();
671671

672+
// ── Caller-only second pass (#1371) ──────────────────────────────────────
673+
// `wasmResults` only covers `protoFiles` (definition files). A file that
674+
// only *calls* a newly-inserted method (e.g. `f.method()`) was excluded from
675+
// the pre-filter, so its call edges to the new nodes are silently dropped.
676+
// After node insertion we know the method name suffixes; text-search the
677+
// remaining JS/TS files and WASM-parse any that contain a matching call.
678+
const newMethodSuffixes = new Set(
679+
newDefs.map((d) => {
680+
const dotIdx = d.name.indexOf('.');
681+
return dotIdx !== -1 ? d.name.slice(dotIdx + 1) : d.name;
682+
}),
683+
);
684+
685+
let mergedWasmResults = wasmResults;
686+
if (newMethodSuffixes.size > 0) {
687+
// Pre-compile patterns once — avoids re-compiling up to newMethodSuffixes.size
688+
// regexes on every file in the scan loop.
689+
const suffixPatterns = [...newMethodSuffixes].map((m) => {
690+
const escaped = m.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
691+
return new RegExp(`\\.${escaped}\\s*\\(`);
692+
});
693+
const protoFileSet = new Set(protoFiles);
694+
const callerCandidateAbs: string[] = [];
695+
for (const relPath of jsFiles) {
696+
if (protoFileSet.has(relPath)) continue; // already parsed in first pass
697+
try {
698+
const content = readFileSafe(path.join(rootDir, relPath));
699+
const matchesAny = suffixPatterns.some((re) => re.test(content));
700+
if (matchesAny) callerCandidateAbs.push(path.join(rootDir, relPath));
701+
} catch {
702+
/* skip unreadable files */
703+
}
704+
}
705+
if (callerCandidateAbs.length > 0) {
706+
try {
707+
const callerWasmResults = await parseFilesWasmForBackfill(callerCandidateAbs, rootDir);
708+
if (callerWasmResults.size > 0) {
709+
mergedWasmResults = new Map([...wasmResults, ...callerWasmResults]);
710+
}
711+
} catch (e) {
712+
debug(`runPostNativePrototypeMethods: caller-only WASM parse failed: ${toErrorMessage(e)}`);
713+
}
714+
}
715+
}
716+
672717
// Build a name → node lookup from all DB nodes (including newly inserted ones).
673718
type NodeEntry = { id: number; file: string; kind: string };
674719
const byNameMap = new Map<string, NodeEntry[]>();
@@ -716,14 +761,13 @@ async function runPostNativePrototypeMethods(
716761
// zero benefit and could OOM on large repositories.
717762
const seenByPair = new Set<string>();
718763

719-
// Resolve call edges in every file — not just those that define new func-prop
720-
// methods. A caller in app.js calling a method defined in lib.js
721-
// would be silently missed if we only scanned definition files.
722-
// The newNodeIds guard inside the loop already prevents duplicate edges.
764+
// Resolve call edges across all parsed files (definition files + caller-only
765+
// files discovered in the second WASM pass above). The newNodeIds guard inside
766+
// the loop prevents emitting duplicate edges for nodes the Rust engine already built.
723767
const newEdgeRows: unknown[][] = [];
724768
const fileNodeStmt = db.prepare(`SELECT id FROM nodes WHERE kind = 'file' AND file = ?`);
725769

726-
for (const [relPath, symbols] of wasmResults) {
770+
for (const [relPath, symbols] of mergedWasmResults) {
727771
const fileNodeRow = fileNodeStmt.get(relPath) as { id: number } | undefined;
728772
if (!fileNodeRow) continue;
729773

@@ -734,14 +778,32 @@ async function runPostNativePrototypeMethods(
734778

735779
const caller = findCaller(lookup, call, symbols.definitions ?? [], relPath, fileNodeRow);
736780

737-
const targets = resolveByMethodOrGlobal(
781+
let targets = resolveByMethodOrGlobal(
738782
lookup,
739783
call,
740784
relPath,
741785
typeMap as Map<string, unknown>,
742786
caller.callerName,
743787
);
744788

789+
// Direct receiver.method fallback: caller-only files often lack typeMap entries
790+
// for the receiver (e.g. `f.process()` where `f` isn't declared in the file).
791+
// Try qualified-name lookup scoped to newly-inserted nodes to avoid false positives.
792+
// Note: `call.receiver` is always truthy here — the `if (!call.receiver) continue`
793+
// guard above ensures we never reach this point with a falsy receiver.
794+
if (
795+
targets.length === 0 &&
796+
call.receiver !== 'this' &&
797+
call.receiver !== 'super' &&
798+
call.receiver !== 'self'
799+
) {
800+
const qualifiedName = `${call.receiver}.${call.name}`;
801+
const direct = lookup
802+
.byName(qualifiedName)
803+
.filter((n) => n.kind === 'method' && newNodeIds.has(n.id));
804+
if (direct.length > 0) targets = direct;
805+
}
806+
745807
for (const t of targets) {
746808
// Only emit edges to newly-inserted func-prop nodes to avoid
747809
// duplicating edges the Rust engine already built.

src/domain/graph/resolver/points-to.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,29 @@ export function buildPointsToMap(
185185
// `function f({ ...rest }) {}` + `f(obj)` + `const obj = { prop: fn }` →
186186
// seed pts["rest.prop"] = {"fn"} so that `rest.prop()` resolves to `fn`.
187187
if (objectRestParamBindings && objectPropBindings && paramBindings) {
188+
// Index paramBindings: "callee::argIndex" → argName[] (O(|paramBindings|) build,
189+
// O(1) lookup — avoids scanning paramBindings for each rest binding).
190+
const paramByCalleeIdx = new Map<string, string[]>();
191+
for (const { callee, argIndex, argName } of paramBindings) {
192+
const k = `${callee}::${argIndex}`;
193+
const list = paramByCalleeIdx.get(k);
194+
if (list) list.push(argName);
195+
else paramByCalleeIdx.set(k, [argName]);
196+
}
197+
198+
// Index objectPropBindings: objectName → {propName, valueName}[]
199+
const propsByObject = new Map<string, Array<{ propName: string; valueName: string }>>();
200+
for (const { objectName, propName, valueName } of objectPropBindings) {
201+
const list = propsByObject.get(objectName);
202+
if (list) list.push({ propName, valueName });
203+
else propsByObject.set(objectName, [{ propName, valueName }]);
204+
}
205+
188206
for (const { callee, restName, argIndex } of objectRestParamBindings) {
189-
for (const { callee: pbCallee, argIndex: pbArgIdx, argName } of paramBindings) {
190-
if (pbCallee !== callee || pbArgIdx !== argIndex) continue;
191-
for (const { objectName, propName, valueName } of objectPropBindings) {
192-
if (objectName !== argName) continue;
207+
const argNames = paramByCalleeIdx.get(`${callee}::${argIndex}`) ?? [];
208+
for (const argName of argNames) {
209+
const props = propsByObject.get(argName) ?? [];
210+
for (const { propName, valueName } of props) {
193211
if (!definitionNames.has(valueName) && !importedNames.has(valueName)) continue;
194212
const key = `${restName}.${propName}`;
195213
if (!pts.has(key)) pts.set(key, new Set());

src/extractors/javascript.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,6 +2143,31 @@ function extractParamBindingsWalk(rootNode: TreeSitterNode, paramBindings: Param
21432143
if (ct === ',' || ct === '(' || ct === ')') continue;
21442144
if (ct === 'identifier' && !BUILTIN_GLOBALS.has(child.text)) {
21452145
paramBindings.push({ callee: fn.text, argIndex: argIdx, argName: child.text });
2146+
} else if (ct === 'spread_element') {
2147+
// f(...[a, b]) — inline array literal: expand each element as a direct param binding.
2148+
const inner =
2149+
child.childForFieldName('argument') ?? (child.childCount > 1 ? child.child(1) : null);
2150+
if (inner?.type === 'array') {
2151+
let elemCount = 0;
2152+
for (let j = 0; j < inner.childCount; j++) {
2153+
const elem = inner.child(j);
2154+
if (!elem) continue;
2155+
if (elem.type === ',' || elem.type === '[' || elem.type === ']') continue;
2156+
if (elem.type === 'identifier' && !BUILTIN_GLOBALS.has(elem.text)) {
2157+
paramBindings.push({
2158+
callee: fn.text,
2159+
argIndex: argIdx + elemCount,
2160+
argName: elem.text,
2161+
});
2162+
}
2163+
elemCount++;
2164+
}
2165+
// Advance by the exact number of slots this spread occupies and skip
2166+
// the unconditional argIdx++ below so that zero-element spreads (...[])
2167+
// do not shift subsequent argument indices.
2168+
argIdx += elemCount;
2169+
continue;
2170+
}
21462171
}
21472172
argIdx++;
21482173
}
@@ -2259,6 +2284,29 @@ function extractSpreadForOfWalk(
22592284
funcStack.push(nameNode.text);
22602285
pushedFunc = true;
22612286
}
2287+
} else if (node.type === 'assignment_expression') {
2288+
// `obj.method = function() { ... }` — func-prop assignment.
2289+
// Mirror handleFuncPropAssignment's logic so for-of loops inside the
2290+
// body get the correct enclosingFunc (e.g. 'obj.method') instead of
2291+
// '<module>' or the wrong outer function name.
2292+
const lhs = node.childForFieldName('left');
2293+
const rhs = node.childForFieldName('right');
2294+
if (
2295+
lhs?.type === 'member_expression' &&
2296+
(rhs?.type === 'function_expression' || rhs?.type === 'arrow_function')
2297+
) {
2298+
const obj = lhs.childForFieldName('object');
2299+
const prop = lhs.childForFieldName('property');
2300+
if (
2301+
obj?.type === 'identifier' &&
2302+
(prop?.type === 'property_identifier' || prop?.type === 'identifier') &&
2303+
!BUILTIN_GLOBALS.has(obj.text) &&
2304+
prop.text !== 'prototype'
2305+
) {
2306+
funcStack.push(`${obj.text}.${prop.text}`);
2307+
pushedFunc = true;
2308+
}
2309+
}
22622310
}
22632311

22642312
if (node.type === 'call_expression') {

src/infrastructure/native.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* to the existing WASM pipeline.
77
*/
88

9+
import { existsSync } from 'node:fs';
910
import { createRequire } from 'node:module';
1011
import os from 'node:os';
1112
import { fileURLToPath } from 'node:url';
@@ -112,17 +113,27 @@ export function loadNative(): NativeAddon | null {
112113

113114
// 2. Locally compiled dev binary — preferred over npm package so that Rust
114115
// changes are visible without publishing. Only used when the file exists.
116+
// If the file exists but fails to load (e.g. stale ABI), we warn and halt
117+
// rather than silently falling through to the npm package — that would
118+
// defeat the purpose of this priority order.
115119
const localFile = PLATFORM_LOCAL_BINARIES[platformKey];
116120
if (localFile) {
117-
try {
118-
const localPath = fileURLToPath(
119-
new URL(`../../crates/codegraph-core/${localFile}`, import.meta.url),
120-
);
121-
_cached = _require(localPath) as NativeAddon;
122-
debug(`loadNative: loaded local dev binary: ${localPath}`);
123-
return _cached;
124-
} catch (err) {
125-
debug(`loadNative: local dev binary not available: ${toErrorMessage(err as Error)}`);
121+
const localPath = fileURLToPath(
122+
new URL(`../../crates/codegraph-core/${localFile}`, import.meta.url),
123+
);
124+
if (existsSync(localPath)) {
125+
try {
126+
_cached = _require(localPath) as NativeAddon;
127+
debug(`loadNative: loaded local dev binary: ${localPath}`);
128+
return _cached;
129+
} catch (err) {
130+
_loadError = err as Error;
131+
warn(
132+
`loadNative: local dev binary exists but failed to load "${localPath}": ${toErrorMessage(err as Error)}`,
133+
);
134+
_cached = null;
135+
return null;
136+
}
126137
}
127138
}
128139

@@ -131,6 +142,7 @@ export function loadNative(): NativeAddon | null {
131142
if (pkg) {
132143
try {
133144
_cached = _require(pkg) as NativeAddon;
145+
debug(`loadNative: loaded npm package: ${pkg}`);
134146
return _cached;
135147
} catch (err) {
136148
_loadError = err as Error;
@@ -154,6 +166,10 @@ export function isNativeAvailable(): boolean {
154166
/**
155167
* Read the version from the platform-specific npm package.json.
156168
* Returns null if the package is not installed or has no version.
169+
*
170+
* Note: always reports the npm package version. When the local dev binary or
171+
* NAPI_RS_NATIVE_LIBRARY_PATH is loaded instead, this version may not match
172+
* the running binary.
157173
*/
158174
export function getNativePackageVersion(): string | null {
159175
const pkg = resolvePlatformPackage();

tests/benchmarks/resolution/fixtures/jelly-micro/more1/expected-edges.json

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)