fix(extract): rewire cross-module function references to their definition (#1781)

_rewire_unique_stub_nodes gated merge targets through _is_type_like_definition,
which rejects any label ending in `)`. So a function referenced from another
module (passed by name, e.g. FastAPI's Depends(get_db)) left its reference edge
dangling on a sourceless name-only stub while the real def had zero incoming
edges — "who references this function" returned nothing. Class/type symbols were
fine; only functions/methods suffered.

Top-level function defs (label `name()`, not `.name()` methods or `Class.m()`
qualifiers) are now eligible rewire targets, but only when:
  - the label key matches exactly one such function corpus-wide (existing
    unique-candidate guard — two same-named functions stay unresolved), AND
  - the candidate shares a language family with the stub's referrers, so a
    Python `get_db` reference can't bind to a unique Go `get_db()` (#1718/#1749
    interop guard), AND
  - the stub is not used as a supertype (inherits/implements/extends) — you
    don't inherit from a function.

Types are unchanged. Regression tests: cross-module function ref binds to def;
cross-language, ambiguous, and supertype cases all correctly left unresolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
EmilNyg
2026-07-10 21:44:31 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent ce5af6fbc5
commit 3c3b6554e7
4 changed files with 121 additions and 6 deletions
+3 -1
View File
@@ -2,7 +2,9 @@
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
## 0.9.12 (unreleased)
## 0.9.13 (unreleased)
- Fix: cross-module references to a function now resolve to its definition instead of dangling on a name-only stub (#1781, thanks @EmilNyg). `_rewire_unique_stub_nodes` gated merge targets through `_is_type_like_definition`, which rejects any label ending in `)` — so function/method defs could never absorb their reference stubs, and "who references this function" returned nothing on the definition node while a sourceless stub held all the edges. Top-level function defs are now eligible rewire targets when the label match is globally unique, gated by a language-family match with the referrers (a Python `get_db` reference can't bind to a unique Go `get_db()`) and excluding stubs used as a supertype (`inherits`/`implements`/`extends` — you don't inherit from a function). Types are unchanged.
- Fix: live PostgreSQL introspection (`--postgres`) now emits foreign-key `references` edges under a read-only role (#1746, thanks @rithyKabir). The FK query read `information_schema.referential_constraints`, which is privilege-filtered — a role with only SELECT sees zero FK rows while tables/views/routines still appear, so every `references` edge silently vanished. It now reads the world-readable `pg_catalog.pg_constraint` (keyed by oid, which also fixes same-named constraints on sibling tables cross-matching in the old name-based joins), preserving composite-FK column order via `UNNEST ... WITH ORDINALITY`.
+56 -4
View File
@@ -1805,10 +1805,27 @@ def _node_label_key(node: dict, fold: bool = False) -> str:
return key.lower() if fold else key
def _is_top_level_function_definition(node: dict) -> bool:
"""A free/top-level function def (label ``name()``), not a method or type.
Methods carry a leading dot (``.foo()``) or a qualifier (``Class.foo()``);
excluding those keeps a bare-name reference from binding to a receiver-scoped
method, which the receiver-typed resolvers own (#1781).
"""
label = str(node.get("label", "")).strip()
return (
node.get("file_type") == "code"
and label.endswith(")")
and not label.startswith(".")
and "." not in label
)
def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None:
"""Map unresolved no-source stubs to a unique real definition with the same label."""
real_by_label: dict[str, list[dict]] = {} # exact-case (all languages)
real_by_label: dict[str, list[dict]] = {} # exact-case type-like (all languages)
real_by_label_ci: dict[str, list[dict]] = {} # case-INSENSITIVE-language reals only
func_by_label: dict[str, list[dict]] = {} # top-level function defs (#1781)
stubs: list[dict] = []
for node in nodes:
@@ -1824,9 +1841,34 @@ def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None:
if _lang_is_case_insensitive(node.get("source_file")):
real_by_label_ci.setdefault(
_node_label_key(node, fold=True), []).append(node)
elif _is_top_level_function_definition(node):
func_by_label.setdefault(key, []).append(node)
continue
stubs.append(node)
# Language families referencing each stub, for the function-merge guard (#1781):
# a cross-module `references` edge to a function used to dangle on a sourceless
# name-only stub because functions were excluded as rewire targets. We now allow
# a UNIQUE function definition to absorb it, but only when it shares a language
# family with the stub's referrers — so a Python `get_db` reference can't bind to
# a unique Go `get_db()` (mirrors the #1718/#1749 interop guard).
stub_ids = {str(s.get("id")) for s in stubs if s.get("id")}
stub_families: dict[str, set] = {}
supertype_stub_ids: set[str] = set() # stubs used as a base type — never a function
_SUPERTYPE_RELATIONS = {"inherits", "implements", "extends"}
for edge in edges:
rel = edge.get("relation")
for endpoint in ("source", "target"):
nid = edge.get(endpoint)
if nid in stub_ids:
fam = _lang_family(edge.get("source_file"))
if fam is not None:
stub_families.setdefault(str(nid), set()).add(fam)
# A stub referenced as a supertype must resolve to a class/type,
# not a same-named function (you don't inherit from a function).
if endpoint == "target" and rel in _SUPERTYPE_RELATIONS:
supertype_stub_ids.add(str(nid))
remap: dict[str, str] = {}
for stub in stubs:
stub_id = str(stub.get("id", ""))
@@ -1834,12 +1876,22 @@ def _rewire_unique_stub_nodes(nodes: list[dict], edges: list[dict]) -> None:
continue
candidates = real_by_label.get(_node_label_key(stub), [])
if len(candidates) != 1:
# No unique exact match — fall back to a case-insensitive match, but
# No unique exact type match — fall back to a case-insensitive match, but
# only against case-insensitive-language definitions (so a case-sensitive
# `PATH` can never absorb a `Path` reference).
candidates = real_by_label_ci.get(_node_label_key(stub, fold=True), [])
if len(candidates) != 1:
continue
if len(candidates) != 1:
# #1781: no unique type — try a unique top-level FUNCTION definition,
# gated by (a) the stub not being used as a supertype and (b) a
# language-family match with the stub's referrers.
fcands = func_by_label.get(_node_label_key(stub), [])
if len(fcands) == 1 and stub_id not in supertype_stub_ids:
fams = stub_families.get(stub_id, set())
cand_fam = _lang_family(fcands[0].get("source_file"))
if not fams or cand_fam is None or cand_fam in fams:
candidates = fcands
if len(candidates) != 1:
continue
target_id = candidates[0].get("id")
if isinstance(target_id, str) and target_id and target_id != stub_id:
remap[stub_id] = target_id
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "graphifyy"
version = "0.9.12"
version = "0.9.13"
description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
readme = "README.md"
license = { file = "LICENSE" }
+61
View File
@@ -1959,3 +1959,64 @@ def test_matlab_m_not_extracted_as_garbage(tmp_path, capsys):
result = extract([m], cache_root=tmp_path)
assert result["nodes"] == [] # no garbage ObjC nodes
assert "no AST extractor" in capsys.readouterr().err # surfaced, not silent
def test_rewire_binds_cross_module_function_reference_to_definition():
"""#1781: a cross-module reference to a function must land on the real
definition, not a sourceless name-only stub (functions were excluded as
rewire targets)."""
from graphify.extract import _rewire_unique_stub_nodes
nodes = [
{"id": "pkg_dep_get_db", "label": "get_db()", "file_type": "code",
"source_file": "pkg/dep.py", "source_location": "L1"},
{"id": "get_db", "label": "get_db()", "file_type": "code", "source_file": ""},
]
edges = [{"source": "pkg_ep_route", "target": "get_db", "relation": "references",
"source_file": "pkg/ep.py", "weight": 1.0}]
_rewire_unique_stub_nodes(nodes, edges)
assert edges[0]["target"] == "pkg_dep_get_db"
assert "get_db" not in {n["id"] for n in nodes} # stub dropped
def test_rewire_does_not_bind_function_reference_across_language():
"""#1781 safety: a Python reference stub must not bind to a unique Go
function of the same name (mirrors the #1749 interop guard)."""
from graphify.extract import _rewire_unique_stub_nodes
nodes = [
{"id": "svc_get_db", "label": "get_db()", "file_type": "code",
"source_file": "svc/main.go", "source_location": "L1"},
{"id": "get_db", "label": "get_db()", "file_type": "code", "source_file": ""},
]
edges = [{"source": "app_route", "target": "get_db", "relation": "references",
"source_file": "app/route.py", "weight": 1.0}]
_rewire_unique_stub_nodes(nodes, edges)
assert edges[0]["target"] == "get_db" # unchanged — cross-language blocked
def test_rewire_does_not_bind_ambiguous_function_reference():
"""#1781 safety: two same-named functions leave the reference on the stub."""
from graphify.extract import _rewire_unique_stub_nodes
nodes = [
{"id": "a_get_db", "label": "get_db()", "file_type": "code", "source_file": "a.py", "source_location": "L1"},
{"id": "b_get_db", "label": "get_db()", "file_type": "code", "source_file": "b.py", "source_location": "L1"},
{"id": "get_db", "label": "get_db()", "file_type": "code", "source_file": ""},
]
edges = [{"source": "c_route", "target": "get_db", "relation": "references",
"source_file": "c.py", "weight": 1.0}]
_rewire_unique_stub_nodes(nodes, edges)
assert edges[0]["target"] == "get_db" # ambiguous — not merged
def test_rewire_does_not_bind_supertype_stub_to_function():
"""#1781 safety: a stub used as a base type must never resolve to a
same-named, same-language function."""
from graphify.extract import _rewire_unique_stub_nodes
nodes = [
{"id": "factory_BookStore", "label": "BookStore()", "file_type": "code",
"source_file": "factory.py", "source_location": "L1"},
{"id": "BookStore", "label": "BookStore", "file_type": "code", "source_file": ""},
]
edges = [{"source": "store_Sqlite", "target": "BookStore", "relation": "inherits",
"source_file": "store.py", "weight": 1.0}]
_rewire_unique_stub_nodes(nodes, edges)
assert edges[0]["target"] == "BookStore" # inherits stub not bound to function