mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 16:39:43 +00:00
fix(extract): skip calls to sourceless stubs
This commit is contained in:
1 parent
fe66389083
commit
9f0ad48259
3 files changed
+89
-1
No files matched your search
@@ -7484,6 +7484,12 @@ def extract(
|
||||
if tgt is None:
|
||||
continue
|
||||
has_import_evidence = False
|
||||
# A source-less candidate is an unresolved external stub, not a
|
||||
# project-defined callable. It can remain a target of type/reference
|
||||
# edges, but a name-only call must not turn it into a cross-file calls
|
||||
# hub (#3156). Source-backed project definitions continue normally.
|
||||
if not nid_to_source_file.get(tgt):
|
||||
continue
|
||||
if rc.get("indirect"):
|
||||
# Cross-file indirect dispatch: a callback passed BY NAME
|
||||
# (`from .h import fn; pool.submit(fn)`, or listed in a dispatch
|
||||
|
||||
@@ -5861,7 +5861,14 @@ def _extract_generic(
|
||||
and not nid_to_sf.get(tgt_nid)
|
||||
):
|
||||
tgt_nid = None
|
||||
if tgt_nid:
|
||||
# A name-only reference can leave a source-less stub behind when
|
||||
# the target is outside the scanned corpus (for example,
|
||||
# ``HTTPException`` imported from FastAPI). It is useful for the
|
||||
# type/reference edge, but it is not a project-defined callable
|
||||
# and must not become a calls hub. Real definitions have a
|
||||
# source_file and continue through the normal call path.
|
||||
external_stub_target = bool(tgt_nid and not nid_to_sf.get(tgt_nid))
|
||||
if tgt_nid and not external_stub_target:
|
||||
pair = (caller_nid, tgt_nid)
|
||||
if pair not in seen_call_pairs:
|
||||
seen_call_pairs.add(pair)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Regression tests for unresolved external call targets (#3156)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _nodes(result: dict) -> dict[str, dict]:
|
||||
return {node["id"]: node for node in result["nodes"]}
|
||||
|
||||
|
||||
def _labelled_edges(result: dict, nodes: dict[str, dict], label: str) -> list[dict]:
|
||||
return [
|
||||
edge
|
||||
for edge in result["edges"]
|
||||
if nodes.get(edge.get("target"), {}).get("label") == label
|
||||
]
|
||||
|
||||
|
||||
def test_sourceless_external_call_target_is_not_emitted(tmp_path: Path):
|
||||
"""A type-reference stub must not become a project-wide calls hub."""
|
||||
typed = tmp_path / "typed.py"
|
||||
typed.write_text(
|
||||
"from fastapi import HTTPException\n"
|
||||
"\n"
|
||||
"def typed_error() -> HTTPException:\n"
|
||||
" return HTTPException(status_code=400)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
caller = tmp_path / "caller.py"
|
||||
caller.write_text(
|
||||
"def bare_error():\n"
|
||||
" return HTTPException(status_code=401)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([typed, caller], cache_root=tmp_path, root=tmp_path)
|
||||
nodes = _nodes(result)
|
||||
external = [node for node in nodes.values() if node.get("label") == "HTTPException"]
|
||||
|
||||
assert len(external) == 1
|
||||
assert external[0].get("source_file") == ""
|
||||
references = _labelled_edges(result, nodes, "HTTPException")
|
||||
assert any(edge.get("context") == "return_type" for edge in references)
|
||||
assert not any(edge.get("relation") == "calls" for edge in references)
|
||||
|
||||
|
||||
def test_source_backed_exception_call_is_preserved(tmp_path: Path):
|
||||
"""A project-defined exception with the same name remains a real call target."""
|
||||
definitions = tmp_path / "exceptions.py"
|
||||
definitions.write_text(
|
||||
"class HTTPException(Exception):\n"
|
||||
" pass\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
caller = tmp_path / "api.py"
|
||||
caller.write_text(
|
||||
"from exceptions import HTTPException\n"
|
||||
"\n"
|
||||
"def raise_error():\n"
|
||||
" return HTTPException()\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([definitions, caller], cache_root=tmp_path, root=tmp_path)
|
||||
nodes = _nodes(result)
|
||||
calls = _labelled_edges(result, nodes, "HTTPException")
|
||||
|
||||
assert any(
|
||||
edge.get("relation") == "calls"
|
||||
and nodes[edge["target"]].get("source_file") == "exceptions.py"
|
||||
for edge in calls
|
||||
), calls
|
||||
Reference in new issue
Block a user