diff --git a/graphify/extractors/go.py b/graphify/extractors/go.py index a0db9a693..3c0e1e79f 100644 --- a/graphify/extractors/go.py +++ b/graphify/extractors/go.py @@ -277,6 +277,16 @@ def walk(node) -> None: for elem in type_body.children: if elem.type != "type_elem": continue + # A type_elem that is a generics type-set constraint - + # a union (`A | B`) or an approximation (`~T`) - is NOT + # interface embedding. Go only embeds a lone interface + # type; union/approximation terms can never be embedded, + # so they must not emit `embeds` heritage edges. Keep the + # type link as a `references` (type_constraint) edge. + is_type_set = any( + c.type == "|" or c.type == "negated_type" + for c in elem.children + ) refs = [] for sub in elem.children: if sub.is_named: @@ -285,7 +295,10 @@ def walk(node) -> None: tgt = ensure_named_node(ref_name, elem.start_point[0] + 1) if tgt == type_nid: continue - if role == "type": + if is_type_set: + add_edge(type_nid, tgt, "references", + elem.start_point[0] + 1, context="type_constraint") + elif role == "type": add_edge(type_nid, tgt, "embeds", elem.start_point[0] + 1) else: diff --git a/tests/test_languages.py b/tests/test_languages.py index f610fbc40..8d7a5a88c 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -1472,6 +1472,47 @@ def test_go_receiver_uses_pkg_scope(): assert "sample" not in server_nodes[0]["id"].split(":")[0] +def test_go_interface_embedding_emits_embeds(): + """`type ReaderLogger interface { Logger; Reader }` embeds both interfaces.""" + r = extract_go(FIXTURES / "sample.go") + embeds = _edge_labels(r, "embeds") + assert ("ReaderLogger", "Logger") in embeds + assert ("ReaderLogger", "Reader") in embeds + + +def test_go_struct_embedding_emits_embeds(): + """`type DataProcessor struct { BaseProcessor }` embeds the base struct.""" + r = extract_go(FIXTURES / "sample.go") + assert ("DataProcessor", "BaseProcessor") in _edge_labels(r, "embeds") + + +def test_go_interface_type_union_is_not_embedding(tmp_path): + """A generics type-set constraint (`A | B`) is NOT interface embedding. + + Go only allows *interfaces* to be embedded, and each embed is its own + element. A union of type terms (`MyInt | MyFloat`) is a constraint type + set that can never be embedded, so its terms must not emit `embeds` + heritage edges. They are reclassified as `references` (type_constraint) + so the type link is preserved without asserting false composition. + """ + source = tmp_path / "constraint.go" + source.write_text( + "package p\n" + "type MyInt int\n" + "type MyFloat float64\n" + "type Number interface {\n" + "\tMyInt | MyFloat\n" + "}\n" + ) + r = extract_go(source) + embeds = _edge_labels(r, "embeds") + assert ("Number", "MyInt") not in embeds + assert ("Number", "MyFloat") not in embeds + refs = _edge_labels(r, "references", "type_constraint") + assert ("Number", "MyInt") in refs + assert ("Number", "MyFloat") in refs + + # --------------------------------------------------------------------------- # Julia # ---------------------------------------------------------------------------