Skip to content

Commit 6e143c2

Browse files
committed
fix(resolver): gate bare-call same-class lookup on language (#1424)
In JS/TS, bare foo() calls inside a class method are module-scoped — there is no implicit class binding. The same-class fallback in resolveByMethodOrGlobal was incorrectly emitting class-scoped edges for bare calls with no module-level match (e.g. flush() inside Processor.run resolving to Processor.flush). Gate the fallback: skip for bare calls in JS/TS, keep for this.method() calls and for C#/Java where bare calls are class-scoped.
1 parent eb61150 commit 6e143c2

1 file changed

Lines changed: 30 additions & 1 deletion

File tree

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,28 @@ export interface CallNodeLookup {
2323

2424
export const RECEIVER_KINDS = new Set(['class', 'struct', 'interface', 'type', 'module']);
2525

26+
/**
27+
* Languages where bare `foo()` calls inside a class method are lexically scoped
28+
* to the module, not the class — there is no implicit this/class binding.
29+
* For these languages, the same-class fallback must not run for bare (no-receiver)
30+
* calls that found no exact same-file match.
31+
*/
32+
const MODULE_SCOPED_BARE_CALL_EXTENSIONS = new Set([
33+
'.js',
34+
'.mjs',
35+
'.cjs',
36+
'.jsx',
37+
'.ts',
38+
'.tsx',
39+
'.mts',
40+
'.cts',
41+
]);
42+
43+
function isModuleScopedLanguage(relPath: string): boolean {
44+
const ext = relPath.slice(relPath.lastIndexOf('.'));
45+
return MODULE_SCOPED_BARE_CALL_EXTENSIONS.has(ext);
46+
}
47+
2648
// ── Shared resolution functions ──────────────────────────────────────────
2749

2850
export function findCaller(
@@ -197,7 +219,14 @@ export function resolveByMethodOrGlobal(
197219
// Also covers no-receiver calls inside class methods, e.g. `IsValidEmail(x)` inside
198220
// `Validators.ValidateUser` → try `Validators.IsValidEmail` (C#/Java static siblings).
199221
// This seeds the initial edge that runChaPostPass later expands to subclass overrides.
200-
if (callerName) {
222+
//
223+
// For JS/TS, bare (no-receiver) calls are module-scoped — there is no implicit class
224+
// binding. Skip the same-class fallback for bare calls in those languages to prevent
225+
// false positives (e.g. `flush()` inside `Processor.run` must not resolve to
226+
// `Processor.flush`). this.method() calls are unaffected: they still reach the fallback
227+
// because `call.receiver === 'this'` is truthy, not a bare call.
228+
const isBareCall = !call.receiver;
229+
if (callerName && !(isBareCall && isModuleScopedLanguage(relPath))) {
201230
const dotIdx = callerName.lastIndexOf('.');
202231
if (dotIdx > -1) {
203232
// Extract only the segment immediately before the method name so that

0 commit comments

Comments
 (0)