Repository navigation
Conversation
…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
force-pushed
the
fix/ruby-call-edge-extraction
branch
from
June 4, 2026 14:36
4a342b2 to
ccd6617
Compare
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>
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 |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Using
codegraphon 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
callnodes usereceiver+methodfields rather than afunctionfield, causing two common patterns to produce unresolvable references:obj = SomeClass.new(...); obj.methodemittedobj.method, andSomeClass.new(...).methodemitted only the method name — neither could be matched by the resolver, socodegraph_callersandcodegraph_calleesboth returned incomplete results.tree-sitter.tsfor call nodes that use amethodfield: chained receivers (class/namespace) emit the class name; bare method receivers emitreceiver.method; complex receivers (e.g. a chained call) emit just the method name.buildLocalScopehook toLanguageExtractorthat lets a language pre-scan a function body and return avar → "ClassName::"prefix map. The engine uses this during call extraction to resolvevar.methodtoClassName::method. Ruby implements it by scanningvar = 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 classnpx vitest run— full suite, no new regressionsshuts down when its parent is SIGKILL'd and stdin stays open