Skip to content

feat(resolution): link Python base methods to their overrides (python-override) - #2076

Open
JosefAschauer wants to merge 3 commits into
colbymchenry:mainfrom
JosefAschauer:feat/python-override-dispatch
Open

JosefAschauer wants to merge 3 commits into
colbymchenry:mainfrom
JosefAschauer:feat/python-override-dispatch

Conversation

@JosefAschauer

Copy link
Copy Markdown
Contributor

Problem

callers/impact of a Python method override miss every call that binds to the base declaration. In the #2074 repro, QdrantConnection.delete_payload_fields shows 0 callers, even though gc_typed calls it through a DocStoreConnection-typed receiver. A module global holding one of several backends (#2075) binds the same way. At runtime those calls run the override, but no static edge reaches it.

The existing interface-impl and cpp-override synthesis passes bridge this for Java, Kotlin, C#, TS/JS, Swift, Scala, Go, Rust and C++. They don't cover Python.

Fix

A pythonOverrideEdges synthesis pass (synthesizedBy: 'python-override', calls, provenance heuristic, with a label in explore output):

  • Each Python override is linked from the nearest declaration of the same name up its extends chain. Python classes often inherit through a base that doesn't redeclare the method (Leaf(Mid(Base))). A branch of the walk stops at its first declaration, so an override of an override links only to its parent.

  • Not dispatch, so skipped on either side:

    • class-creation hooks: __init__, __new__, __init_subclass__, __class_getitem__;
    • @staticmethod and @classmethod;
    • properties: @property, cached_property, .setter/.getter/.deleter.

    Python nodes only carry isStatic, so decorators are read from the source lines, including past comment and blank lines.

  • Only real supertypes: an extends edge is followed when it was resolved through an import or its base is in the same file. A cross-file bare-name guess (exact-match / fuzzy) is not followed, whatever its confidence. Confidence reflects proximity, not whether the base was guessed.

  • No per-base cap: each edge is a real nearest-declaration override, and a cap would drop some silently.

Validation

  • __tests__/resolution.test.ts adds 2 cases:

    • direct override, override through a non-redeclaring intermediate base, and override of an override (nearest only);
    • no edge for a constructor, static method, class method (including one with a comment before the def) or property;
    • an unrelated same-name class is not linked, and getCallers of an override reaches the base-typed call site;
    • guessed supertypes at the repo root and under a package path are not followed; an import-resolved base and a same-file base are.

    Every rule was mutation-checked.

  • npx tsc --noEmit: clean. resolution, frameworks-integration, extraction, function-ref and ui-flow-api tests: 1014 passed.

  • Full suite on Linux (together with fix(resolution): resolve method values on untyped Python module globals (#2074) #2075): 5386 passed, 4 failed. The failures are environmental:

    • bundle-launcher ×2: no zip binary.
    • cli-sync ×2: the host runs Node 26, which prints the unsupported-Node banner.
  • Real project (RAGFlow, 5.5k files):

    • 991 python-override edges. The samples I checked were all genuine overrides, e.g. ComponentBase._invoke → each agent tool's _invoke, and LoadConnector.load_credentials → each connector's.
    • impact of QdrantConnection.delete_payload_fields now reaches its base-typed caller.
    • Indexing time is about the same.
  • Independent review (Grok): all findings fixed and pinned. Final verdict SHIP.

Not addressed

  • Generic bases (class Leaf(Base[str])) produce no extends edge at extraction time, so no override edge is created. This is a pre-existing extraction gap, better handled as a separate change.
  • MRO through mixins: a method that only a mixin declares isn't reached from a base-typed call. That's correct, because the call binds to the base's own declaration.

Refs #2074 (the "Related, not addressed here" part). Independent of #2075.

🤖 Generated with Claude Code

JosefAschauer and others added 3 commits September 28, 2026 09:18
…-override)

A call bound to a base method (a base-typed receiver `store: Base;
store.fetch()`, or a module global holding one of several backends) runs
the subclass override at runtime, but no static edge reaches the override,
so `callers`/`impact` of `Leaf.fetch` missed every such call. The
interface-impl synthesis that bridges this for Java, Kotlin, C#, TS/JS,
Swift, Scala, Go and Rust did not cover Python.

The new pass links each Python override to the NEAREST declaration of the
same name up its `extends` chain, since Python classes often inherit through
a base that does not redeclare the method (`Leaf(Mid(Base))`). Class-creation
hooks (`__init__`, `__new__`, `__init_subclass__`, `__class_getitem__`) are
not virtual and are skipped. Edges are `calls`, provenance `heuristic`,
`synthesizedBy: 'python-override'`, labelled in explore output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
…ed supertypes

Review follow-up. `Base.make()` on a @staticmethod or @classmethod names
its class and never runs the override, and a @Property is read, not
called: skip them (and cached_property / .setter / .getter / .deleter) on
either side; Python decorators are read from the source lines, since only
`isStatic` is on the node. An `extends` edge resolved by bare-name guess
(confidence < 0.7, e.g. two `Base` classes and no import) no longer
carries dispatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
…ypes only

Review follow-up. Confidence reflects proximity, not whether a supertype was
guessed: a same-file base can score 0.4 while a cross-file bare-name guess
under a package path scores 0.7. Follow an `extends` edge when it was
resolved through an import or its base is in the same file; skip a
cross-file `exact-match` / `fuzzy` guess. Decorators are read past blank and
comment lines between them and the def.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant