Repository navigation
feat(resolution): link Python base methods to their overrides (python-override) - #2076
JosefAschauer wants to merge 4 commits into
Conversation
…-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
…olbymchenry#2471, colbymchenry#2483; colbymchenry#2076 dropped) into fork main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @JosefAschauer, this is a great addition! On Django, netbox, pretix, mealie, flask and fastapi it adds about 5,000 override links and changes nothing else, and every link we sampled was a real override. Before we land it we need to settle one interaction: with base → override links, |
|
Thanks! Did you review the 'not addressed' items? |
Upstream moved synthesized-hop wording into src/graph/synthesized-hop.ts (colbymchenry#2428); the python-override label moves there as 'base → override', pinned in synthesized-hop.test.ts.
|
A data point on the explore interaction: I merged this branch onto current main (08b187a and later, which includes #3079, 'Flow reaches a named symbol a shorter route gets to first'), and |
|
Thanks for landing the rest so quickly! A gentle ping in case this one slipped past in the batch: it's still open, and it merges into current main without conflicts. With #3079 in main, the two explore tests it used to trip now pass (see my comment above). I'm happy to rebase or adjust anything if you want it in a different shape. |
Problem
callers/impactof a Python method override miss every call that binds to the base declaration. In the #2074 repro,QdrantConnection.delete_payload_fieldsshows 0 callers, even thoughgc_typedcalls it through aDocStoreConnection-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
pythonOverrideEdgessynthesis pass (synthesizedBy: 'python-override',calls, provenanceheuristic, with a label in explore output):Each Python override is linked from the nearest declaration of the same name up its
extendschain. 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:
__init__,__new__,__init_subclass__,__class_getitem__;@staticmethodand@classmethod;@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
extendsedge 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.tsadds 2 cases:getCallersof an override reaches the base-typed call site;Every rule was mutation-checked.
npx tsc --noEmit: clean.resolution,frameworks-integration,extraction,function-refandui-flow-apitests: 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: nozipbinary.cli-sync×2: the host runs Node 26, which prints the unsupported-Node banner.Real project (RAGFlow, 5.5k files):
python-overrideedges. The samples I checked were all genuine overrides, e.g.ComponentBase._invoke→ each agent tool's_invoke, andLoadConnector.load_credentials→ each connector's.impactofQdrantConnection.delete_payload_fieldsnow reaches its base-typed caller.Independent review (Grok): all findings fixed and pinned. Final verdict SHIP.
Not addressed
class Leaf(Base[str])) produce noextendsedge at extraction time, so no override edge is created. This is a pre-existing extraction gap, better handled as a separate change.Refs #2074 (the "Related, not addressed here" part). Independent of #2075.
🤖 Generated with Claude Code