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:
safishamsi
2026-06-29 14:49:42 +01:00
parent 1652dadd60
commit 51fc00a30f
2 changed files with 35 additions and 18 deletions
+12 -5
View File
@@ -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,
+23 -13
View File
@@ -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