mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 18:37:12 +00:00
fix(swift): type-qualified static calls resolve as EXTRACTED, not INFERRED (#1533)
A type-qualified Swift call (`Type.staticMethod()`, `Singleton.shared.method()`) names the receiver type explicitly in source, so the resolved edge is an exact reference — now emitted as EXTRACTED (1.0), matching the Python qualified-class-method pass (_resolve_python_member_calls). Instance calls whose receiver type comes from local inference (`obj.method()`) stay INFERRED (0.8). Resolution and the single-definition god-node guard are unchanged. This addresses the actionable part of #1533's "static calls" report: the edge was always produced (graphify models calls as method->method), it was just under-confident. Updated the confidence test to assert the instance/type-qualified split. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-5
@@ -9492,9 +9492,10 @@ def _resolve_swift_member_calls(
|
||||
(#543/#1219). Swift extractors record the receiver of each member call and a
|
||||
per-file ``name -> type`` table (``swift_type_table``); this pass uses them to
|
||||
type the receiver, then emits an edge ONLY when that type name resolves to
|
||||
exactly one definition. Everything it adds is INFERRED (type inference, not an
|
||||
explicit import), and the line-12503 drop stays intact: this is purely
|
||||
additive and fires only on receiver-typed Swift calls.
|
||||
exactly one definition. A type-qualified call (``Type.staticMethod()``) is
|
||||
EXTRACTED (the type is named explicitly in source); an instance call typed via
|
||||
local inference (``obj.method()``) is INFERRED. The shared-pass member-call drop
|
||||
stays intact: this is purely additive and fires only on receiver-typed Swift calls.
|
||||
|
||||
Must run after id-disambiguation so node ids and caller_nids are final.
|
||||
"""
|
||||
@@ -9551,8 +9552,10 @@ def _resolve_swift_member_calls(
|
||||
# declaring file's local type table.
|
||||
if receiver[:1].isupper():
|
||||
type_name = receiver
|
||||
type_qualified = True
|
||||
else:
|
||||
type_name = type_table_by_file.get(rc.get("source_file", ""), {}).get(receiver)
|
||||
type_qualified = False
|
||||
if not type_name:
|
||||
continue
|
||||
type_defs = type_def_nids.get(_key(type_name), [])
|
||||
@@ -9568,13 +9571,17 @@ def _resolve_swift_member_calls(
|
||||
if target == caller or (caller, target) in existing_pairs:
|
||||
continue
|
||||
existing_pairs.add((caller, target))
|
||||
# A type-qualified call (`Type.staticMethod()`) names the receiver type
|
||||
# explicitly in source, so it is an exact reference — EXTRACTED, matching
|
||||
# the Python qualified-class-method pass (#1533). An instance call whose
|
||||
# receiver type came from local inference (`obj.method()`) stays INFERRED.
|
||||
all_edges.append({
|
||||
"source": caller,
|
||||
"target": target,
|
||||
"relation": relation,
|
||||
"context": "call",
|
||||
"confidence": "INFERRED",
|
||||
"confidence_score": 0.8,
|
||||
"confidence": "EXTRACTED" if type_qualified else "INFERRED",
|
||||
"confidence_score": 1.0 if type_qualified else 0.8,
|
||||
"source_file": rc.get("source_file", ""),
|
||||
"source_location": rc.get("source_location"),
|
||||
"weight": 1.0,
|
||||
|
||||
@@ -70,32 +70,42 @@ def test_swift_cross_file_member_calls_resolve(tmp_path: Path):
|
||||
assert (".go()", "calls", ".method()") in edges # Singleton.shared.method()
|
||||
|
||||
|
||||
def test_swift_cross_file_member_calls_are_inferred_and_resolve_to_real_nodes(tmp_path: Path):
|
||||
# The new edges must be INFERRED (type inference, not an explicit import) and
|
||||
# land on real definition nodes so build_from_json keeps them.
|
||||
def test_swift_cross_file_member_calls_have_correct_confidence_and_resolve(tmp_path: Path):
|
||||
# Instance calls typed via local inference (vm.update(), self.svc.fetch()) are
|
||||
# INFERRED; type-qualified static calls (SessionType.staticMethod(),
|
||||
# Singleton.shared.method()) name the receiver type explicitly in source, so
|
||||
# they are EXTRACTED, matching the Python qualified-class-method pass (#1533).
|
||||
# All must land on real definition nodes so build_from_json keeps them.
|
||||
files = _issue_fixture(tmp_path / "src")
|
||||
result = extract(files, cache_root=tmp_path / "cache")
|
||||
|
||||
node_ids = {n["id"] for n in result["nodes"]}
|
||||
src_by_id = {n["id"]: n.get("source_file") for n in result["nodes"]}
|
||||
|
||||
member_targets = {".update()", ".fetch()", ".staticMethod()", ".method()"}
|
||||
seen_targets: set[str] = set()
|
||||
inferred_targets = {".update()", ".fetch()"}
|
||||
extracted_targets = {".staticMethod()", ".method()"}
|
||||
seen_inferred: set[str] = set()
|
||||
seen_extracted: set[str] = set()
|
||||
for e in result["edges"]:
|
||||
tgt_label = _label(result, e["target"])
|
||||
if e.get("relation") == "calls" and tgt_label in member_targets:
|
||||
assert e["confidence"] == "INFERRED"
|
||||
assert e["confidence_score"] == 0.8
|
||||
assert e["target"] in node_ids
|
||||
assert src_by_id.get(e["target"]) # resolved to a real, source-backed def
|
||||
seen_targets.add(tgt_label)
|
||||
assert seen_targets == member_targets
|
||||
if e.get("relation") != "calls":
|
||||
continue
|
||||
if tgt_label in inferred_targets:
|
||||
assert e["confidence"] == "INFERRED" and e["confidence_score"] == 0.8
|
||||
assert e["target"] in node_ids and src_by_id.get(e["target"])
|
||||
seen_inferred.add(tgt_label)
|
||||
elif tgt_label in extracted_targets:
|
||||
assert e["confidence"] == "EXTRACTED" and e["confidence_score"] == 1.0
|
||||
assert e["target"] in node_ids and src_by_id.get(e["target"])
|
||||
seen_extracted.add(tgt_label)
|
||||
assert seen_inferred == inferred_targets
|
||||
assert seen_extracted == extracted_targets
|
||||
|
||||
# Edges survive graph construction (no dangling targets pruned).
|
||||
g = build_from_json(result)
|
||||
surviving = sum(
|
||||
1 for _, _, d in g.edges(data=True)
|
||||
if d.get("confidence") == "INFERRED" and d.get("relation") == "calls"
|
||||
if d.get("relation") == "calls" and d.get("confidence") in ("INFERRED", "EXTRACTED")
|
||||
)
|
||||
assert surviving >= 5
|
||||
|
||||
|
||||
Reference in New Issue
Block a user