Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions graphify/extractors/go.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,27 @@ def walk(node) -> None:
field.start_point[0] + 1, context=ctx)
elif type_body.type == "interface_type":
for elem in type_body.children:
if elem.type == "method_elem":
# A method requirement declared in the interface body
# is part of the interface's contract. Emit it as a
# method node so the interface isn't left an empty
# shell and calls against the interface can resolve
# (mirrors receiver methods and the way the other
# extractors capture interface members).
m_name_node = elem.child_by_field_name("name")
if m_name_node is None:
for mc in elem.children:
if mc.type == "field_identifier":
m_name_node = mc
break
if m_name_node is None:
continue
m_name = _read_text(m_name_node, source)
m_line = elem.start_point[0] + 1
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)
continue
if elem.type != "type_elem":
continue
# A type_elem that is a generics type-set constraint -
Expand Down
108 changes: 108 additions & 0 deletions tests/test_go_interface_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Regression coverage for method requirements declared in a Go interface.

The interface body carries the type's contract. Before the fix the extractor
only handled interface embedding and generic type-set constraints, so an
interface like ``type Reader interface { Read(p []byte) (int, error) }`` became
an empty node with no members - the method requirements were dropped entirely.
"""

from pathlib import Path

from graphify.extract import extract


def _extract(root: Path) -> dict:
return extract(
sorted(root.rglob("*.go")),
cache_root=root,
root=root,
parallel=False,
)


def _methods_of(result: dict, type_label: str) -> set[str]:
"""Labels of nodes reached by a ``method`` edge from the named type."""
by_id = {node["id"]: node for node in result["nodes"]}
type_ids = {nid for nid, n in by_id.items() if n.get("label") == type_label}
return {
by_id[edge["target"]]["label"].strip(".()")
for edge in result["edges"]
if edge.get("relation") == "method" and edge.get("source") in type_ids
}


def test_interface_method_requirements_are_extracted(tmp_path: Path) -> None:
"""Each method requirement becomes a method node under the interface."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
(tmp_path / "shape.go").write_text(
"package repro\n\n"
"type Shape interface {\n"
"\tArea() float64\n"
"\tPerimeter() (float64, error)\n"
"}\n"
)

result = _extract(tmp_path)
assert _methods_of(result, "Shape") == {"Area", "Perimeter"}


def test_interface_embedding_still_produces_a_heritage_edge(tmp_path: Path) -> None:
"""A bare embedded interface stays an ``embeds`` edge, not a method."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
(tmp_path / "rw.go").write_text(
"package repro\n\n"
"type Reader interface {\n"
"\tRead(p []byte) (int, error)\n"
"}\n\n"
"type ReadCloser interface {\n"
"\tReader\n"
"\tClose() error\n"
"}\n"
)

result = _extract(tmp_path)
by_id = {node["id"]: node for node in result["nodes"]}
rc_ids = {nid for nid, n in by_id.items() if n.get("label") == "ReadCloser"}
reader_ids = {nid for nid, n in by_id.items() if n.get("label") == "Reader"}

# The embedded interface is heritage, not a method of ReadCloser.
assert any(
edge.get("relation") == "embeds"
and edge.get("source") in rc_ids
and edge.get("target") in reader_ids
for edge in result["edges"]
)
# Only the directly declared method requirement is a method of ReadCloser.
assert _methods_of(result, "ReadCloser") == {"Close"}
assert _methods_of(result, "Reader") == {"Read"}


def test_interface_method_is_distinct_from_a_concrete_method(tmp_path: Path) -> None:
"""An interface's ``Area`` and a struct's ``Area`` are two separate nodes."""
(tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n")
(tmp_path / "shape.go").write_text(
"package repro\n\n"
"type Shape interface {\n"
"\tArea() float64\n"
"}\n\n"
"type Rect struct{ W, H float64 }\n\n"
"func (r Rect) Area() float64 { return r.W * r.H }\n"
)

result = _extract(tmp_path)
by_id = {node["id"]: node for node in result["nodes"]}
shape_ids = {nid for nid, n in by_id.items() if n.get("label") == "Shape"}
rect_ids = {nid for nid, n in by_id.items() if n.get("label") == "Rect"}

shape_area = {
edge["target"]
for edge in result["edges"]
if edge.get("relation") == "method" and edge.get("source") in shape_ids
}
rect_area = {
edge["target"]
for edge in result["edges"]
if edge.get("relation") == "method" and edge.get("source") in rect_ids
}
assert shape_area and rect_area
assert shape_area.isdisjoint(rect_area), "interface and struct methods collapsed"
Loading