From 5bb8a6114082dcfefc81311326dfc169a03e0451 Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Wed, 30 Sep 2026 09:29:04 +0530 Subject: [PATCH] fix(cpp): extract enum / enum class specifiers and their enumerators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `enum_specifier` was missing from the C++ class_types, so an `enum` or `enum class` produced no nodes at all: the enum type and every enumerator vanished from the graph, even though struct/class specifiers right next to them were extracted normally. Add `enum_specifier` to the C++ class_types — its name and body sit on the same `name`/`body` fields as struct_specifier (type_identifier + enumerator_list) so the type gets a node and its body is walked — and add `_cpp_extra_walk` to emit a node plus a `case_of` edge per `enumerator`. Anonymous enums (`typedef enum { A, B } Flag`) carry no type name and are skipped by the existing name guard rather than emitting a nameless node. This is the C++ parity of Java's enum_constant (#1719), Swift, and Scala. (cherry picked from commit 9cde6c2a7f88acd06a9832daf143274d5fe172a3) --- graphify/extract.py | 7 +++- graphify/extractors/engine.py | 46 +++++++++++++++++--- tests/test_cpp_nested_and_cli.py | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index dc5300f73..1193ff9b7 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1349,7 +1349,12 @@ _C_CONFIG = LanguageConfig( _CPP_CONFIG = LanguageConfig( ts_module="tree_sitter_cpp", - class_types=frozenset({"class_specifier", "struct_specifier"}), + # enum_specifier is a class-like container: an `enum` / `enum class` owns a + # 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"}), function_types=frozenset({"function_definition"}), import_types=frozenset({"preproc_include"}), call_types=frozenset({"call_expression"}), diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 3dca9cd87..d5f48054a 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -3010,6 +3010,35 @@ def _scala_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s return True return False +def _cpp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, + nodes: list, edges: list, seen_ids: set, function_bodies: list, + parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool: + """Emit a node per C++ enumerator with a case_of edge. Returns True if handled. + + An `enum` / `enum class` is now a class-like container, so its body + (`enumerator_list`) is walked with the enum as parent. Each member is an + `enumerator` node (`Red`, `Green = 2`) whose name is its `name` field + (an `identifier`). Without this the enum type is a memberless leaf. This is + the C++ parity of Java #1719 (enum_constant), Swift, and Scala. + """ + if node.type == "enumerator" and parent_class_nid: + name_node = node.child_by_field_name("name") + if name_node is None: + name_node = next( + (c for c in node.children if c.type == "identifier"), None + ) + if name_node is None: + return True + member_name = _read_text(name_node, source) + if member_name: + line = node.start_point[0] + 1 + member_nid = _make_id(parent_class_nid, member_name) + if member_nid not in seen_ids: + add_node_fn(member_nid, member_name, line) + add_edge_fn(parent_class_nid, member_nid, "case_of", line) + return True + return False + def _java_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str, nodes: list, edges: list, seen_ids: set, function_bodies: list, parent_class_nid: str | None, add_node_fn, add_edge_fn, @@ -5075,11 +5104,11 @@ def _extract_generic( # it a `contains` edge from the enclosing type. The declarator loop # below still runs, since `class Inner { } inst;` declares a member # alongside the type. - # Only class/struct nested types are recovered here: `enum_specifier` - # is deliberately not in C++'s `class_types`, so a nested `enum` and - # its enumerators are still not emitted. That is outside #2876's scope - # (which is about nested class/struct and C++/CLI) and is left as a - # known gap rather than widened here. + # `enum_specifier` is now in C++'s `class_types` (#3939), so a nested + # `enum` / `enum class` inside a class or namespace body is recovered + # the same way: it gets a `contains` edge from the enclosing type and + # its enumerators get `case_of` edges via `_cpp_extra_walk`. Nested + # class/struct types (#2876) are recovered through the same branch. is_nested_type = ( type_node is not None and type_node.type in config.class_types @@ -5762,6 +5791,13 @@ def _extract_generic( parent_class_nid, add_node, add_edge, walk): return + # C++ enumerators (`Red`, `Green = 2`) inside an enum / enum class body. + if config.ts_module == "tree_sitter_cpp": + if _cpp_extra_walk(node, source, file_nid, stem, str_path, + nodes, edges, seen_ids, function_bodies, + parent_class_nid, add_node, add_edge): + return + if config.ts_module == "tree_sitter_ruby": if _ruby_extra_walk(node, source, file_nid, stem, str_path, nodes, edges, seen_ids, function_bodies, diff --git a/tests/test_cpp_nested_and_cli.py b/tests/test_cpp_nested_and_cli.py index 6b7d4647f..c42004ba7 100644 --- a/tests/test_cpp_nested_and_cli.py +++ b/tests/test_cpp_nested_and_cli.py @@ -12,6 +12,78 @@ def _labels(path: Path) -> list[str]: return [n["label"] for n in extract_cpp(path)["nodes"]] +def test_cpp_enum_specifier_members_are_extracted(tmp_path): + """An `enum` / `enum class` is a class-like container whose members must be + nodes. `enum_specifier` was missing from the C++ class_types, so the enum + type and all of its enumerators produced no nodes at all — the whole type + vanished. Each enumerator must become a node with a `case_of` edge (parity + with Java #1719 / Swift / Scala enums). + """ + p = tmp_path / "colors.hpp" + p.write_text( + "enum class Color { Red, Green = 2, Blue };\n" + "enum Old { X, Y };\n" + "typedef enum { A, B } Flag;\n" + ) + result = extract_cpp(p) + assert result.get("parse_errors") is None + labels = {n["label"] for n in result["nodes"]} + assert {"Color", "Red", "Green", "Blue", "Old", "X", "Y"} <= labels + ids = {n["id"]: n["label"] for n in result["nodes"]} + case_of = { + (ids.get(e["source"]), ids.get(e["target"])) + for e in result["edges"] + if e["relation"] == "case_of" + } + assert ("Color", "Red") in case_of + assert ("Color", "Green") in case_of + assert ("Color", "Blue") in case_of + assert ("Old", "X") in case_of + assert ("Old", "Y") in case_of + # An anonymous enum (`typedef enum { A, B } Flag`) has no type name, so it is + # skipped rather than emitting a nameless node; its members do not appear. + assert "A" not in labels + assert "Flag" not in labels + + +def test_cpp_enum_nested_in_class_and_namespace_is_extracted(tmp_path): + """Putting `enum_specifier` in class_types also recovers a nested enum inside + a class or namespace body: the enum is `contains`-ed by its enclosing type + and its enumerators get `case_of` edges (the branch that used to skip them).""" + p = tmp_path / "nested_enum.hpp" + p.write_text( + "class Widget {\n" + "public:\n" + " enum class State { Idle, Active };\n" + "};\n" + "namespace net {\n" + " enum Proto { Tcp, Udp };\n" + "}\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()) + assert {"Widget", "State", "Idle", "Active", "Proto", "Tcp", "Udp"} <= labels + contains = { + (ids.get(e["source"]), ids.get(e["target"])) + for e in result["edges"] + if e["relation"] == "contains" + } + case_of = { + (ids.get(e["source"]), ids.get(e["target"])) + for e in result["edges"] + if e["relation"] == "case_of" + } + # the class-nested enum is contained by its class, members hang off the enum + assert ("Widget", "State") in contains + assert ("State", "Idle") in case_of + assert ("State", "Active") in case_of + # the namespace-nested enum's members resolve too + assert ("Proto", "Tcp") in case_of + assert ("Proto", "Udp") in case_of + + 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