From b6c9c34359b13fe6f741ee97aeac9f6002f10295 Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Sun, 4 Oct 2026 13:16:08 +0530 Subject: [PATCH] fix(cpp): extract union specifiers as class-like type nodes --- graphify/extract.py | 9 ++++++-- tests/test_cpp_nested_and_cli.py | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index 9e87c05f9e..db78dc2829 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1359,8 +1359,13 @@ def _get_c_func_name(node, source: bytes) -> str | None: # set of enumerators. Its name and body sit on the same `name`/`body` fields # as struct_specifier (type_identifier + enumerator_list), so it gets a node # and a body walk; the enumerators are emitted by _cpp_extra_walk (the C++ - # parity of Java #1719 / Swift / Scala enums). - class_types=frozenset({"class_specifier", "struct_specifier", "enum_specifier"}), + # parity of Java #1719 / Swift / Scala enums). union_specifier is likewise a + # class-like container with the identical name/body fields (type_identifier + + # field_declaration_list), so a `union { ... }` becomes a type node with its + # data members instead of being dropped along with everything it declares. + class_types=frozenset({ + "class_specifier", "struct_specifier", "enum_specifier", "union_specifier", + }), function_types=frozenset({"function_definition"}), import_types=frozenset({"preproc_include"}), call_types=frozenset({"call_expression"}), diff --git a/tests/test_cpp_nested_and_cli.py b/tests/test_cpp_nested_and_cli.py index c42004ba75..300e5a5895 100644 --- a/tests/test_cpp_nested_and_cli.py +++ b/tests/test_cpp_nested_and_cli.py @@ -84,6 +84,45 @@ def test_cpp_enum_nested_in_class_and_namespace_is_extracted(tmp_path): assert ("Proto", "Udp") in case_of +def test_cpp_union_specifier_is_extracted(tmp_path): + """A named `union` is a class-like container whose type node and data members + must survive. `union_specifier` was missing from the C++ class_types, so a + `union { ... }` and everything it declared produced no nodes at all — the whole + type vanished. It shares struct_specifier's name/body fields (type_identifier + + field_declaration_list), so it must get a type node and its members, like a + struct. An anonymous union has no type name and is skipped, like an anonymous + enum. + """ + p = tmp_path / "value.hpp" + p.write_text( + "union Value { int i; float f; };\n" + "struct Point { int x; };\n" + "typedef union { int a; char b; } Anon;\n" + ) + result = extract_cpp(p) + assert result.get("parse_errors") is None + ids = {n["id"]: n["label"] for n in result["nodes"]} + labels = set(ids.values()) + # Pre-fix the whole `union Value { ... }` declaration vanished. + assert {"Value", "i", "f"} <= labels + contains = { + (ids.get(e["source"]), ids.get(e["target"])) + for e in result["edges"] + if e["relation"] == "contains" + } + defines = { + (ids.get(e["source"]), ids.get(e["target"])) + for e in result["edges"] + if e["relation"] == "defines" + } + assert ("value.hpp", "Value") in contains + assert ("Value", "i") in defines + assert ("Value", "f") in defines + # An anonymous union (`typedef union { ... } Anon`) has no type name, so it is + # skipped rather than emitting a nameless node, like an anonymous enum. + assert "Anon" not in labels + + def test_nested_cpp_class_is_extracted(tmp_path): # A nested type is a field_declaration whose `type` field IS the # class_specifier; the member-variable branch used to consume it and return