FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(cha): scope CHA implementor map by file, walk ancestors for inherited methods by carlos-alm · Pull Request #2399 · optave/ops-codegraph-tool · GitHub

fix(cha): scope CHA implementor map by file, walk ancestors for inherited methods - #2399

Merged
carlos-alm merged 2 commits into
mainfrom
fix/issue-2237-cha-rta-dispatch-scoping
Aug 9, 2026
Merged

fix(cha): scope CHA implementor map by file, walk ancestors for inherited methods#2399
carlos-alm merged 2 commits into
mainfrom
fix/issue-2237-cha-rta-dispatch-scoping

Conversation

Copy link
Copy Markdown
Contributor

Summary

Two bugs in Class Hierarchy Analysis (CHA) dispatch (issue #2237), present identically in both engines and across all four independent implementations of the same implementors-map + BFS pattern found while researching the fix:

  • WASM inline (emitChaCallEdgesForCall, buildChaPostPass in stages/build-edges.ts)
  • WASM/JS-orchestrated-native DB-driven post-pass (runChaPostPass in helpers.ts)
  • Native orchestrator DB-driven post-pass (runPostNativeCha in stages/native-orchestrator.ts)
  • Rust native additive dispatch + fallback tier (emit_cha_dispatch_edges, resolve_call_targets_core tier 3.7 in build_edges.rs)

Issue 1 — implementor-map collision: the implementors map (interface/base-class name → concrete classes) was keyed by bare simple name, scanning every file with no file/module scoping. Two unrelated files each declaring their own same-named interface had their implementor sets merged, producing a false call edge into the wrong file's method.

Issue 2 — missed inherited methods: CHA dispatch did a direct qualified lookup (${concreteClass}.${method}) for every RTA-instantiated concrete class. When that class inherits the dispatched method from an ancestor without overriding it, no node exists under the concrete class's own qualified name, so the lookup missed and the edge was never emitted.

Fix

  • Added a file-scoped implementors map (${parentName}|${file}, populated only when that file also locally declares a same-named parent) that the BFS root prefers over the bare map — mirrors the existing parentsByFile composite-key precedent from resolveThisDispatch resolves this/super dispatch to the wrong file when a same-named base class exists elsewhere with no same-file match #2062, applied to the inverse (parent → children) direction.
  • Added an ancestor-walk fallback that follows the class hierarchy up to the declaring ancestor when a direct qualified lookup misses — mirrors resolveThisDispatch's existing walk.
  • Mirrored identically across all four implementation sites in both engines.
  • A subtlety caught only by the end-to-end integration test: "locally declared names" must be derived from each file's full definitions list (filtered to class/interface/struct/etc. kinds), not the classes-relations list (ClassRelation[]/ClassInfo[]) — a bare interface with no extends/implements clause of its own never appears in the relations list, so the very shape this fix targets (interface Handler {} with a concrete implementor) was invisible to the first version of the file-scoping check.

Test plan

  • New unit tests in tests/unit/cha.test.ts (8 cases: 2 bug-specific + 6 preserved-behavior/regression) — verified fail-without-fix, pass-with-fix.
  • New dual-engine integration test tests/integration/issue-2237-cha-implementor-scoping.test.ts (4 cases, wasm + native) — verified all 4 fail without the fix on both engines, pass with it.
  • New Rust unit tests in crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs (2 cases) — verified fail-without-fix, pass-with-fix.
  • Existing CHA regression suites unaffected: tests/unit/cha.test.ts (pre-existing resolveThisDispatch resolves this/super dispatch to the wrong file when a same-named base class exists elsewhere with no same-file match #2062 cases), tests/integration/phase-8.5-cha-dispatch.test.ts, tests/integration/issue-2078-cha-sibling-caller.test.ts (legitimate cross-file dispatch through a shared interface name, which this fix must not break).
  • Full suite: npm test (4788 passed), npx tsc --noEmit, npm run lint, cargo test --release (867 passed), cargo clippy (no new warnings).
  • node scripts/parity-compare.mjs --langs javascript,typescript — parity OK.

…ited methods

Two bugs in Class Hierarchy Analysis (CHA) dispatch, present identically
in both engines and across all four independent implementations of the
same implementors-map + BFS pattern (WASM inline, WASM/native DB-driven
post-pass, native orchestrator post-pass):

1. The implementors map (interface/base-class name -> concrete classes)
   was keyed by bare simple name, scanning every file with no file/module
   scoping. Two unrelated files each declaring their own same-named
   interface had their implementor sets merged, producing a false call
   edge into the wrong file's method.

2. CHA dispatch did a direct qualified lookup (`${concreteClass}.${method}`)
   for every RTA-instantiated concrete class. When that class inherits the
   dispatched method from an ancestor without overriding it, no node
   exists under the concrete class's own qualified name, so the lookup
   missed and the edge was never emitted.

Add a file-scoped implementors map (`${parentName}|${file}`, populated
only when that file also locally declares a same-named parent) that the
BFS root prefers over the bare map, and an ancestor-walk fallback that
follows the class hierarchy up to the declaring ancestor when a direct
qualified lookup misses. Mirrored across cha.ts, helpers.ts's
runChaPostPass, native-orchestrator.ts's runPostNativeCha, and the Rust
build_edges.rs.

The file-scoping fix required deriving "locally declared names" from each
file's full definitions list (filtered to class/interface/struct/etc.
kinds), not the classes-relations list — a bare interface with no
extends/implements clause of its own never appears in the latter.

Closes #2237

Internal resolver fix — no README/CLAUDE.md/ROADMAP-documented behavior
changes. docs check acknowledged.

Impact: 19 functions changed, 23 affected

greptile-apps Bot commented Aug 9, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR scopes CHA implementor traversal by file and adds ancestor walking for inherited virtual methods across the TypeScript, database-driven, native-orchestrated, and Rust implementations.

  • Adds file-scoped implementor and parent maps while retaining global fallbacks for cross-file hierarchies.
  • Propagates file identity through CHA BFS and qualified-method lookup.
  • Adds unit and dual-engine integration coverage for implementor collisions and inherited dispatch.

Confidence Score: 4/5

The PR is not yet safe to merge because an inherited dispatch can still resolve an unrelated file's same-qualified concrete method before walking the selected hierarchy.

The new resolver preserves file identity during BFS but immediately abandons it when the selected concrete class lacks an override, allowing a project-wide name collision to produce a false call edge and suppress the correct inherited target.

Files Needing Attention: src/domain/graph/builder/cha.ts, src/domain/graph/builder/helpers.ts, src/domain/graph/builder/stages/native-orchestrator.ts, crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs

Important Files Changed

Filename Overview
src/domain/graph/builder/cha.ts Adds the shared file-aware CHA traversal and ancestor resolver, but its early global method fallback can discard the selected implementor identity.
src/domain/graph/builder/helpers.ts Mirrors the DB-driven CHA scoping and ancestor walk, including the same premature global qualified-name fallback.
src/domain/graph/builder/stages/native-orchestrator.ts Mirrors file-aware CHA expansion for the native orchestrator post-pass and retains the same collision path during method fallback.
crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs Implements native CHA parity, including the same global method fallback before ancestor traversal.
src/domain/graph/builder/incremental.ts Passes caller-file identity into incremental CHA resolution.
src/domain/graph/builder/stages/build-edges.ts Passes caller-file identity into inline CHA resolution.
tests/unit/cha.test.ts Adds focused CHA scoping and inherited-method regression tests, but does not combine an inherited method with an unrelated concrete-class override.
tests/integration/issue-2237-cha-implementor-scoping.test.ts Adds dual-engine regression coverage for implementor and ancestor collisions without exercising the premature concrete-method fallback.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Call["Typed virtual call"] --> Root["CHA receiver root + caller file"]
  Root --> BFS["File-aware implementor BFS"]
  BFS --> RTA{"Implementor instantiated?"}
  RTA -->|No| BFS
  RTA -->|Yes| Scoped["Same-file qualified method lookup"]
  Scoped -->|Found| Edge["Emit CHA call edge"]
  Scoped -->|Missing| Global["Project-wide qualified-name fallback"]
  Global -->|Found| Wrong["May select unrelated same-named class method"]
  Global -->|Missing| Parent["Walk scoped parent chain"]
  Parent --> Scoped
Loading

Reviews (2): Last reviewed commit: "fix(cha): preserve file identity through..." | Re-trigger Greptile

Comment on lines +408 to +410
const qualified = `${current}.${methodName}`;
const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
if (found.length > 0) return found;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Scoped implementor identity is lost

When unrelated files declare the same interface, implementing class, and method names, the scoped BFS selects a bare class name and lookup.byName returns that qualified method from every file, causing false call edges to unrelated implementations. Preserve the selected implementor's file identity through method resolution; the DB and Rust mirrors require the same correction.

Knowledge Base Used:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Fixed in 8b3d112. File identity is now propagated through the whole BFS (not just the root) and into the qualified-method lookup: each hop prefers a same-file lookup and falls back to the bare/global one only when the scoped lookup finds nothing — never a regression, only a precision gain when file identity is known. Mirrored across cha.ts, helpers.ts's runChaPostPass, native-orchestrator.ts's runPostNativeCha, and Rust's build_edges.rs.

Added a dedicated regression test (two files each declaring their own same-named implementor class with its own same-named method) in tests/unit/cha.test.ts, crates/codegraph-core/.../build_edges.rs, and tests/integration/issue-2237-cha-implementor-scoping.test.ts — verified it reproduces the leak without this fix and is clean with it.

Comment thread src/domain/graph/builder/cha.ts Outdated
const qualified = `${current}.${methodName}`;
const found = lookup.byName(qualified).filter((n) => n.kind === 'method');
if (found.length > 0) return found;
current = chaCtx.parents.get(current);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Ancestor walk crosses class hierarchies

When two files declare an identically named class with different parent chains, this first-write-wins bare-name lookup can follow the unrelated class's ancestors, causing an inherited dispatch to target the wrong method or omit the correct edge. The walk must retain the selected concrete class's file identity, with the equivalent change applied to the DB and Rust implementations.

Knowledge Base Used:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Fixed in 8b3d112 (same commit as the sibling finding above). The ancestor walk now also carries the known file forward: at each hop it prefers parentsByFile/cha_parents_by_file (scoped to the descendant's own file) before falling back to the bare, first-write-wins parents/cha_parents map — so a same-named class in an unrelated file with a different parent chain can no longer be followed by mistake.

Added a dedicated regression test (two files each declaring their own same-named class with a different parent, with the bare map deliberately poisoned toward the wrong one) in the same three test files as the sibling finding — verified it reproduces the cross-hierarchy walk without this fix and is clean with it.

github-actions Bot commented Aug 9, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

18 functions changed23 callers affected across 7 files

  • recordImplements in src/domain/graph/builder/cha.ts:70 (3 transitive callers)
  • recordExtends in src/domain/graph/builder/cha.ts:100 (3 transitive callers)
  • addToFileScoped in src/domain/graph/builder/cha.ts:126 (7 transitive callers)
  • buildChaContext in src/domain/graph/builder/cha.ts:168 (3 transitive callers)
  • buildChaContextFromDb in src/domain/graph/builder/cha.ts:226 (3 transitive callers)
  • resolveMethodViaAncestors in src/domain/graph/builder/cha.ts:412 (7 transitive callers)
  • resolveChaTargets in src/domain/graph/builder/cha.ts:473 (9 transitive callers)
  • buildImplementorMap in src/domain/graph/builder/helpers.ts:477 (3 transitive callers)
  • findMethodViaAncestors in src/domain/graph/builder/helpers.ts:626 (3 transitive callers)
  • expandChaCall in src/domain/graph/builder/helpers.ts:669 (3 transitive callers)
  • runChaPostPass in src/domain/graph/builder/helpers.ts:754 (3 transitive callers)
  • emitChaDispatchForCall in src/domain/graph/builder/incremental.ts:1917 (3 transitive callers)
  • buildChaPostPass in src/domain/graph/builder/stages/build-edges.ts:965 (3 transitive callers)
  • emitChaCallEdgesForCall in src/domain/graph/builder/stages/build-edges.ts:1834 (3 transitive callers)
  • buildChaImplementorsMap in src/domain/graph/builder/stages/native-orchestrator.ts:670 (3 transitive callers)
  • expandChaEdges in src/domain/graph/builder/stages/native-orchestrator.ts:915 (3 transitive callers)
  • runPostNativeCha in src/domain/graph/builder/stages/native-orchestrator.ts:1093 (3 transitive callers)
  • runPostNativeThisDispatch in src/domain/graph/builder/stages/native-orchestrator.ts:1425 (3 transitive callers)

Fixes cargo fmt (rustfmt) failure from the prior push, and addresses a
Greptile review finding: once resolveChaTargets/resolve_cha_dispatch (and
their DB-driven mirrors) scope the BFS root to a specific file, that file
identity was dropped again before the qualified-method lookup and the
ancestor-parent lookup — so a same-named concrete class (or a same-named
class with a different parent) declared independently in an unrelated
file could still leak into the resolved edge.

Propagate the known file through every BFS level (not just the root) and
into resolveMethodViaAncestors/resolve_method_via_ancestors: each step
now prefers a same-file lookup (method node, or parents-by-file edge) and
falls back to the prior bare/global lookup when nothing scoped is found
— never a regression, only a precision gain when file identity happens
to be known. Mirrored across cha.ts, helpers.ts, native-orchestrator.ts,
and build_edges.rs.

Internal resolver fix — no README/CLAUDE.md/ROADMAP-documented behavior
changes. docs check acknowledged.

Impact: 9 functions changed, 18 affected

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment on lines +424 to +428
const qualified = `${current}.${methodName}`;
const scopedFound = currentFile ? lookup.byNameAndFile(qualified, currentFile) : [];
const found = (scopedFound.length > 0 ? scopedFound : lookup.byName(qualified)).filter(
(n) => n.kind === 'method',
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Global fallback bypasses ancestor walk

When a file-scoped concrete implementor inherits the dispatched method and an unrelated file declares the same concrete class name with its own same-named method, the empty scoped lookup falls back to the project-wide byName result before consulting the selected class's parent. CHA therefore emits a false edge to the unrelated method and omits the inherited target from the selected hierarchy.

Knowledge Base Used:

carlos-alm merged commit f218736 into main Aug 9, 2026
64 of 66 checks passed
carlos-alm deleted the fix/issue-2237-cha-rta-dispatch-scoping branch August 9, 2026 16:54
github-actions Bot locked and limited conversation to collaborators Aug 9, 2026
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 subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant


Back | FazBrowse Home | New Git URL