mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 08:59:26 +00:00
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:
1 parent
2f9deae8d5
commit
d89efbf9ef
11 files changed
+655
-54
No files matched your search
+7
-3
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user