fix(extract): skip calls to sourceless stubs

This commit is contained in:
Dev Chiniwala committed 2026-09-13 15:42:21 +05:30
1 parent fe66389083
commit 9f0ad48259
3 files changed
+89 -1

No files matched your search

+6
View File
@@ -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
+8 -1
View File
@@ -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)
+75
View File
@@ -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