fix(cpp): extract namespace-scope `using` and `typedef` aliases as type nodes by madhusudanpatnaik · Pull Request #4580 · Graphify-Labs/graphify · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion graphify/cache.py
79 changes: 79 additions & 0 deletions graphify/extractors/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -4780,6 +4780,70 @@ def _scala_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: s
return True
return False

def _cpp_declarator_type_name(declarator, source: bytes) -> str | None:
"""The name a typedef declarator introduces: `Handle`, `*HandlePtr`,
`(*Fn)(int)`, `Arr[4]` each wrap one `type_identifier`."""
node = declarator
while node is not None and node.type != "type_identifier":
inner = node.child_by_field_name("declarator")
if inner is None and node.type == "parenthesized_declarator":
inner = next(iter(node.named_children), None)
node = inner
return _read_text(node, source) if node is not None else None


def _cpp_type_last_name(type_node, source: bytes) -> str | None:
"""`Bar` for `ns::Bar` or `ns::Bar<T>`: the name a qualified type ends in."""
if type_node is not None and type_node.type == "type_descriptor":
type_node = type_node.child_by_field_name("type")
if type_node is None or type_node.type != "qualified_identifier":
return None
while type_node is not None and type_node.type == "qualified_identifier":
type_node = type_node.child_by_field_name("name")
if type_node is not None and type_node.type == "template_type":
type_node = type_node.child_by_field_name("name")
return _read_text(type_node, source) if type_node is not None else None


def _cpp_alias_names(node, source: bytes) -> list[str]:
"""The type names a namespace- or file-scope `using X = ...;` or `typedef`
declares. A member alias (`using value_type = T;` in a class) is not one: it
sits in every container and C++ member ids are not class-qualified, so they
would fold into one hub. Neither is the struct a C-style
`typedef struct Foo {...} Foo;` names (the struct's own node is it), nor a
same-name re-export (`using Bar = ns::Bar;`), which denotes the original
type and declares no new name. An include guard or `#if`/`#ifdef` block is
a conditional wrapper, not a scope, so an alias inside one keeps the scope
around it."""
scope = node.parent
while scope is not None and (
scope.type == "template_declaration" or scope.type.startswith("preproc_")):
scope = scope.parent
if scope is None or scope.type not in ("translation_unit", "declaration_list"):
return []
target = node.child_by_field_name("type")
if node.type == "alias_declaration":
name_node = node.child_by_field_name("name")
candidates = [_read_text(name_node, source)] if name_node is not None else []
else:
candidates = [_cpp_declarator_type_name(d, source)
for d in node.children_by_field_name("declarator")]
own_names = {_cpp_type_last_name(target, source)}
if target is not None and target.type in (
"struct_specifier", "class_specifier", "union_specifier", "enum_specifier"):
spec_name = target.child_by_field_name("name")
if spec_name is not None:
own_names.add(_read_text(spec_name, source))
# A set, not `in names`: one typedef can declare thousands of names.
names: list[str] = []
seen = set(own_names)
for name in candidates:
if name and name not in seen:
seen.add(name)
names.append(name)
return names


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:
Expand All @@ -4790,7 +4854,22 @@ def _cpp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str
`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.

A namespace- or file-scope alias (`using X = ...;`, `typedef ... X;`) also
gets a type node contained by the file, so a type reference to it lands on
a sourced declaration instead of a sourceless stub. Returns False for it:
the walk goes on, so a typedef'd struct body still gets its own node.
C++ never populates namespace_stack, so the id is the one
ensure_named_node looks up for a name declared in this file.
"""
if node.type in ("alias_declaration", "type_definition"):
line = node.start_point[0] + 1
for alias_name in _cpp_alias_names(node, source):
alias_nid = _make_id(stem, alias_name)
if alias_nid not in seen_ids:
add_node_fn(alias_nid, alias_name, line)
add_edge_fn(file_nid, alias_nid, "contains", line)
return False
if node.type == "enumerator" and parent_class_nid:
name_node = node.child_by_field_name("name")
if name_node is None:
Expand Down
17 changes: 14 additions & 3 deletions tests/test_cpp_nested_and_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,15 @@ def test_cpp_enum_specifier_members_are_extracted(tmp_path):
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.
# The typedef name `Flag` is a declared type of its own (an alias node, with
# no members of its own) since namespace-scope typedefs are extracted.
assert "A" not in labels
assert "Flag" not in labels
assert "" not in labels
assert ("colors.hpp", "Flag") in {
(ids.get(e["source"]), ids.get(e["target"]))
for e in result["edges"] if e["relation"] == "contains"
}
assert not any(src == "Flag" for src, _ in case_of)


def test_cpp_enum_nested_in_class_and_namespace_is_extracted(tmp_path):
Expand Down Expand Up @@ -119,8 +126,12 @@ def test_cpp_union_specifier_is_extracted(tmp_path):
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
# skipped rather than emitting a nameless node, like an anonymous enum. The
# typedef name `Anon` is an alias node of its own, owning none of the members.
assert "" not in labels
assert ("value.hpp", "Anon") in contains
assert not any(src == "Anon" for src, _ in defines)
assert not {"a", "b"} & labels


def test_cpp_union_member_function_is_a_method(tmp_path):
Expand Down
174 changes: 174 additions & 0 deletions tests/test_cpp_type_alias.py
Loading