From 7c09b4d3609ffd83ed1d6e2ed2246a6eaeac968b Mon Sep 17 00:00:00 2001 From: Alex Tumanov <6143578+oleksii-tumanov@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:08:22 -0500 Subject: [PATCH] fix: reject incomplete Objective-C enum macro parses Validate both complete argument spans and reject unsupported body children instead of emitting partial enum graphs. AI-Assistance: OpenAI Codex --- graphify/extractors/objc.py | 18 ++++++++++++++++++ tests/test_objc_enum_declarations.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/graphify/extractors/objc.py b/graphify/extractors/objc.py index 5e1d6c97d..14a0c02ec 100644 --- a/graphify/extractors/objc.py +++ b/graphify/extractors/objc.py @@ -322,6 +322,19 @@ def extract_objc(path: Path) -> dict: "identifier", "type_identifier" ): continue + # Valid headers also have grammar ERROR wrappers. Check complete + # argument spans rather than ignoring extra tokens inside them. + underlying_type = outer_types[0] + if any(token.is_missing for token in tokens) or any( + opening.end_byte <= token.start_byte < comma.start_byte + and not (underlying_type.start_byte <= token.start_byte + and token.end_byte <= underlying_type.end_byte) + or comma.end_byte <= token.start_byte < closing.start_byte + and not (names[0].start_byte <= token.start_byte + and token.end_byte <= names[0].end_byte) + for token in tokens + ): + continue following = [token for token in source_leaves if token.start_byte >= macro.end_byte] @@ -363,6 +376,11 @@ def extract_objc(path: Path) -> dict: or body.has_error or body.start_byte != len(prefix) or body.end_byte != len(prefix) + len(body_bytes)): continue + # Conditional members need preprocessor handling. Do not emit a + # partial enum when a clean body contains unsupported child nodes. + if any(child.type not in {"{", "}", ",", "comment", "enumerator"} + for child in body.children): + continue members: list[tuple[str, int]] = [] previous_member_end = None diff --git a/tests/test_objc_enum_declarations.py b/tests/test_objc_enum_declarations.py index ba827a06e..ae75ea034 100644 --- a/tests/test_objc_enum_declarations.py +++ b/tests/test_objc_enum_declarations.py @@ -210,3 +210,25 @@ def test_objc_macro_enums_preserve_siblings_and_unrelated_parser_errors(tmp_path ("Direction", "North"), ("Direction", "South"), ("Kind", "First"), ("Kind", "Second"), ("Mask", "Enabled"), } + + +@pytest.mark.parametrize( + "declaration", + [ + "typedef NS_ENUM(NSInteger = 1, Broken) { Bad };", + "typedef NS_ENUM(NSInteger, *Broken) { Bad };", + "typedef NS_ENUM(NSInteger, Broken[2]) { Bad };", + "typedef NS_OPTIONS(NSUInteger, Broken) {\n" + "#ifdef FEATURE\n Bad = 1 << 0,\n#endif\n Other = 1 << 1,\n};", + ], +) +def test_objc_macro_enum_unsupported_parts_do_not_emit_partial_enums(tmp_path, declaration): + source = tmp_path / "Partial.h" + source.write_text( + "#import \n" + declaration + "\n" + "typedef NS_ENUM(NSInteger, Good) { Kept };\n", + encoding="utf-8", + ) + result = extract([source], cache_root=tmp_path) + assert {n["label"] for n in result["nodes"]}.isdisjoint({"Broken", "Bad", "Other"}) + assert _edge_labels(result, "case_of") == {("Good", "Kept")}