Repository navigation
fix(ruby): calls to !/? methods and calls on what .new made link their method - #3049
Merged
Merged
Conversation
…k their method
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>
This was referenced Oct 30, 2026
inth3shadows
added a commit
to inth3shadows/codegraph
that referenced
this pull request
Oct 30, 2026
Brings 9f379af..bc9291a (colbymchenry#3030, colbymchenry#3031, colbymchenry#3043, colbymchenry#3045, colbymchenry#3046, colbymchenry#3049, colbymchenry#3044, colbymchenry#3047, colbymchenry#3051, colbymchenry#3050, colbymchenry#3052, colbymchenry#3053, colbymchenry#3025, colbymchenry#3055). Clean auto-merge.
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.
Summary
In Ruby, two kinds of call linked nothing:
!or?, made on a receiver:writer.flush!orwriter.ready?afterwriter = CacheWriter.new, andCacheWriter.create!(…)on the class.CacheWriter.new(key).write_cacheorBilling::Ledger.new.charge!(2).Both now link. The class a
.newbuilds is the one Ruby's constant lookup finds where the call is written, and the method is that class's own or one its ancestry declares. A class from outside the project links nothing: rack-protection'sRequest.new(env).hostis rack'sRack::Request, not the project'sSinatra::Request.Reimplements #684 by @ricobl, who found both gaps. This PR has our own implementation and tests in both engines, and the CHANGELOG entry credits them.
Root cause
matchMethodCall's receiver pattern^([\w.]+)\.(\w+:?(?:\w+:)*)$takes no trailing!or?. Every such call on a receiver fell through the method-call strategies. A bareflush!already linked through the bare-name path.tree-sitter.ts, andextract_callincodegraph-kernel/src/ruby.rs. SoFoo.new(args).barwas recorded asFoo.new(args).bar, with the arguments in it, and matched no strategy. ARack::Builder.new do … end.to_appwas recorded with the whole block's text as its name.Fix
Const.new(…),A::B.newor::Foo.newis recorded asnew Const().m. This is the shape a constructed receiver already has in TS/JS, Java and C#. Anewon a value (klass.new.run,self.class.new(x).run) and later links of a chain keep the receiver's text.ruby-constant-paths.ts: this is the module fix(ruby): a call on a namespaced class links its caller to the class and its method #2827 added forA::B.m. Anew Const().mcall is resolved there, before the name pre-filter.::-rooted name is looked up at the top level only.rubyAncestry) that declares it.constantsNamed+rubyAncestrywalk rather thanresolveMethodOnType. The latter matches<type>::<method>by suffix and walks supertypes by simple name, so for a namespaced class it would find no inherited method, and for a top-level one it could take a nested namesake.rubyAncestryno longer reads a module a call returns as a mixin of the constant the call starts at. Before,include ActionDispatch::Routing::RouteSet.new.tap { … }.url_helperscountedRouteSetas a mixin. In the first A/B that sent 61 of actionpack'sW.new.url_forcalls toRouteSet#url_for.!and?: the method part of the Ruby dot pattern inmatchMethodCalltakes a trailing!or?. Such a call then goes through the same strategies as any other Ruby method call, with one exception: the last one. That strategy scores several project methods of the name by the words they share with a value receiver, and it doesn't run for these names.key?,empty?,respond_to?,is_a?andmerge!are answered by Ruby's core classes on nearly every receiver, so a shared word doesn't say which project method is meant, or whether any is. For example, activesupport'soptions.key?on a Hash went toInheritableOptions#key?, and actionpack'sbody.respond_to?went toRackBody#respond_to?.MarshalWithFallback.dumped?) still names its owner, so it keeps the scoring.Validation
Tests:
__tests__/ruby-constructed-receivers.test.tsruns in both engines. It covers:!/?call and a!class method;.newchains with and without arguments, and namespaced and::-rooted classes;CardinsideBilling::LedgerisBilling::Card, notAdmin::Card) and an inherited method;?call on an untyped value with two project methods of the name, which gets no guess;includenames a module a call returns, which gives it no mixin;__tests__/kernel-ruby-parity.test.tsgets a constructed-receiver case: kernel and wasm emit identical refs.Kernel parity:
scripts/kernel-parity.mjsfound byte-parity on every file, with none deferred to wasm: the fixtures 10/10, sinatra 147/147, and jekyll with the activesupport and actionpack subtrees 1,044/1,044.A/B: kernel engine, main
37052b18vs this change, natural-key edge diff. The Rails repos are subtrees of rails793d744, each copied out as its own folder.No nodes changed anywhere. Two actionpack edges keep their target and only gain confidence (
NoDependenciesController.new.view_cache_dependencies, 0.7 → 0.85). The unresolved-ref changes are the renamednew X().mrefs. The controls, requests (Python) and cobra (Go), are byte-identical by dump-graph.Triage: every group was checked; the large ones by distinct call → target pair.
Right by construction:
new X().m: the class comes from constant lookup and the method from the class or its ancestry.Samples confirm them, for example
Date.new(…).advance→Date#advance(activesupport reopensDate),Date.new.all_day→DateAndTime::Calculations#all_day(a mixin), andGenericEnumerable.new.exclude?→Enumerable#exclude?.The wrong ones all come from the two name strategies that plain Ruby names also go through:
bad_cookies.empty?×2 →Sinatra::Cookies::Jar#empty?.cache.key?on the regenerator's Hash →Jekyll::Cache#key?, anddocument.asset_file?on a Page →Document#asset_file?.content_type.nil?×2 →Mime::NullType#nil?, andcache_store.exist?→RailsEntityStore#exist?.self.class.respond_to?→AttributeMethods#respond_to?, sqlite3 code'scolumn.auto_increment?→MySQL::Column, andtype.serializable?→EnumType#serializable?.The 2 removed actionpack edges were wrong on main.
ActionController::Base.with(…)is ActiveSupport'sObject#with; it reachedAbstractController::Railties::RoutesHelpers.withonly through the misread mixinextend ::AbstractController::Railties::RoutesHelpers.with(app.routes).Index time: median of 3 interleaved runs: activesupport 5.35 s vs 4.63 s, jekyll 2.66 s vs 2.73 s. That is noise.
Full suite: on this tree (tested as
c63b0411, on mainb700a3ee; only the commit message changed since), kernel built,--maxWorkers=4. 797 files (791 passed, 6 skipped) and 9,578 tests: 9,493 passed, 0 failed, 85 skipped.Left out
!/?names, as they do for plain names. One is a receiver named after its class (entry.expired?→Cache::Entry); the other is the one project method of the name when it shares a receiver word. The triage counts their few wrong edges. The receiver-word scoring is still on for plain Ruby names: on main it makes, for example, activesupport'sTime.utcreachDateTime::utc567 times. A Ruby standard-method stoplist, like the one fix(resolution): library-method calls on untyped receivers need receiver evidence (Rust, Go, Python, Kotlin, Scala, C#, Java) #1947 added for Python, Scala and Java, would address both, and is a separate change.@writer.flush) is not typed here. That is a follow-up PR.🤖 Generated with Claude Code