Repository navigation
fix: a call to an inherited method finds the ancestor that declares it (#984) - #1185
Merged
3 commits merged intoNov 9, 2026
Merged
3 commits merged into
3 commits merged into
Conversation
The parser put a file path in front of names it could not check, so h.hashCode() became One.java::Helper.hashCode, self.shared() named the subclass when shared was inherited, and Vec::new() became src/lib.rs::new. None names a node, yet each passed every check a real name does, and every edge said EXTRACTED and 1.0 whether it was bound or not. - refresh_target_resolution first settles endpoints: a path-qualified CALLS/REFERENCES target or TESTED_BY source with no node goes back to the plain name, keeps the qualified form in extra.claimed_endpoint, and returns to it if the node appears, so update and rebuild agree. - A settled edge is skipped by the bare resolver, and a name match on it only counts for a method of the class it named. - Tiers are EXTRACTED 1.0, INFERRED 0.8 or the resolver's score, AMBIGUOUS 0.4, UNRESOLVED 0.2. No schema change. - callers_of, callees_of and references_to say in the existing confidence field when some call sites are name matches only. - Rust: an unanchored single name is not an item of the crate root, an inline module still binds, and module files must match case. - tests_for no longer follows a callee with no node; one query per hop. On ripgrep 1,071 invented endpoints went to 0 and real call links rose from 4,064 to 4,296 with none lost; petclinic and gin lost no links. Fixes #984 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups for #984: - A rename's name-match fallback skips a settled call whose claimed class is not the renamed method's class, so h.build() on a Helper is no longer a high-confidence edit when renaming One.build. - Owner scopes are compared from the innermost name out, as far as both go: Alpha.Inner no longer matches Beta.Inner, while a claim that knew only Inner still matches Outer.Inner. - A cross-file C++ scoped call (cpp_scoped_target) and a ReScript call bound through open/include (new rescript_open_module) are INFERRED, not EXTRACTED. - The HCL resolver reads the Terraform name from claimed_endpoint, so an update that adds a declaration in a sibling file links the reference as a rebuild does. - The Rust inline-module shortcut needs the item defined in that module; a name it only re-exports stays unresolved instead of landing on a same-named function in the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#984) Builds on #1171 and must merge after it. The ideas come from #1036 by Anthony Ala: an INHERITS walk for inherited self-calls, a receiver's declared type kept when its class needs no import, Java packages as evidence, and a call on another object not landing on the caller's own method. Written fresh on top of #1171's settle pass, with each defect the review of #1036 found fixed: - A class is known by its qualified name. A parent or a receiver type counts only when the file, its imports, its Java package or its PHP namespace says which class it is, so a second Widget elsewhere is never used. - PHP $this is the calling object, in both receiver lists. - Package evidence names a receiver type or a class target only, never a plain ping(). - The Java package comes from the parse-time node data, not from disk. - The walk runs before the settle pass and takes up settled edges again; a moved edge keeps the name the parser wrote, so each pass checks it again and an update matches a rebuild. - One transaction; TESTED_BY mirrors move with their calls. - Python: decline when another ancestor could win under the MRO, or when a parent is outside the graph. Co-Authored-By: Anthony Ala <aala@conga.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
code-review-graph reviewOverall risk: 0.85 (CRITICAL) — 125 changed function(s)/class(es), 72 affected flow(s), 47 test gap(s) (16 with no tested caller found, 31 reached only through a caller) Risk-scored changes
Affected execution flows
Test gaps
Reached only through a callerNo test names these directly, but a tested caller reaches them along the call path shown. That is a path in the call graph, not a record of execution -- the caller's tests may never take this branch. Treat it as where to look, not as coverage: these count as gaps and are scored as untested.
Powered by code-review-graph — local-first analysis; no code leaves the CI runner. |
HengYangDS
pushed a commit
to HengYangDS/code-review-graph
that referenced
this pull request
Nov 9, 2026
…rge train) tirth8205#1154 lets the parser bind a receiver typed by a class in the same Java package, so pet.getName() is written as Pet.getName and the tirth8205#1185 inherited pass moves it to NamedEntity.getName. The target is the one tirth8205#1185 expects; the edge records the move under inherited_endpoint, not inherited_through. 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.
Part of #984.
Builds on #1171 and must merge after it. This branch is #1171 plus one commit (
da8f6ba), so until #1171 lands the diff here shows both. Please review only that commit.Credit
The ideas come from #1036 by Anthony Ala (@aala-conga): an INHERITS walk for inherited self-calls, keeping a receiver's declared type, Java packages as evidence, and a call on another object not landing on the caller's own method (parts 1, 2, 4 and 5 of that PR). This is a fresh write on top of #1171's resolver, not a merge of #1036, with every problem found in the review of #1036 fixed (table below). The commit carries
Co-Authored-By: Anthony Ala. Nothing was pushed to #1036.What was wrong
After #1171 a call to an inherited method through a subclass is an honest
UNRESOLVEDedge, butcallers_ofandtests_foron the real method still find nothing:this.handle()inLeaf extends Alphais written<leaf>::Leaf.handle, which names no node.$this->dispatch()in a PHP test case, and Java'sthis.getName(), only match a method of the same name nearby.pet.getName()on a JavaPetin the same package keeps no type at all, so it is a bare name guess, andpet.addVisit(v)insideOwner.addVisitwas bound toOwner.addVisititself.What this does
inheritance.py(new): a class is known by its qualified name, never its short name. A parent inextends, or a receiver type, means a class only when the file says which: the same file, a file it imports, Java'sjava_typeor a same-package class (read from thejava_packagethe parser already stores on Class nodes, never from disk), a full Java or PHP import name, or a PHP class in the same namespace (newphp_namespaceson PHP File nodes). An inherited method is the nearest one up the superclass chain; interfaces and traits hand down no body (php_kindmarks them). Where a class has two parents that can hold code (Python), the method must be the only declaration among all ancestors, so no method order can pick another. A parent the graph cannot name stops the walk._resolve_inherited_endpoints) runs at the start ofrefresh_target_resolution, before fix: an edge that names no node says so, and confidence means something (#984) #1171's settle pass, on fresh<path>::Class.methodnames, on edges settled earlier (claimed_endpoint) and on edges it moved before. A moved edge keeps the parser's name inextra.inherited_endpoint, so every pass checks it again from scratch: if an update adds the method nearer, removes it, or leaves the answer unsure, the edge goes where a fresh parse and this pass put it.TESTED_BYmirrors take the same path. One transaction.$this,this,selfandsupermean the caller's own class (or its parent), then its ancestor. A declared receiver type (Pet pet) means that class or its ancestor, and nothing else, even when the class is not imported; a type from outside the graph binds nothing. A Java class name receiver (EntityUtils.getById()) and a constructor (new Pet()) may use the same package. A call on another, untyped object is not bound to the caller's own method on that alone (Java, Kotlin, C#, TypeScript only: the parser reads no PHP or JavaScript parameter types, so there$otherinequals(Uuid $other)looks untyped but is the caller's own class).receiver_typewithreceiver_resolution: typed_receiver_unresolvedwhen the type's file is unknown (Java, Kotlin, JS, TS), but not for a type parameter (T) or Java'svar; recordsreceiver: "this"for Javathis.m().find_dead_codestill countsUtil.make()as use ofUtilonce the edge moves toBase.make.The review points on #1036
Widget,a/basevsb/base)test_a_homonym_class_in_another_file_does_not_bind$thistreated as another object$thisin both receiver liststest_php_this_binds_to_its_own_method_and_to_an_inherited_one,test_php_this_still_reaches_a_method_of_its_own_classping()landing on same-packageB.ping)test_same_package_class_that_is_not_an_ancestor_does_not_bindjava_packageinheritance.py)h.equals()back toWidget.equals)test_a_typed_receiver_whose_class_lacks_the_method_does_not_loop_backtest_an_update_moves_the_call_as_a_rebuild_would,test_a_settled_call_is_taken_up_once_the_ancestor_appearstest_python_two_parents_that_both_declare_it_do_not_bind,test_python_parent_outside_the_graph_stops_the_walktest_the_walk_writes_once_and_a_second_pass_moves_nothingtest_a_test_through_a_subclass_is_a_test_of_the_ancestorThe inherited self-call probe (
test_inherited_self_call_through_a_subclass_binds) binds. 15 of the 21 new tests fail on #1171; the other 6 are the guards above, which #1171 also passes.Proof on real repositories
Shallow clones, each built with #1171 (
0a9c0a6) and with this branch, edges compared row by row: spring-projects/spring-petclinic500158f, CodelyTV/typescript-ddd-example94717b0(TypeScript,extends AggregateRoot,extends MongoRepository<T>), CodelyTV/php-ddd-example9271c46(PHP, test case and controller chains)."Inherited calls" are
CALLSedges bound to a method the call's class inherits.TESTED_BYthe same wayOn PHP, #1171 already found 97 of these by name, because a test file
uses its parent test case.Other changes, every one read:
owner.getPet(),new Pet(),EntityUtils.getById()), and 19 self-loops fix: an edge that names no node says so, and confidence means something (#984) #1171 made up moved to the right class:pet.addVisit(visit)insideOwner.addVisitnow goes toPet.addVisit;vets.findAll()in the testfindAllwas aTESTED_BYfrom the test to itself, nowVetRepository.findAll.logger.debug()insideWinstonLogger.debug,express.listen()insideServer.listen,amqplib.connect(),channel.close(), andthis.value.toString()on a primitive.$thiscalls to the class's own method that fix: an edge that names no node says so, and confidence means something (#984) #1171 left ambiguous.CALLStiers, #1171 → this: petclinicINFERRED17 → 253,UNRESOLVED1,570 → 1,334; TypeScriptINFERRED66 → 75; PHPINFERRED531 → 571,AMBIGUOUS26 → 23.EXTRACTEDis unchanged on all three.Checked against the source (all of the new inherited links were read; these are the ones worth showing):
OwnerController.java:86owner.getId()→BaseEntity.getId:Owner extends Person extends BaseEntity,getIdatBaseEntity.java:39, two levels up, different package.PetValidator.java:41pet.getName()(Pet pet = (Pet) obj;at line 40) →NamedEntity.getName(NamedEntity.java:37).MongoCourseRepository.ts:15this.persist(...)→MongoRepository.persist:extends MongoRepository<Course>,persistatMongoRepository.ts:23.CoursesCounter.ts:31this.record(...)→AggregateRoot.record(AggregateRoot.ts:17).CoursesGetWebController.php:19$this->ask(...)→ApiController.ask:extends WebController extends ApiController, the second in the same namespace with nouse,askatApiController.php:30.CreateCourseCommandHandlerTest.php:35$this->dispatch(...)→UnitTestCase.dispatchthroughCoursesModuleUnitTestCase(UnitTestCase.php:70), and itsTESTED_BYmirror too.Update equals rebuild. On a copy of the TypeScript repo, renaming
MongoRepository.persistand runningupdategave the sameCALLS,TESTED_BYandREFERENCESrows as a fresh build (2,001 rows, 0 differ); the 4 moved calls went back toUNRESOLVEDwith their claim.Cost. Steady
refresh_target_resolutionon a 76,000-edge graph: 0.115 s on #1171, 0.156 s here.Left out
ping()) is not read asthis.ping(): the parser records no receiver forgetOwner().getName()either, so the two cannot be told apart.for (Pet pet : pets)) still carries no type, and Java interface default methods are not followed.super.m()inside an overridem()is still bound to the override by the parser, as before.Schema
No change. New
extrakeys:inherited_endpoint,inherited_through; newreceiver_resolutionvaluetyped_receiver_unresolved;php_namespaceson PHP File nodes andphp_kindon PHP interfaces and traits. The next build puts an existing graph right.Checks
uv run ruff check code_review_graph/: clean.uv run --extra dev python -m mypy code_review_graph/ --ignore-missing-imports --no-strict-optional: clean.uv run pytest tests/ --tb=short -q: 6,444 passed, 900 skipped, 3 xfailed, 4 xpassed.-m "surface or e2e": 40 passed, 2 skipped.tests/test_inherited_calls.py(21 tests). One fix: an edge that names no node says so, and confidence means something (#984) #1171 test now expectsself.shared()to reachBase.shared, still neverOther.shared.🤖 Generated with Claude Code