Repository navigation
fix(ruby): a call on an instance variable links the class initialize gives it - #3053
Merged
Merged
Conversation
…gives it 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>
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, a call on an instance variable, like
@writer.flush, now links to the method of the class that the class'sinitializegives the variable:@writer = CacheWriter.new(…)makes it aCacheWriter. Before, no call on an instance variable linked at all. On main every such call stays unresolved, for example 2,780 of them in activesupport and 3,239 in actionpack.T5's triage of #684 found this gap; there is no issue or contributor PR for it. It builds on #3049's receiver code.
Root cause
Both extractors record such a call as
@writer.flush. No strategy reads an instance variable's type.matchMethodCall's receiver pattern doesn't even admit the@, so the call ended as a failed reference.Fix
ruby-constant-paths.tsresolves@name.mthe same way #3049 resolves a call on what.newmade:def self.x, notclass << self) of a class. A module's methods run on whatever includes it, so they are left out.initialize: every assignment to@namein that class'sinitialize(=or||=) must start a statement and be a loneConst.new,A::B.new(…)or::Foo.new …. Arguments in or out of parentheses, a block or a statement modifier may follow. All of them must name one constant..tap,writer || Foo.new,@a, @b = …, or a chain continued on the next line.initializegives the class. The method is its own instance method, else the first ancestor's (rubyAncestry). A class the project doesn't declare (@logger = Logger.new($stdout)) links nothing.initializeis read, as the triage asked. A reassignment in another method doesn't untype the variable (see the triage).There is no extraction change, so the kernel is unchanged.
Validation
Tests:
__tests__/ruby-ivar-receivers.test.tsruns in both engines. It covers:||=with arguments over several lines, a namespaced class built with a block, and an inherited method;.tap, a multiple assignment, and a variable thatinitializenever assigns;With main's resolver the positive test fails in both engines, and the 6 untyped and outside-class tests pass. With the fix all 8 pass.
A/B: kernel engine, main
3a4cd3bfvs this change, natural-key edge diff. The Rails repos are subtrees of rails793d744. The branch was then rebased onto25144678, whose JVM changes (java.rs, kotlin.rs, JVM-gated resolution) don't touch Ruby. The suite ran on the rebased commit.Every edge was checked against its class's
initialize. Some examples:@liquid_renderer.stats_table→LiquidRenderer;@registry.add→Fanout::Registry, and@lock.*→Concurrency::ShareLock;@scanner.*→Journey::Scanner, and@router.serve→Journey::Router;@available.poll→ConnectionPool::Queue(inherited byConnectionLeasingQueue), and@state.finalized?→TransactionState.The 3 that are wrong in practice are
AbstractAdapter's@pool.*calls.initializesets@pool = NullPool.new, so they go toNullPool, butpool=later replaces it with the realConnectionPool.The controls, requests (Python) and cobra (Go), are byte-identical by dump-graph.
Index time: median of 3 interleaved runs: activesupport 4.72 s vs 4.79 s, activerecord 11.54 s vs 11.54 s.
Full suite: on this commit (
e186fd6a, on main19b74d10), afternpm ci, kernel built,--maxWorkers=4. 801 files (795 passed, 6 skipped) and 9,606 tests: 9,520 passed, 0 failed, 86 skipped.Left out
def writer; @writer ||= Foo.new; end) stays untyped, since onlyinitializeis read. So does one set in a superclass'sinitialize.initializetype. Making any assignment outsideinitializeuntype it would fix theNullPoolcase, but it would also drop right edges such as activesupport's@registry = @registry.add(…), which is still aRegistry.🤖 Generated with Claude Code