From bda82f9deddc4960f27a3b138ddca72ed6ae0379 Mon Sep 17 00:00:00 2001 From: oleksii-tumanov Date: Tue, 22 Sep 2026 02:34:03 -0500 Subject: [PATCH] fix(go): preserve interface method signature references (cherry picked from commit 00fb3099839a9145a097c1db881dae192aedfc75) --- graphify/extractors/go.py | 1 + tests/test_go_interface_methods.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/graphify/extractors/go.py b/graphify/extractors/go.py index a6bdd4ae6..2dde79bd2 100644 --- a/graphify/extractors/go.py +++ b/graphify/extractors/go.py @@ -390,6 +390,7 @@ def extract_go(path: Path) -> dict: m_nid = symbol_nid(_make_id(type_nid, m_name), m_name) add_node(m_nid, f".{m_name}()", m_line) add_edge(type_nid, m_nid, "method", m_line) + emit_go_method_refs(elem, m_nid, m_line) continue if elem.type != "type_elem": continue diff --git a/tests/test_go_interface_methods.py b/tests/test_go_interface_methods.py index d90c846dd..f615a1876 100644 --- a/tests/test_go_interface_methods.py +++ b/tests/test_go_interface_methods.py @@ -8,6 +8,8 @@ an empty node with no members - the method requirements were dropped entirely. from pathlib import Path +import pytest + from graphify.extract import extract @@ -106,3 +108,60 @@ def test_interface_method_is_distinct_from_a_concrete_method(tmp_path: Path) -> } assert shape_area and rect_area assert shape_area.isdisjoint(rect_area), "interface and struct methods collapsed" + + +def _signature_refs(result: dict, owner_label: str) -> tuple[str, list[dict]]: + by_id = {node["id"]: node for node in result["nodes"]} + method_ids = { + edge["target"] + for edge in result["edges"] + if edge.get("relation") == "method" + and by_id[edge["source"]].get("label") == owner_label + } + assert len(method_ids) == 1 + method_id = method_ids.pop() + return method_id, [ + {**edge, "target_label": by_id[edge["target"]]["label"]} + for edge in result["edges"] + if edge.get("relation") == "references" and edge["source"] == method_id + ] + + +@pytest.mark.parametrize( + ("signature", "body"), + [ + ("Handle(Request) Response", "return Response{}"), + ("Handle(req *Request) (res *Response, err error)", "return nil, nil"), + ], +) +def test_interface_method_signature_keeps_type_references(tmp_path: Path, signature, body): + path = tmp_path / "contract.go" + path.write_text( + "package contracts\n\n" + "type Request struct{}\n" + "type Response struct{}\n" + "type Service interface {\n" + f" {signature}\n" + "}\n\n" + "type Impl struct{}\n" + f"func (Impl) {signature} {{ {body} }}\n", + encoding="utf-8", + ) + result = _extract(tmp_path) + interface_id, interface_refs = _signature_refs(result, "Service") + concrete_id, concrete_refs = _signature_refs(result, "Impl") + expected = {("Request", "parameter_type"), ("Response", "return_type")} + assert {(edge["target_label"], edge["context"]) for edge in concrete_refs} == expected + assert {(edge["target_label"], edge["context"]) for edge in interface_refs} == expected + assert len(interface_refs) == 2 + assert interface_id != concrete_id + assert not any(node["label"] == "error" for node in result["nodes"]) + for edge in interface_refs: + assert edge["source_file"] == "contract.go" + assert edge["source_location"] == "L6" + assert edge["confidence"] == "EXTRACTED" + + # A warm extraction replays the same source-owned signature edges. + warm = _extract(tmp_path) + assert _signature_refs(warm, "Service") == (interface_id, interface_refs) + assert _signature_refs(warm, "Impl") == (concrete_id, concrete_refs)