mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
fix(objc): extract C-style enum declarations
Objective-C is an object-oriented extractor that already emits @interface,
@implementation and @protocol type nodes, but plain `enum` and
`typedef enum` definitions were dropped entirely: both the enum type and all
of its enumerators were invisible in the graph.
Handle `enum_specifier` (and the `typedef enum { ... } Name;` form, where the
name is the typedef alias), emitting a type node plus a node and `case_of` edge
per enumerator — the same shape every other language with enums uses (Java
#1719, C++ #3939, Swift). A nameless enum and a bodyless `enum X y;` reference
are skipped so no phantom node is minted.
NS_ENUM/NS_OPTIONS is deliberately out of scope: it is a macro call that
tree-sitter-objc parses into an ERROR subtree, so there is no clean AST to
read. Documented in the test.
Add regression tests for the plain, typedef, and skip cases.
(cherry picked from commit 2d30c413d3)
This commit is contained in:
1 parent
e444d5ac37
commit
e4c48bf97e
2 files changed
+145
No files matched your search
@@ -226,6 +226,30 @@ def extract_objc(path: Path) -> dict:
|
||||
if entry is not None:
|
||||
_record_field_type(cls_nid, *entry)
|
||||
|
||||
def _emit_enum(enum_spec, name: str, line: int) -> None:
|
||||
"""Emit a C-style enum type node plus a node + `case_of` edge per
|
||||
enumerator, mirroring how every other language with enums models them
|
||||
(Java #1719, C++ #3939, Swift). Objective-C is an object-oriented
|
||||
extractor that already emits @interface/@protocol types, but plain
|
||||
`enum`/`typedef enum` definitions were dropped entirely.
|
||||
"""
|
||||
enum_nid = _make_id(stem, name)
|
||||
add_node(enum_nid, name, line)
|
||||
add_edge(file_nid, enum_nid, "contains", line)
|
||||
body = next((c for c in enum_spec.children if c.type == "enumerator_list"), None)
|
||||
if body is None:
|
||||
return
|
||||
for member in body.children:
|
||||
if member.type != "enumerator":
|
||||
continue
|
||||
mname = next((_read(c) for c in member.children if c.type == "identifier"), None)
|
||||
if not mname:
|
||||
continue
|
||||
m_line = member.start_point[0] + 1
|
||||
member_nid = _make_id(enum_nid, mname)
|
||||
add_node(member_nid, mname, m_line)
|
||||
add_edge(enum_nid, member_nid, "case_of", m_line)
|
||||
|
||||
def walk(node, parent_nid: str | None = None) -> None:
|
||||
t = node.type
|
||||
line = node.start_point[0] + 1
|
||||
@@ -408,6 +432,42 @@ def extract_objc(path: Path) -> dict:
|
||||
method_bodies.append((method_nid, node, container))
|
||||
return
|
||||
|
||||
if t == "type_definition":
|
||||
# `typedef enum { ... } Name;` / `typedef enum Tag { ... } Name;`.
|
||||
# The enum body sits on a child enum_specifier that carries no usable
|
||||
# name; the name callers reference is the typedef alias (the trailing
|
||||
# type_identifier). NS_ENUM/NS_OPTIONS never reach here — they are
|
||||
# macro_type_specifiers that tree-sitter parses into ERROR subtrees,
|
||||
# so they remain unsupported.
|
||||
enum_spec = next((c for c in node.children if c.type == "enum_specifier"), None)
|
||||
if enum_spec is not None and any(
|
||||
c.type == "enumerator_list" for c in enum_spec.children
|
||||
):
|
||||
alias = next(
|
||||
(_read(c) for c in reversed(node.children) if c.type == "type_identifier"),
|
||||
None,
|
||||
)
|
||||
if alias is None:
|
||||
alias = next(
|
||||
(_read(c) for c in enum_spec.children if c.type == "type_identifier"),
|
||||
None,
|
||||
)
|
||||
if alias:
|
||||
_emit_enum(enum_spec, alias, line)
|
||||
return
|
||||
# Not an enum typedef: fall through to the generic recursion.
|
||||
|
||||
if t == "enum_specifier":
|
||||
# A named C-style enum definition: `enum Color { Red, Green };`. Skip
|
||||
# a bare reference / forward declaration (no enumerator_list) and an
|
||||
# anonymous enum (no name): neither is a nameable definition.
|
||||
name = next(
|
||||
(_read(c) for c in node.children if c.type == "type_identifier"), None
|
||||
)
|
||||
if name and any(c.type == "enumerator_list" for c in node.children):
|
||||
_emit_enum(node, name, line)
|
||||
return
|
||||
|
||||
for child in node.children:
|
||||
walk(child, parent_nid)
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""C-style enum extraction for Objective-C.
|
||||
|
||||
Objective-C is an object-oriented extractor that already emits @interface,
|
||||
@implementation and @protocol type nodes, but plain `enum` and `typedef enum`
|
||||
definitions were dropped entirely — the type and all of its enumerators were
|
||||
invisible. Every other language with enums emits a type node plus a `case_of`
|
||||
edge per member (Java #1719, C++ #3939, Swift); ObjC now matches.
|
||||
|
||||
The NS_ENUM/NS_OPTIONS macro spelling is deliberately out of scope: it is a
|
||||
macro call that tree-sitter-objc parses into an ERROR subtree, so there is no
|
||||
clean AST to read.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _edge_labels(result: dict, relation: str) -> set[tuple[str, str]]:
|
||||
labels = {node["id"]: node["label"] for node in result["nodes"]}
|
||||
return {
|
||||
(labels.get(edge["source"], edge["source"]), labels.get(edge["target"], edge["target"]))
|
||||
for edge in result["edges"]
|
||||
if edge["relation"] == relation
|
||||
}
|
||||
|
||||
|
||||
def test_objc_plain_enum_emits_type_and_case_of_members(tmp_path):
|
||||
source = tmp_path / "Color.m"
|
||||
source.write_text(
|
||||
"enum Color { Red, Green, Blue };\n"
|
||||
"@interface Foo : NSObject\n@end\n"
|
||||
"@implementation Foo\n@end\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([source], cache_root=tmp_path)
|
||||
|
||||
labels = {n["label"] for n in result["nodes"]}
|
||||
assert "Color" in labels, "enum type dropped"
|
||||
case_of = _edge_labels(result, "case_of")
|
||||
assert ("Color", "Red") in case_of
|
||||
assert ("Color", "Green") in case_of
|
||||
assert ("Color", "Blue") in case_of
|
||||
# The enum does not disturb the surrounding class.
|
||||
assert "Foo" in labels
|
||||
|
||||
|
||||
def test_objc_typedef_enum_uses_the_typedef_alias(tmp_path):
|
||||
# `.m` is shared with MATLAB, so give the file an unambiguous ObjC marker so
|
||||
# detection routes it to the ObjC extractor.
|
||||
source = tmp_path / "Dir.m"
|
||||
source.write_text(
|
||||
"#import <Foundation/Foundation.h>\n"
|
||||
"typedef enum { North, South } Direction;\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([source], cache_root=tmp_path)
|
||||
|
||||
# The anonymous enum is named after its typedef alias, the name callers use.
|
||||
assert "Direction" in {n["label"] for n in result["nodes"]}
|
||||
case_of = _edge_labels(result, "case_of")
|
||||
assert ("Direction", "North") in case_of
|
||||
assert ("Direction", "South") in case_of
|
||||
|
||||
|
||||
def test_objc_anonymous_and_bodyless_enums_mint_no_phantom(tmp_path):
|
||||
# A nameless enum has nothing to name a node after; a bare `enum X y;` use is
|
||||
# a reference, not a definition. Neither should fabricate a node — while the
|
||||
# surrounding class is still extracted.
|
||||
source = tmp_path / "None.m"
|
||||
source.write_text(
|
||||
"#import <Foundation/Foundation.h>\n"
|
||||
"enum { Loose1, Loose2 };\n"
|
||||
"@interface Keep : NSObject\n@end\n"
|
||||
"@implementation Keep\n@end\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([source], cache_root=tmp_path)
|
||||
|
||||
labels = {n["label"] for n in result["nodes"]}
|
||||
assert "Keep" in labels, "surrounding class should still be extracted"
|
||||
assert labels.isdisjoint({"Loose1", "Loose2"})
|
||||
assert not _edge_labels(result, "case_of")
|
||||
Reference in new issue
Block a user