From bc877de285e08838f402f2f403b001f2f8ad6060 Mon Sep 17 00:00:00 2001 From: Synvoya <16019863+Synvoya@users.noreply.github.com> Date: Sun, 12 Jul 2026 12:19:47 +1000 Subject: [PATCH] fix(go): don't emit embeds for interface type-set constraints An interface type_elem that is a generics type-set constraint - a union (`A | B`) or an approximation (`~T`) - was walked like an embedded interface, so each non-predeclared term produced an `embeds` heritage edge (e.g. `type Number interface { MyInt | MyFloat }` emitted Number->MyInt and Number->MyFloat embeds). Go only embeds a lone interface type, and each embed is its own element; union/approximation terms are a constraint type set that can never be embedded, so those edges assert composition that does not exist. Detect type-set elems (a `|` or `negated_type` child) and reclassify their terms as `references` (context=type_constraint), preserving the type link without the false heritage edge. Genuine interface embedding (single/qualified/generic interface terms) is unchanged. Adds regression coverage: positive guards for struct and interface embedding, plus a negative control asserting union terms are not embeds. --- graphify/extractors/go.py | 15 +++++++++++++- tests/test_languages.py | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) 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 # ---------------------------------------------------------------------------