mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-25 08:11:54 +00:00
fix(cpp): extract namespace-scope using/typedef aliases as type nodes (#4580)
Applied from PR #4580 (authorship preserved); stale cache-schema bump dropped for one combined bump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
8cfda7f1ce
commit
3bab26798e
3 files changed
+267
-3
No files matched your search
@@ -4848,6 +4848,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:
|
||||
@@ -4858,7 +4922,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:
|
||||
|
||||
@@ -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):
|
||||
@@ -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):
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
"""C++ `using X = ...;` and `typedef ... X;` at namespace scope are declarations.
|
||||
|
||||
The C++ extractor emitted no node for an alias declaration or a typedef, so a
|
||||
field, parameter or return typed with one, even in the declaring header,
|
||||
resolved to a SOURCELESS stub that the corpus rewire can grow into a hub. A
|
||||
namespace- or file-scope alias is now a type node contained by its file; every
|
||||
declarator of a typedef counts (`typedef int Handle, *HandlePtr;`,
|
||||
`typedef int (*Fn)(int);`, `typedef int Arr[4];`), and an alias template
|
||||
(`template <class T> using Ptr = T*;`) too. An include guard or an
|
||||
`#if`/`#ifdef` block is a conditional wrapper, not a scope: an alias inside
|
||||
one at file scope is still file-scope.
|
||||
|
||||
Not declarations here: a member alias (`using value_type = T;` in a class: one
|
||||
in every container, and C++ class-member ids are not class-qualified, so they
|
||||
would fold into one hub); the struct a C-style `typedef struct Foo {...} Foo;`
|
||||
names (the struct's own node is it); and a same-name re-export
|
||||
(`using Bar = ns::Bar;`), which denotes the original type and declares no new
|
||||
name.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
importlib.util.find_spec("tree_sitter_cpp") is None,
|
||||
reason="tree_sitter_cpp not installed",
|
||||
)
|
||||
|
||||
_HEADER = textwrap.dedent("""\
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
namespace app {
|
||||
using IntVec = std::vector<int>;
|
||||
typedef int Handle, *HandlePtr;
|
||||
typedef struct { int x; } Point;
|
||||
typedef int (*Fn)(int);
|
||||
template <typename T> using Ptr = T*;
|
||||
using Callback = std::function<void(int)>;
|
||||
struct Widget {
|
||||
using value_type = int;
|
||||
typedef int size_type;
|
||||
IntVec items;
|
||||
Handle h;
|
||||
Point p;
|
||||
Fn f;
|
||||
Ptr<int> raw;
|
||||
Callback cb;
|
||||
IntVec get(Handle h, Callback cb);
|
||||
};
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
def _extract(tmp_path: Path, files: dict[str, str]) -> dict:
|
||||
for name, body in files.items():
|
||||
(tmp_path / name).parent.mkdir(parents=True, exist_ok=True)
|
||||
(tmp_path / name).write_text(body, encoding="utf-8")
|
||||
return extract([tmp_path / n for n in files], root=tmp_path,
|
||||
cache_root=tmp_path / "graphify-out", parallel=False)
|
||||
|
||||
|
||||
def _labels(result: dict) -> dict[str, str]:
|
||||
return {n["id"]: n["label"] for n in result["nodes"]}
|
||||
|
||||
|
||||
def _pairs(result: dict, relation: str) -> set[tuple[str, str]]:
|
||||
label = _labels(result)
|
||||
return {(label.get(e["source"], str(e["source"])), label.get(e["target"], str(e["target"])))
|
||||
for e in result["edges"] if e["relation"] == relation}
|
||||
|
||||
|
||||
def test_each_namespace_scope_alias_is_a_sourced_node_on_its_line(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"widget.hpp": _HEADER})
|
||||
expected = {"IntVec": "L4", "Handle": "L5", "HandlePtr": "L5", "Point": "L6",
|
||||
"Fn": "L7", "Ptr": "L8", "Callback": "L9"}
|
||||
for label, line in expected.items():
|
||||
matches = [n for n in result["nodes"] if n["label"] == label]
|
||||
assert len(matches) == 1, label
|
||||
assert matches[0]["source_file"], label
|
||||
assert matches[0]["source_location"] == line, label
|
||||
assert [n["label"] for n in result["nodes"] if not n.get("source_file")] == []
|
||||
|
||||
|
||||
def test_each_alias_is_contained_by_its_file(tmp_path: Path):
|
||||
contained = {tgt for _src, tgt in _pairs(_extract(tmp_path, {"widget.hpp": _HEADER}), "contains")}
|
||||
assert {"IntVec", "Handle", "HandlePtr", "Point", "Fn", "Ptr", "Callback"} <= contained
|
||||
|
||||
|
||||
def test_fields_reference_the_local_aliases(tmp_path: Path):
|
||||
refs = _pairs(_extract(tmp_path, {"widget.hpp": _HEADER}), "references")
|
||||
assert {("Widget", "IntVec"), ("Widget", "Handle"), ("Widget", "Point"), ("Widget", "Fn"),
|
||||
("Widget", "Ptr"), ("Widget", "Callback")} <= refs
|
||||
|
||||
|
||||
def test_member_aliases_are_not_file_level_nodes(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"widget.hpp": _HEADER})
|
||||
assert [n for n in result["nodes"] if n["label"] in ("value_type", "size_type")] == []
|
||||
|
||||
|
||||
def test_a_c_style_typedef_of_a_named_struct_is_the_struct(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"foo.hpp": "typedef struct Foo { int y; } Foo;\nstruct User { Foo f; };\n"})
|
||||
foos = [n for n in result["nodes"] if n["label"] == "Foo"]
|
||||
assert len(foos) == 1 and foos[0]["source_file"]
|
||||
assert ("User", "Foo") in _pairs(result, "references")
|
||||
|
||||
|
||||
def test_a_same_name_re_export_emits_no_node(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"api.hpp": "namespace ns { struct Bar {}; }\nusing Bar = ns::Bar;\n"})
|
||||
bars = [n for n in result["nodes"] if n["label"] == "Bar"]
|
||||
assert len(bars) == 1
|
||||
assert bars[0]["source_location"] == "L1"
|
||||
|
||||
|
||||
def test_a_reference_from_another_file_lands_on_the_alias(tmp_path: Path):
|
||||
result = _extract(tmp_path, {
|
||||
"types.hpp": "#include <vector>\nusing IntVec = std::vector<int>;\n",
|
||||
"user.hpp": '#include "types.hpp"\nstruct Bag { IntVec v; };\n',
|
||||
})
|
||||
[alias] = [n for n in result["nodes"] if n["label"] == "IntVec"]
|
||||
assert alias["source_file"].endswith("types.hpp")
|
||||
label = _labels(result)
|
||||
targets = {e["target"] for e in result["edges"]
|
||||
if e["relation"] == "references" and label.get(e["source"]) == "Bag"}
|
||||
assert targets == {alias["id"]}
|
||||
|
||||
|
||||
_GUARDED = textwrap.dedent("""\
|
||||
#ifndef CONFIG_HPP
|
||||
#define CONFIG_HPP
|
||||
using Guarded = int;
|
||||
typedef long GuardedT;
|
||||
#if defined(USE_WIDE)
|
||||
using Char = wchar_t;
|
||||
#elif defined(USE_U16)
|
||||
using Char16 = char16_t;
|
||||
#else
|
||||
using Narrow = char;
|
||||
#endif
|
||||
#ifdef HAVE_POOL
|
||||
template <typename T> using Pool = T*;
|
||||
#endif
|
||||
struct Config {
|
||||
#ifdef DEBUG
|
||||
using debug_type = int;
|
||||
#endif
|
||||
Guarded g;
|
||||
GuardedT t;
|
||||
Narrow n;
|
||||
};
|
||||
#endif
|
||||
""")
|
||||
|
||||
|
||||
def test_aliases_inside_include_guards_and_conditionals_are_nodes(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"config.hpp": _GUARDED})
|
||||
expected = {"Guarded": "L3", "GuardedT": "L4", "Char": "L6", "Char16": "L8",
|
||||
"Narrow": "L10", "Pool": "L13"}
|
||||
for label, line in expected.items():
|
||||
matches = [n for n in result["nodes"] if n["label"] == label]
|
||||
assert len(matches) == 1, label
|
||||
assert matches[0]["source_file"], label
|
||||
assert matches[0]["source_location"] == line, label
|
||||
assert {("Config", "Guarded"), ("Config", "GuardedT"), ("Config", "Narrow")} <= _pairs(result, "references")
|
||||
|
||||
|
||||
def test_a_member_alias_inside_a_conditional_is_still_a_member(tmp_path: Path):
|
||||
result = _extract(tmp_path, {"config.hpp": _GUARDED})
|
||||
assert [n for n in result["nodes"] if n["label"] == "debug_type"] == []
|
||||
Reference in new issue
Block a user