Skip to content

fix(extraction): fix Ruby call edge extraction for local vars and chained .new - #684

Closed
ricobl wants to merge 2 commits into
colbymchenry:mainfrom
ricobl:fix/ruby-call-edge-extraction
Closed

ricobl wants to merge 2 commits into
colbymchenry:mainfrom
ricobl:fix/ruby-call-edge-extraction

Conversation

@ricobl

@ricobl ricobl commented Jun 4, 2026 •

Copy link
Copy Markdown

Motivation

Using codegraph on a Ruby / Rails project has been a bit of a hit and miss for me, references are often not found via callers, callees and other commands, leaving agents to fallback to grep.

Summary

  • Ruby call nodes use receiver + method fields rather than a function field, causing two common patterns to produce unresolvable references: obj = SomeClass.new(...); obj.method emitted obj.method, and SomeClass.new(...).method emitted only the method name — neither could be matched by the resolver, so codegraph_callers and codegraph_callees both returned incomplete results.
  • Added handling in tree-sitter.ts for call nodes that use a method field: chained receivers (class/namespace) emit the class name; bare method receivers emit receiver.method; complex receivers (e.g. a chained call) emit just the method name.
  • Added a buildLocalScope hook to LanguageExtractor that lets a language pre-scan a function body and return a var → "ClassName::" prefix map. The engine uses this during call extraction to resolve var.method to ClassName::method. Ruby implements it by scanning var = ClassName.new(...) assignments. The hook is separator-agnostic so other languages can adopt it without engine changes.

Test plan

  • npx vitest run __tests__/extraction.test.ts -t "Ruby calls" — 5 tests covering bare .new, namespaced .new, chained .new(...).method, local var assignment with simple class, and local var assignment with namespaced class
  • npx vitest run — full suite, no new regressions
    • there seems to be a failing MCP PPID watchdog test: shuts down when its parent is SIGKILL'd and stdin stays open

ricobl and others added 2 commits June 5, 2026 00:18
…ined .new

Ruby call nodes use receiver + method fields instead of a function field,
causing the generic extractor to miss two common patterns:

- `var = SomeClass.new(...); var.method` emitted `var.method` which the
  resolver couldn't match to `SomeClass::method`
- `SomeClass.new(...).method` emitted only the method name without its class

Both the caller and callee sides of these edges were broken as a result.

Fixes:
- Extract the `method` field from Ruby call nodes in the generic extractor
  so chained `.new(...).method` correctly emits the method name
- Add a `buildLocalScope` hook to `LanguageExtractor` that lets a language
  pre-scan a function body and return a var → class prefix map; the engine
  uses this to resolve `var.method` to `ClassName::method` at extraction time
- Ruby implements the hook by scanning for `var = ClassName.new(...)` assignments

The hook is separator-agnostic (map values include the separator, e.g. `"Foo::"`),
so other languages can implement it without engine changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ricobl
ricobl force-pushed the fix/ruby-call-edge-extraction branch from 4a342b2 to ccd6617 Compare June 4, 2026 14:36
colbymchenry added a commit that referenced this pull request Oct 30, 2026
…k their method (#3049)

Two kinds of Ruby call linked nothing. A call to a method whose name ends
in `!` or `?` on a receiver (`writer.flush!`, `writer.ready?` after
`writer = CacheWriter.new`, `CacheWriter.create!(1)`) failed the method-call
pattern, whose method part took no `!` or `?`. A method called straight on
a new object (`CacheWriter.new(key).write_cache`, `Billing::Ledger.new.charge!`)
was recorded by both extractors with the receiver's raw text, arguments and
blocks included, which no strategy matched.

Both extractors now record a call on `Const.new(...)`, `A::B.new` or
`::Foo.new` as `new Const().m`, the constructed-receiver shape TS/JS, Java
and C# already use; the wasm extractor and the kernel's ruby.rs emit the
same refs. ruby-constant-paths resolves that shape before the name
pre-filter: the class by Ruby's constant lookup from the namespaces around
the call, the method as the class's own instance method or the first its
ancestry declares, and nothing when either is missing, so rack-protection's
`Request.new(env)` (rack's Rack::Request) links nothing. The Ruby dot pattern
in matchMethodCall takes a trailing `!` or `?`; the receiver-word scoring
among several project methods of the name stays off for such names on a
value receiver, where it was wrong on 55 of 74 new edges (`options.key?`
on a Hash -> InheritableOptions#key?). rubyAncestry no longer reads a
module a call returns (`include RouteSet.new.tap {...}.url_helpers`) as a
mixin of the constant the call starts at.

Validation, kernel engine, main 37052b1 vs this change:
- sinatra cb22afd +12, jekyll 97c3bdc +53, activesupport +398,
  actionpack +97 (2 removed: `ActionController::Base.with` had reached
  RoutesHelpers.with through a misread mixin), activerecord +1012
  (rails 793d744 subtrees). About 21 of 1,572 added edges are wrong, all
  from the class-name and single-method guesses that plain names get too.
- Controls requests (Python) and cobra (Go): dump-graph byte-identical.
- kernel-parity.mjs: fixtures 10/10, sinatra 147/147, jekyll + rails
  1044/1044 files byte-parity, none deferred.
- New tests: 10 fail with main's kernel and TS, all pass with the fix, in
  both engines. Dropping the scoring gate or the mixin fix fails its guard
  test.
- Index time, median of 3 interleaved: activesupport 5.35 vs 4.63 s,
  jekyll 2.66 vs 2.73 s (noise).

Reimplements PR #684 by @ricobl, who found both gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry

Copy link
Copy Markdown
Owner

Thanks for this, @ricobl, and for the clear write-up of both gaps: bang and question methods on a receiver, and methods called straight on Foo.new(...), linked nothing. We landed a fix in #3049, which records Foo.new(...).bar as a call on Foo in both of our extractors, resolves the class the way Ruby's constant lookup does (inherited methods included), and links nothing for a class from outside your project. It was reimplemented independently with new tests, and you're credited in the changelog. Closing this in favor of #3049; it ships in the next release.

colbymchenry added a commit that referenced this pull request Oct 30, 2026
… to its class's method (#3053)

Both extractors record a Ruby call on an instance variable as
`@writer.flush`, and no strategy read the variable's type, so every such
call stayed unresolved: 2,780 in activesupport and 3,239 in actionpack on
main, for example.

ruby-constant-paths now resolves `@name.m` written in an instance method of
a class (not a class method, not a module) by the class's `initialize`.
When every assignment to `@name` there starts a statement and is a lone
`Const.new(...)`, `A::B.new` or `::Foo.new ...` of one constant
(arguments with or without parentheses, a block or a statement modifier
may follow), the class is the one Ruby's constant lookup finds for it from
`initialize`, and the method is that class's own instance method or the
first its ancestry declares. Anything else leaves the variable untyped for
the other strategies, as before: a conditional, two classes, `.tap`,
`writer || Foo.new`, `@a, @b = ...`, a chain continued on the next line. A
class the project doesn't declare (`Logger.new($stdout)`) links nothing.

Validation, kernel engine, main 3a4cd3b vs this change:
- jekyll 97c3bdc +8, activesupport +19, actionpack +14, activerecord +30
  (rails 793d744 subtrees), sinatra cb22afd 0; nothing removed. 68 of the
  71 are right; 3 in activerecord follow `@pool = NullPool.new` from
  `initialize`, which `pool=` later replaces with the real pool.
- Controls requests (Python) and cobra (Go): dump-graph byte-identical.
- New tests fail with main's resolver (the positive case, in both engines)
  and pass with the fix; the untyped and outside-class cases pass on both.
- Index time, median of 3 interleaved: activesupport 4.72 vs 4.79 s,
  activerecord 11.54 vs 11.54 s.

The gap was found in the triage of PR #684.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants