mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 08:59:26 +00:00
fix(go): extract method requirements declared in an interface body
The interface_type branch only handled interface embedding and generic
type-set constraints, so the method requirements that make up an
interface's contract were dropped. An interface such as
type Shape interface {
Area() float64
Perimeter() (float64, error)
}
became an empty node - Area and Perimeter never entered the graph, and
nothing could resolve against the interface's method set.
Walk the method_elem children too, mirroring the receiver-method path:
each requirement becomes a .Method() node hung off the interface via a
method edge. Embedding (type_elem) keeps emitting embeds/references, so
the two never blur.
Adds regression coverage: requirements surface as method nodes, an
embedded interface stays a heritage edge, and an interface method and a
concrete struct method of the same name remain distinct nodes.
This commit is contained in:
1 parent
b9cd957072
commit
739940a42b
2 files changed
+129
No files matched your search
@@ -370,6 +370,27 @@ def extract_go(path: Path) -> dict:
|
||||
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 -
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user