mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 15:40:05 +00:00
fix(cpp): extract enum / enum class specifiers and their enumerators
`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 9cde6c2a7f)
This commit is contained in:
1 parent
35207e9b4b
commit
5bb8a61140
3 files changed
+119
-6
No files matched your search
+6
-1
@@ -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"}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user