fix(extraction): never fabricate an edge from a call-result receiver (#1748)
A member call whose receiver is itself a call — `d.setdefault(k, []).append(v)`, `make().run()` — used to drop the receiver at extraction time, degrade to the bare method name, and exact-match any top-level project symbol of that name (Python and JavaScript/TypeScript). Keep the inner callee encoded as `<inner>().<method>` in the TS extractor and native kernel; the name-matcher refuses to guess for that shape (store-accessor exception only). Based on #1692, rebased onto main after #1746. Fixes #1683. Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
co-authored by
Colby McHenry
parent
bffd50e4f1
commit
bb1d3093eb
@@ -4573,6 +4573,32 @@ export class TreeSitterExtractor {
|
||||
// scope keywords: such calls previously emitted a bare method
|
||||
// name, which either failed to resolve or resolved ambiguously.
|
||||
calleeName = `${getNodeText(receiver, this.source)}.${methodName}`;
|
||||
} else if (
|
||||
(this.language === 'typescript' ||
|
||||
this.language === 'javascript' ||
|
||||
this.language === 'tsx' ||
|
||||
this.language === 'jsx' ||
|
||||
this.language === 'python') &&
|
||||
receiver &&
|
||||
(receiver.type === 'call_expression' || receiver.type === 'call')
|
||||
) {
|
||||
// Receiver that is itself a call — `d.setdefault(k, []).append(v)`,
|
||||
// `make().run()`, `res.json().data` (#1683). The bare method name
|
||||
// this used to emit exact-matched any top-level project symbol of
|
||||
// that name and fabricated a call edge from an unrelated function
|
||||
// (`append`, `get`, `run`…). Keep the inner callee, encoded as
|
||||
// `<inner>().<method>` like the Java/Kotlin/C++ chains: the
|
||||
// marker never appears in an ordinary ref, so nothing name-matches
|
||||
// it, and a chain resolver can later infer the receiver's type
|
||||
// from what the inner call returns. An inner callee that is not a
|
||||
// plain name or member chain (`(await x)()`, `arr[0]()`) has no
|
||||
// static receiver at all — emit nothing: a silent miss, never a
|
||||
// wrong edge. The inner call is visited on its own either way.
|
||||
// Mirrored in the kernel (tsjs/extractors.rs, python.rs).
|
||||
const innerFn = getChildByField(receiver, 'function');
|
||||
const innerCallee = innerFn ? getNodeText(innerFn, this.source).replace(/\s+/g, '') : '';
|
||||
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(innerCallee)) return;
|
||||
calleeName = `${innerCallee}().${methodName}`;
|
||||
} else if (
|
||||
this.language === 'go' &&
|
||||
receiver &&
|
||||
|
||||
@@ -974,6 +974,19 @@ export class ReferenceResolver {
|
||||
if (fwEarly) return fwEarly;
|
||||
|
||||
// Strategy 2: Try import-based resolution
|
||||
// A TS/JS/Python call-receiver chain (`useStore.getState().reset`, #1683)
|
||||
// names the ROOT's import, not the method's: letting resolveViaImport see
|
||||
// it binds the call to the imported store constant and the method is
|
||||
// never looked up. The name-matcher owns the chain shape for these
|
||||
// languages — the Java/Kotlin/C++ chains keep their existing path.
|
||||
if (
|
||||
ref.referenceKind === 'calls' &&
|
||||
CHAIN_SHAPE.test(ref.referenceName) &&
|
||||
(ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx' || ref.language === 'python')
|
||||
) {
|
||||
return this.gateLanguage(matchReference(ref, this.context), ref);
|
||||
}
|
||||
|
||||
const tImp = this.profileStages ? process.hrtime.bigint() : 0n;
|
||||
const importResult = this.gateLanguage(resolveViaImport(ref, this.context), ref);
|
||||
if (this.profileStages) this.stageAdd('viaImport', ref, !!importResult, tImp);
|
||||
|
||||
@@ -2507,6 +2507,30 @@ function matchRustSelfFieldCall(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one fallback a TS/JS/Python call-receiver chain keeps (#1683): a STORE
|
||||
* ACCESSOR. Zustand's `get()` inside the store factory and
|
||||
* `useStore.getState()` outside it hand back the store whose actions are
|
||||
* indexed as functions (#1573), so a unique callable of the method's name in
|
||||
* the same language family is what `get().reset()` reaches. Nothing else
|
||||
* qualifies: a chain rooted in a project value still says nothing about what
|
||||
* the inner call RETURNS — `db.prepare(sql).all()` would bind to any project
|
||||
* function named `all` — so it resolves to nothing, exactly like a chain
|
||||
* rooted in a parameter (`d.setdefault(k, []).append(v)`).
|
||||
*/
|
||||
function matchStoreAccessorChain(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
const m = ref.referenceName.match(/^([\w$.]+)\(\)\.(\w+)$/);
|
||||
if (!m || !m[1] || !m[2]) return null;
|
||||
const inner = m[1];
|
||||
const method = m[2];
|
||||
if (!(inner === 'get' || inner === 'getState' || inner.endsWith('.getState'))) return null;
|
||||
const callables = context
|
||||
.getNodesByName(method)
|
||||
.filter((n) => (n.kind === 'function' || n.kind === 'method') && sameLanguageFamily(n.language, ref.language) && n.id !== ref.fromNodeId);
|
||||
if (callables.length !== 1) return null;
|
||||
return { original: ref, targetNodeId: callables[0]!.id, confidence: 0.6, resolvedBy: 'exact-match' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a camelCase or PascalCase string into words.
|
||||
*/
|
||||
@@ -2917,6 +2941,19 @@ export function matchReference(
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
// A call-receiver chain the extractor encoded as `<inner>().<method>` for a
|
||||
// language with no chain resolver above (TS/JS, Python — #1683) is a
|
||||
// receiver whose type is unknown. Nothing below may guess for it: the
|
||||
// method-call pattern rejects the parens, exact name never matches, but the
|
||||
// fuzzy strategy splits on `.` and would hand `make().run` to any `run` —
|
||||
// the fabricated edge the encoding exists to prevent.
|
||||
if (
|
||||
ref.referenceName.includes('().') &&
|
||||
(ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx' || ref.language === 'python')
|
||||
) {
|
||||
return nmTimed('storeAccessorChain', ref, () => matchStoreAccessorChain(ref, context));
|
||||
}
|
||||
|
||||
// 2. Method call pattern
|
||||
result = nmTimed('methodCall', ref, () => matchMethodCall(ref, context));
|
||||
if (result) return result;
|
||||
|
||||
Reference in New Issue
Block a user