fix(extraction/ruby): build receiver.method calls so instance calls resolve (#1110) (#1111)

The Ruby extractor dropped the method name from a `receiver.method` call:
`lg.log()` was recorded as a call to `lg` (the bare receiver), which
matches no symbol, so the reference resolved to nothing and no method
edge was ever produced. A Ruby method invoked through a receiver had no
recorded callers and was invisible to impact/blast-radius and explore
flow traces. This is the Ruby-specific blocker noted in #1108 — that
local-variable type-inference fix couldn't help Ruby because the call
reference itself was missing.

extractCall recognized receiver-bearing calls by the `object`/`name`/
`function` fields other grammars use; tree-sitter-ruby's `call` node uses
`receiver` + `method`, so it fell through to the generic fallback that
takes the first named child (the receiver) as the callee. Handle Ruby
`call`/`method_call` explicitly: build `receiver.method`, keep bare
`foo(...)` as the method name, emit `Foo.new` as an `instantiates` ref,
and give a capitalized (constant) receiver a `references` edge so a class
used only via its class methods still records a dependent.

With this plus #1108, `lg = Logger.new; lg.log` resolves `lg.log` to
`Logger#log`, and the two-file same-name case is same-file-correct
(#1079). Adds Ruby to the local-variable inference test matrix plus a
focused test asserting `Foo.new` stays an instantiation.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-01 15:26:49 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent ed64db08b4
commit 3424ff36c5
3 changed files with 82 additions and 0 deletions
+24
View File
@@ -1681,6 +1681,8 @@ func main() {
src: `<?php\nclass Logger { function log() { return 1; } }\nfunction useIt() { $lg = new Logger(); return $lg->log(); }\n` },
{ lang: 'Scala (val x = new T)', file: 'Svc.scala',
src: `class Logger { def log(): Int = 1 }\nobject A { def use(): Int = { val lg = new Logger(); lg.log() } }\n` },
{ lang: 'Ruby (x = T.new)', file: 'svc.rb',
src: `class Logger\n def log\n 1\n end\nend\ndef use\n lg = Logger.new\n lg.log\nend\n` },
];
for (const c of cases) {
@@ -1702,6 +1704,28 @@ func main() {
).toBeGreaterThan(0);
});
}
it('Ruby: builds receiver.method and keeps Foo.new as an instantiation', async () => {
// The Ruby extractor previously took the receiver as the callee and
// dropped the method name (`lg.log()` -> a call to `lg`). Now it builds
// `lg.log`, while `Logger.new` must still record an instantiation.
fs.writeFileSync(
path.join(tempDir, 'svc.rb'),
`class Logger\n def log\n 1\n end\nend\ndef run\n lg = Logger.new\n lg.log\nend\n`,
);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
const run = cg.getNodesByKind('function').find((n) => n.name === 'run')!;
const logMethod = cg.getNodesByKind('method').find((n) => n.name === 'log')!;
const logger = cg.getNodesByKind('class').find((n) => n.name === 'Logger')!;
const out = cg.getOutgoingEdges(run.id);
// lg.log resolved to the method (the receiver-type inference kicked in).
expect(out.some((e) => e.kind === 'calls' && e.target === logMethod.id)).toBe(true);
// Logger.new is still an instantiation of the class.
expect(out.some((e) => e.kind === 'instantiates' && e.target === logger.id)).toBe(true);
});
});
describe('Name Matcher: kind bias for new ref kinds', () => {