fix(extract): scope Pascal/Delphi call resolution + resolve inherited calls across files (#1739)

Both Pascal extractors resolved every call via a single file-wide
{method_name: node_id} dict, so two unrelated classes declaring a same-named
method (property accessors, generated COM/TLB wrapper classes) collapsed onto
whichever declaration was inserted last, producing wrong cross-class `calls`
edges. Resolution is now scoped: own class -> ancestor chain (inherits) ->
file-level free function -> unambiguous file-wide match; ambiguous at every
level emits no edge rather than guessing (same god-node guard as the Ruby
resolver).

Adds graphify/pascal_resolution.py, a corpus-wide post-extraction resolver
(registered via resolver_registry) that walks the inherits chain across file
boundaries, so a call from a manual descendant to a method it inherits from a
base class in a separate unit (the generated-base/manual-descendant split)
resolves. Also stops both extractors from emitting a duplicate base-class stub
carrying the referencing file's source_file, which collided with the real node
under cross-file id disambiguation. cache.py gives the new raw_calls bucket the
same portable-path treatment as nodes/edges so it round-trips.

Re-applied to the post-#1737 module layout (extractor hunks land in
graphify/extractors/pascal.py; registration stays in extract.py). Added one
adaptation the original PR predated: the cross-file resolver's god-node guard
now counts DISTINCT method nids, because the tree-sitter extractor emits a
method edge for both the interface declaration and the implementation, so the
same method_nid arrives twice -- without deduping, every inherited call looked
ambiguous and resolved to nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
richtextandClaude Opus 4.8 authored and safishamsi committed 2026-07-09 00:32:21 +01:00
1 parent 2f9deae8d5
commit d89efbf9ef
11 files changed
+655 -54

No files matched your search

+7 -3
View File
@@ -276,7 +276,11 @@ def _relativize_source_files_in(payload: dict, root: Path) -> None:
root_resolved = Path(root).resolve()
except OSError:
return
for bucket in ("nodes", "edges", "hyperedges"):
# raw_calls (#: Pascal/Delphi cross-file inherited-call resolution) carries
# source_file the same way nodes/edges/hyperedges do, so it needs the same
# portable-path treatment for cache entries to round-trip correctly across
# machines/checkout directories.
for bucket in ("nodes", "edges", "hyperedges", "raw_calls"):
for item in payload.get(bucket, []):
if not isinstance(item, dict):
continue
@@ -307,7 +311,7 @@ def _absolutize_source_files_in(payload: dict, root: Path) -> None:
root_resolved = Path(root).resolve()
except OSError:
return
for bucket in ("nodes", "edges", "hyperedges"):
for bucket in ("nodes", "edges", "hyperedges", "raw_calls"):
for item in payload.get(bucket, []):
if not isinstance(item, dict):
continue
@@ -400,7 +404,7 @@ def save_cached(path: Path, result: dict, root: Path = Path("."), kind: str = "a
# source_file field's original absolute form. Mutating the input here would
# silently break those remaps on the first extraction pass.
on_disk = result
if isinstance(result, dict) and any(result.get(k) for k in ("nodes", "edges", "hyperedges")):
if isinstance(result, dict) and any(result.get(k) for k in ("nodes", "edges", "hyperedges", "raw_calls")):
import copy as _copy
on_disk = _copy.deepcopy(result)
_relativize_source_files_in(on_disk, root)