From 88d2b610faedde0be5af3cc0fcdd78c247f2e746 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:07:11 +0000 Subject: [PATCH 01/25] Recognize C static assertions --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 1 + tests/test_trivial_c_static_assert.py | 72 +++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 tests/test_trivial_c_static_assert.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index 0fb579e..edf95ec 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -138,6 +138,7 @@ class PlyLexer: "sizeof", "static", "static_assert", + "_Static_assert", "static_cast", "struct", "switch", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 93a9446..816ed4a 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -370,6 +370,7 @@ def parse(self) -> None: "protected": self._process_access_specifier, "public": self._process_access_specifier, "static_assert": self._consume_static_assert, + "_Static_assert": self._consume_static_assert, "template": self._parse_template, "typedef": self._parse_typedef, "using": self._parse_using, diff --git a/tests/test_trivial_c_static_assert.py b/tests/test_trivial_c_static_assert.py new file mode 100644 index 0000000..e1304ff --- /dev/null +++ b/tests/test_trivial_c_static_assert.py @@ -0,0 +1,72 @@ +from cxxheaderparser.simple import ( + ClassDecl, + ClassScope, + Field, + NamespaceScope, + ParsedData, + Variable, + parse_string, +) +from cxxheaderparser.types import FundamentalSpecifier, NameSpecifier, PQName, Type +from cxxheaderparser.tokfmt import PlyLexer + + +def test_c_static_assert_namespace() -> None: + content = """ + _Static_assert(1, "ok"); int x; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ] + ) + ) + + +def test_c_static_assert_class() -> None: + content = """ + struct S { _Static_assert(1, "ok"); int x; }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + fields=[ + Field( + access="public", + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="x", + ) + ], + ) + ] + ) + ) + + +def test_c_static_assert_raw_keyword(): + lexer = PlyLexer() + lexer.input("_Static_assert") + token = lexer.token() + assert (token.type, token.value) == ("_Static_assert", "_Static_assert") From 69337083431fe567b21b7337cd95deab3c2d11d1 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:07:32 +0000 Subject: [PATCH 02/25] Recognize GNU signed fundamental specifiers --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 1 + tests/test_trivial_gnu_signed.py | 97 ++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/test_trivial_gnu_signed.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index edf95ec..bf4ff21 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -135,6 +135,7 @@ class PlyLexer: "return", "short", "signed", + "__signed__", "sizeof", "static", "static_assert", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 816ed4a..b0121bf 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1729,6 +1729,7 @@ def _parse_pqname_decltype_specifier(self) -> DecltypeSpecifier: _compound_fundamentals = { "unsigned", "signed", + "__signed__", "short", "int", "long", diff --git a/tests/test_trivial_gnu_signed.py b/tests/test_trivial_gnu_signed.py new file mode 100644 index 0000000..f1590a1 --- /dev/null +++ b/tests/test_trivial_gnu_signed.py @@ -0,0 +1,97 @@ +from cxxheaderparser.simple import NamespaceScope, ParsedData, Variable, parse_string +from cxxheaderparser.types import FundamentalSpecifier, NameSpecifier, PQName, Type +from cxxheaderparser.tokfmt import PlyLexer + + +def test_gnu_signed_spelling_1() -> None: + content = """ + __signed__ x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="__signed__")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_gnu_signed_spelling_2() -> None: + content = """ + __signed__ char x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="__signed__ char")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_gnu_signed_spelling_3() -> None: + content = """ + long __signed__ int x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="long __signed__ int")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_gnu_signed_raw_keyword(): + lexer = PlyLexer() + lexer.input("__signed__") + token = lexer.token() + assert (token.type, token.value) == ("__signed__", "__signed__") From 0438a12240bae0ca134a402af5a1d55580b013fd Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:07:51 +0000 Subject: [PATCH 03/25] Discard GNU extension prefixes without losing doxygen --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 9 ++- tests/test_trivial_gnu_extension.py | 99 +++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 tests/test_trivial_gnu_extension.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index bf4ff21..cfa4378 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -71,6 +71,7 @@ class PlyLexer: keywords = { "__attribute__", + "__extension__", "alignas", "alignof", "asm", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index b0121bf..021ec0b 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -359,6 +359,7 @@ def parse(self) -> None: _translation_unit_tokens: typing.Dict[ str, typing.Callable[[LexToken, typing.Optional[str]], typing.Any] ] = { + "__extension__": lambda _1, _2: None, "__attribute__": self._consume_gcc_attribute, "__declspec": self._consume_declspec, "alignas": self._consume_attribute_specifier_seq, @@ -382,7 +383,13 @@ def parse(self) -> None: ";": lambda _1, _2: None, } - _keep_doxygen = {"__declspec", "alignas", "__attribute__", "DBL_LBRACKET"} + _keep_doxygen = { + "__declspec", + "alignas", + "__attribute__", + "DBL_LBRACKET", + "__extension__", + } tok = None diff --git a/tests/test_trivial_gnu_extension.py b/tests/test_trivial_gnu_extension.py new file mode 100644 index 0000000..b6fa3fb --- /dev/null +++ b/tests/test_trivial_gnu_extension.py @@ -0,0 +1,99 @@ +from cxxheaderparser.simple import ( + ClassDecl, + ClassScope, + Field, + NamespaceScope, + ParsedData, + Typedef, + Variable, + parse_string, +) +from cxxheaderparser.types import FundamentalSpecifier, NameSpecifier, PQName, Type +from cxxheaderparser.tokfmt import PlyLexer + + +def test_gnu_extension_preserves_doxygen_typedef() -> None: + content = """ + /// Documentation + __extension__ typedef int T; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + typedefs=[ + Typedef( + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name="T", + ) + ] + ) + ) + + +def test_gnu_extension_preserves_doxygen_variable() -> None: + content = """ + /// Documentation + __extension__ int x; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + doxygen="/// Documentation", + ) + ] + ) + ) + + +def test_gnu_extension_preserves_doxygen_class() -> None: + content = """ + /// Documentation + __extension__ struct S { int x; }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ), + doxygen="/// Documentation", + ), + fields=[ + Field( + access="public", + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="x", + ) + ], + ) + ] + ) + ) + + +def test_gnu_extension_raw_keyword(): + lexer = PlyLexer() + lexer.input("__extension__") + token = lexer.token() + assert (token.type, token.value) == ("__extension__", "__extension__") From 97fc5c5538e8ef9e5bddcc19e57ad3451131019d Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:08:11 +0000 Subject: [PATCH 04/25] Interpret GNU inline spelling in existing inline paths --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 4 +- tests/test_trivial_gnu_inline.py | 131 +++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tests/test_trivial_gnu_inline.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index cfa4378..ef4c95f 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -111,6 +111,7 @@ class PlyLexer: "goto", "if", "__inline", + "__inline__", "inline", "int", "__int8", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 021ec0b..f50242f 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -366,6 +366,7 @@ def parse(self) -> None: "extern": self._parse_extern, "friend": self._parse_friend_decl, "inline": self._parse_inline, + "__inline__": self._parse_inline, "namespace": self._parse_namespace, "private": self._process_access_specifier, "protected": self._process_access_specifier, @@ -2800,6 +2801,7 @@ def _parse_cv_ptr_or_fn( "volatile", "mutable", "__inline", + "__inline__", "__forceinline", "this", "ELLIPSIS", @@ -2872,7 +2874,7 @@ def _parse_type( if linkage_tok: mods.extern_linkage = linkage_tok.value mods.extern = tok - elif tok_type in ("__inline", "__forceinline", "inline"): + elif tok_type in ("__inline", "__inline__", "__forceinline", "inline"): mods.inline = tok elif tok_type == "static": mods.static = tok diff --git a/tests/test_trivial_gnu_inline.py b/tests/test_trivial_gnu_inline.py new file mode 100644 index 0000000..a925f2f --- /dev/null +++ b/tests/test_trivial_gnu_inline.py @@ -0,0 +1,131 @@ +from cxxheaderparser.simple import ( + ClassDecl, + ClassScope, + Function, + Method, + NamespaceScope, + ParsedData, + Variable, + parse_string, +) +from cxxheaderparser.types import FundamentalSpecifier, NameSpecifier, PQName, Type +from cxxheaderparser.tokfmt import PlyLexer + + +def test_gnu_inline_definition() -> None: + content = """ + __inline__ void f() {} + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + inline=True, + has_body=True, + ) + ] + ) + ) + + +def test_gnu_inline_static() -> None: + content = """ + static __inline__ int f(); + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + static=True, + inline=True, + ) + ] + ) + ) + + +def test_gnu_inline_method() -> None: + content = """ + struct S { __inline__ void f(); }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + inline=True, + access="public", + ) + ], + ) + ] + ) + ) + + +def test_gnu_inline_namespace() -> None: + content = """ + __inline__ namespace N { int x; } + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + namespaces={ + "N": NamespaceScope( + name="N", + inline=True, + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + ) + ], + ) + } + ) + ) + + +def test_gnu_inline_raw_keyword(): + lexer = PlyLexer() + lexer.input("__inline__") + token = lexer.token() + assert (token.type, token.value) == ("__inline__", "__inline__") From c77aed97c4c987474e70ebe62e77d06cde32c4da Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:08:25 +0000 Subject: [PATCH 05/25] Classify char8_t as a fundamental type --- cxxheaderparser/parser.py | 1 + tests/test_trivial_char8_t.py | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/test_trivial_char8_t.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index f50242f..6ad2770 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1752,6 +1752,7 @@ def _parse_pqname_decltype_specifier(self) -> DecltypeSpecifier: _fundamentals = _compound_fundamentals | { "bool", + "char8_t", "char16_t", "char32_t", "nullptr_t", diff --git a/tests/test_trivial_char8_t.py b/tests/test_trivial_char8_t.py new file mode 100644 index 0000000..6a601bc --- /dev/null +++ b/tests/test_trivial_char8_t.py @@ -0,0 +1,52 @@ +from cxxheaderparser.simple import ( + Function, + NamespaceScope, + ParsedData, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + FundamentalSpecifier, + NameSpecifier, + PQName, + Parameter, + Type, +) + + +def test_char8_t_is_fundamental() -> None: + content = """ + char8_t x; char8_t f(char8_t); + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="char8_t")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[ + Parameter( + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="char8_t")] + ) + ) + ) + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="char8_t")]) + ), + ) + ], + ) + ) From c782f91f961770fbf6b0d8d1cda3d0a14ca0cb4d Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:08:50 +0000 Subject: [PATCH 06/25] Interpret GNU volatile spelling at every qualifier layer --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 20 ++- tests/test_trivial_gnu_volatile.py | 187 +++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 tests/test_trivial_gnu_volatile.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index ef4c95f..645097e 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -160,6 +160,7 @@ class PlyLexer: "virtual", "void", "volatile", + "__volatile__", "wchar_t", "while", } diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 6ad2770..5958f29 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2275,7 +2275,9 @@ def _parse_method_end(self, method: Method) -> None: raise self._parse_error(tok, "0/delete/default") break - elif tok_value in ("const", "volatile", "override", "final"): + elif tok.type in ("volatile", "__volatile__"): + method.volatile = True + elif tok_value in ("const", "override", "final"): setattr(method, tok_value, True) elif tok_value in ("&", "&&"): method.ref_qualifier = tok_value @@ -2509,7 +2511,7 @@ def _parse_function_type_qualifiers(self) -> _FunctionTypeQualifiers: const = False volatile = False while True: - tok = self.lex.token_if("const", "volatile") + tok = self.lex.token_if("const", "volatile", "__volatile__") if not tok: break if tok.type == "const": @@ -2671,7 +2673,14 @@ def _parse_cv_ptr_or_fn( continue tok = self.lex.token_if( - "*", "const", "volatile", "__restrict__", "__restrict", "restrict", "(" + "*", + "const", + "volatile", + "__volatile__", + "__restrict__", + "__restrict", + "restrict", + "(", ) if not tok: break @@ -2686,7 +2695,7 @@ def _parse_cv_ptr_or_fn( if not isinstance(dtype, (Pointer, MemberPointer, Type)): raise self._parse_error(tok) dtype.const = True - elif tok.type == "volatile": + elif tok.type in ("volatile", "__volatile__"): if not isinstance(dtype, (Pointer, MemberPointer, Type)): raise self._parse_error(tok) dtype.volatile = True @@ -2800,6 +2809,7 @@ def _parse_cv_ptr_or_fn( | _type_kwd_meth | { "volatile", + "__volatile__", "mutable", "__inline", "__inline__", @@ -2891,7 +2901,7 @@ def _parse_type( mods.virtual = tok elif tok_type == "mutable": mods.mutable = tok - elif tok_type == "volatile": + elif tok_type in ("volatile", "__volatile__"): volatile = True elif tok_type in _attribute_start: self._consume_attribute(tok) diff --git a/tests/test_trivial_gnu_volatile.py b/tests/test_trivial_gnu_volatile.py new file mode 100644 index 0000000..a4591ab --- /dev/null +++ b/tests/test_trivial_gnu_volatile.py @@ -0,0 +1,187 @@ +from cxxheaderparser.simple import ( + ClassDecl, + ClassScope, + Method, + NamespaceScope, + ParsedData, + Typedef, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + FunctionType, + FundamentalSpecifier, + MemberPointer, + NameSpecifier, + PQName, + Pointer, + Type, +) +from cxxheaderparser.tokfmt import PlyLexer + + +def test_gnu_volatile_prefix() -> None: + content = """ + __volatile__ int x; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]), + volatile=True, + ), + ) + ] + ) + ) + + +def test_gnu_volatile_suffix() -> None: + content = """ + int __volatile__ x; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]), + volatile=True, + ), + ) + ] + ) + ) + + +def test_gnu_volatile_pointer() -> None: + content = """ + int * __volatile__ p; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="p")]), + type=Pointer( + ptr_to=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + volatile=True, + ), + ) + ] + ) + ) + + +def test_gnu_volatile_method() -> None: + content = """ + struct S { void f() __volatile__; }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + access="public", + volatile=True, + ) + ], + ) + ] + ) + ) + + +def test_gnu_volatile_function_typedef() -> None: + content = """ + typedef void F() __volatile__; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + typedefs=[ + Typedef( + type=FunctionType( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + parameters=[], + volatile=True, + ), + name="F", + ) + ] + ) + ) + + +def test_gnu_volatile_member_pointer_typedef() -> None: + content = """ + typedef void (S::*F)() __volatile__; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + typedefs=[ + Typedef( + type=MemberPointer( + ptr_to=FunctionType( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + parameters=[], + volatile=True, + ), + classname=PQName(segments=[NameSpecifier(name="S")]), + ), + name="F", + ) + ] + ) + ) + + +def test_gnu_volatile_raw_keyword(): + lexer = PlyLexer() + lexer.input("__volatile__") + token = lexer.token() + assert (token.type, token.value) == ("__volatile__", "__volatile__") From 6d8e4056727ee9fedeee66e25a93284ca7e6677a Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:09:09 +0000 Subject: [PATCH 07/25] Recognize C complex fundamental specifiers --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 1 + tests/test_trivial_c_complex.py | 97 +++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/test_trivial_c_complex.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index 645097e..11075e6 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -96,6 +96,7 @@ class PlyLexer: "delete", "do", "double", + "_Complex", "dynamic_cast", "else", "enum", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 5958f29..609c620 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1743,6 +1743,7 @@ def _parse_pqname_decltype_specifier(self) -> DecltypeSpecifier: "long", "float", "double", + "_Complex", "char", "__int8", "__int16", diff --git a/tests/test_trivial_c_complex.py b/tests/test_trivial_c_complex.py new file mode 100644 index 0000000..077373a --- /dev/null +++ b/tests/test_trivial_c_complex.py @@ -0,0 +1,97 @@ +from cxxheaderparser.simple import NamespaceScope, ParsedData, Variable, parse_string +from cxxheaderparser.types import FundamentalSpecifier, NameSpecifier, PQName, Type +from cxxheaderparser.tokfmt import PlyLexer + + +def test_complex_spelling_1() -> None: + content = """ + float _Complex x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="float _Complex")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_complex_spelling_2() -> None: + content = """ + _Complex double x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="_Complex double")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_complex_spelling_3() -> None: + content = """ + long double _Complex x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="long double _Complex")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_c_complex_raw_keyword(): + lexer = PlyLexer() + lexer.input("_Complex") + token = lexer.token() + assert (token.type, token.value) == ("_Complex", "_Complex") From df80f604342b1e4524e733650ece229b0f92e0fa Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:09:28 +0000 Subject: [PATCH 08/25] Recognize GNU 128-bit fundamental specifiers --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 1 + tests/test_trivial_gnu_int128.py | 97 ++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/test_trivial_gnu_int128.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index 11075e6..7bc276c 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -119,6 +119,7 @@ class PlyLexer: "__int16", "__int32", "__int64", + "__int128", "long", "mutable", "namespace", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 609c620..a082989 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1749,6 +1749,7 @@ def _parse_pqname_decltype_specifier(self) -> DecltypeSpecifier: "__int16", "__int32", "__int64", + "__int128", } _fundamentals = _compound_fundamentals | { diff --git a/tests/test_trivial_gnu_int128.py b/tests/test_trivial_gnu_int128.py new file mode 100644 index 0000000..2525067 --- /dev/null +++ b/tests/test_trivial_gnu_int128.py @@ -0,0 +1,97 @@ +from cxxheaderparser.simple import NamespaceScope, ParsedData, Variable, parse_string +from cxxheaderparser.types import FundamentalSpecifier, NameSpecifier, PQName, Type +from cxxheaderparser.tokfmt import PlyLexer + + +def test_int128_spelling_1() -> None: + content = """ + __int128 x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="__int128")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_int128_spelling_2() -> None: + content = """ + unsigned __int128 x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="unsigned __int128")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_int128_spelling_3() -> None: + content = """ + signed __int128 x; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="signed __int128")] + ) + ), + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ] + ) + ) + + +def test_gnu_int128_raw_keyword(): + lexer = PlyLexer() + lexer.input("__int128") + token = lexer.token() + assert (token.type, token.value) == ("__int128", "__int128") From abb08998b184526d9ccff0583dba8b5b1d6f0659 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:09:46 +0000 Subject: [PATCH 09/25] Allow nullptr_t as a typedef declarator name --- cxxheaderparser/parser.py | 2 +- tests/test_trivial_nullptr_t_typedef.py | 50 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/test_trivial_nullptr_t_typedef.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index a082989..385b78a 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1934,7 +1934,7 @@ def _parse_pqname( segments.append(self._parse_pqname_decltype_specifier()) # check for fundamentals, consume them - elif tok_value in _fundamentals: + elif tok_value in _fundamentals and (fund_ok or tok.type != "NAME"): if not fund_ok: raise self._parse_error(tok) diff --git a/tests/test_trivial_nullptr_t_typedef.py b/tests/test_trivial_nullptr_t_typedef.py new file mode 100644 index 0000000..d861a0e --- /dev/null +++ b/tests/test_trivial_nullptr_t_typedef.py @@ -0,0 +1,50 @@ +from cxxheaderparser.simple import ( + NamespaceScope, + ParsedData, + Typedef, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + DecltypeSpecifier, + FundamentalSpecifier, + NameSpecifier, + PQName, + Token, + Type, +) + + +def test_nullptr_t_typedef_name() -> None: + content = """ + typedef decltype(nullptr) nullptr_t; nullptr_t x; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + typedefs=[ + Typedef( + type=Type( + typename=PQName( + segments=[ + DecltypeSpecifier(tokens=[Token(value="nullptr")]) + ] + ) + ), + name="nullptr_t", + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="nullptr_t")] + ) + ), + ) + ], + ) + ) From fad624664bd36557bbcc8be369419b2df8d0348c Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:10:31 +0000 Subject: [PATCH 10/25] Allow requires expressions without parameter groups --- cxxheaderparser/parser.py | 6 +- tests/test_trivial_requires_requires.py | 112 ++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 tests/test_trivial_requires_requires.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 385b78a..9c73550 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -900,9 +900,11 @@ def _parse_requires( tok = self.lex.token() elif tok.type == "requires": rawtoks.append(tok) - for tt in ("(", "{"): - tok = self._next_token_must_be(tt) + tok = self._next_token_must_be("(", "{") + if tok.type == "(": rawtoks.extend(self._consume_balanced_tokens(tok)) + tok = self._next_token_must_be("{") + rawtoks.extend(self._consume_balanced_tokens(tok)) tok = self.lex.token() else: tok = self._parse_requires_segment(tok, rawtoks) diff --git a/tests/test_trivial_requires_requires.py b/tests/test_trivial_requires_requires.py new file mode 100644 index 0000000..f1cc166 --- /dev/null +++ b/tests/test_trivial_requires_requires.py @@ -0,0 +1,112 @@ +from cxxheaderparser.simple import ( + Function, + NamespaceScope, + ParsedData, + Value, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + FundamentalSpecifier, + NameSpecifier, + PQName, + TemplateDecl, + TemplateTypeParam, + Token, + Type, +) + + +def test_requires_expression_without_parameters() -> None: + content = """ + template requires requires { typename T::type; } void f(); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")], + raw_requires_pre=Value( + tokens=[ + Token(value="requires"), + Token(value="{"), + Token(value="typename"), + Token(value="T"), + Token(value="::"), + Token(value="type"), + Token(value=";"), + Token(value="}"), + ] + ), + ), + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_requires_expression_with_parameters() -> None: + content = """ + template requires requires (T t) { t.f(); } void f(); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")], + raw_requires_pre=Value( + tokens=[ + Token(value="requires"), + Token(value="("), + Token(value="T"), + Token(value="t"), + Token(value=")"), + Token(value="{"), + Token(value="t"), + Token(value="."), + Token(value="f"), + Token(value="("), + Token(value=")"), + Token(value=";"), + Token(value="}"), + ] + ), + ), + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) From 5764da22968b0195b2bca65c598adb7adc17ea89 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:10:50 +0000 Subject: [PATCH 11/25] Accept boolean literals in constraints --- cxxheaderparser/parser.py | 3 + tests/test_trivial_requires_true.py | 248 ++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 tests/test_trivial_requires_true.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 9c73550..545ab57 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -906,6 +906,9 @@ def _parse_requires( tok = self._next_token_must_be("{") rawtoks.extend(self._consume_balanced_tokens(tok)) tok = self.lex.token() + elif tok.type in ("true", "false"): + rawtoks.append(tok) + tok = self.lex.token() else: tok = self._parse_requires_segment(tok, rawtoks) diff --git a/tests/test_trivial_requires_true.py b/tests/test_trivial_requires_true.py new file mode 100644 index 0000000..21460f7 --- /dev/null +++ b/tests/test_trivial_requires_true.py @@ -0,0 +1,248 @@ +from cxxheaderparser.simple import ( + Function, + NamespaceScope, + ParsedData, + Value, + parse_string, +) +from cxxheaderparser.types import ( + FundamentalSpecifier, + NameSpecifier, + PQName, + TemplateDecl, + TemplateTypeParam, + Token, + Type, +) + + +def test_boolean_constraint_leading_true() -> None: + content = """ + template requires true void f(); + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")], + raw_requires_pre=Value(tokens=[Token(value="true")]), + ), + ) + ] + ) + ) + + +def test_boolean_constraint_leading_false() -> None: + content = """ + template requires false void f(); + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")], + raw_requires_pre=Value(tokens=[Token(value="false")]), + ), + ) + ] + ) + ) + + +def test_boolean_constraint_leading_or() -> None: + content = """ + template requires false || true void f(); + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")], + raw_requires_pre=Value( + tokens=[ + Token(value="false"), + Token(value="||"), + Token(value="true"), + ] + ), + ), + ) + ] + ) + ) + + +def test_boolean_constraint_leading_and() -> None: + content = """ + template requires true && false void f(); + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")], + raw_requires_pre=Value( + tokens=[ + Token(value="true"), + Token(value="&&"), + Token(value="false"), + ] + ), + ), + ) + ] + ) + ) + + +def test_boolean_constraint_trailing_true() -> None: + content = """ + template void f() requires true; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + raw_requires=Value(tokens=[Token(value="true")]), + ) + ] + ) + ) + + +def test_boolean_constraint_trailing_false() -> None: + content = """ + template void f() requires false; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + raw_requires=Value(tokens=[Token(value="false")]), + ) + ] + ) + ) + + +def test_boolean_constraint_trailing_or() -> None: + content = """ + template void f() requires false || true; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + raw_requires=Value( + tokens=[ + Token(value="false"), + Token(value="||"), + Token(value="true"), + ] + ), + ) + ] + ) + ) + + +def test_boolean_constraint_trailing_and() -> None: + content = """ + template void f() requires true && false; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + raw_requires=Value( + tokens=[ + Token(value="true"), + Token(value="&&"), + Token(value="false"), + ] + ), + ) + ] + ) + ) From b731ef045bec8e44aa8a9c635132d946fa769c22 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:11:09 +0000 Subject: [PATCH 12/25] Redispatch constrained templates before parsing aliases --- cxxheaderparser/parser.py | 7 ++-- tests/test_trivial_constrained_alias.py | 54 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 tests/test_trivial_constrained_alias.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 545ab57..8fa44a9 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -724,15 +724,16 @@ def _parse_template(self, tok: LexToken, doxygen: typing.Optional[str]) -> None: self._parse_declarations(tok, doxygen, templates) return + if tok.type == "requires": + template.raw_requires_pre = self._parse_requires(tok) + tok = self.lex.token() + if tok.type == "using": self._parse_using(tok, doxygen, template) elif tok.type == "friend": self._parse_friend_decl(tok, doxygen, template) elif tok.type == "concept": self._parse_concept(tok, doxygen, template) - elif tok.type == "requires": - template.raw_requires_pre = self._parse_requires(tok) - self._parse_declarations(self.lex.token(), doxygen, template) else: self._parse_declarations(tok, doxygen, template) diff --git a/tests/test_trivial_constrained_alias.py b/tests/test_trivial_constrained_alias.py new file mode 100644 index 0000000..0c6f9a5 --- /dev/null +++ b/tests/test_trivial_constrained_alias.py @@ -0,0 +1,54 @@ +from cxxheaderparser.simple import ( + NamespaceScope, + ParsedData, + UsingAlias, + Value, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + FundamentalSpecifier, + NameSpecifier, + PQName, + TemplateDecl, + TemplateTypeParam, + Token, + Type, +) + + +def test_constrained_alias() -> None: + content = """ + template requires (true) using A = T; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + using_alias=[ + UsingAlias( + alias="A", + type=Type(typename=PQName(segments=[NameSpecifier(name="T")])), + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")], + raw_requires_pre=Value( + tokens=[ + Token(value="("), + Token(value="true"), + Token(value=")"), + ] + ), + ), + ) + ], + ) + ) From 0750e1f16037a6be18deb886c8fdc2f76c83cff0 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:11:30 +0000 Subject: [PATCH 13/25] Parse trailing constraints after noexcept and return types --- cxxheaderparser/parser.py | 14 +- ...test_trivial_trailing_requires_noexcept.py | 179 ++++++++++++++++++ 2 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 tests/test_trivial_trailing_requires_noexcept.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 8fa44a9..b32eb22 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2220,19 +2220,19 @@ def _parse_fn_end(self, fn: Function) -> None: if otok: toks = self._consume_balanced_tokens(otok)[1:-1] fn.noexcept = self._create_value(toks) - else: - rtok = self.lex.token_if("requires") - if rtok: - # requires on a function must always be accompanied by a template - if fn.template is None: - raise self._parse_error(rtok) - fn.raw_requires = self._parse_requires(rtok) if self.lex.token_if("ARROW"): return_type = self._parse_trailing_return_type(fn.return_type) fn.has_trailing_return = True fn.return_type = return_type + rtok = self.lex.token_if("requires") + if rtok: + # Free-function constraints require a template. + if fn.template is None: + raise self._parse_error(rtok) + fn.raw_requires = self._parse_requires(rtok) + extra_attrs = self._consume_decl_attributes() if extra_attrs: fn.attributes.extend(extra_attrs) diff --git a/tests/test_trivial_trailing_requires_noexcept.py b/tests/test_trivial_trailing_requires_noexcept.py new file mode 100644 index 0000000..10658dd --- /dev/null +++ b/tests/test_trivial_trailing_requires_noexcept.py @@ -0,0 +1,179 @@ +from cxxheaderparser.simple import ( + Function, + NamespaceScope, + ParsedData, + Value, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + FundamentalSpecifier, + NameSpecifier, + PQName, + TemplateDecl, + TemplateTypeParam, + Token, + Type, +) +import re +import pytest +from cxxheaderparser.errors import CxxParseError + + +def test_sequential_function_suffixes_noexcept() -> None: + content = """ + template void f() noexcept requires (true); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + noexcept=Value(tokens=[]), + raw_requires=Value( + tokens=[Token(value="("), Token(value="true"), Token(value=")")] + ), + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_sequential_function_suffixes_conditional_noexcept() -> None: + content = """ + template void f() noexcept(false) requires (true); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + noexcept=Value(tokens=[Token(value="false")]), + raw_requires=Value( + tokens=[Token(value="("), Token(value="true"), Token(value=")")] + ), + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_sequential_function_suffixes_return_type() -> None: + content = """ + template auto f() -> int requires (true); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + has_trailing_return=True, + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + raw_requires=Value( + tokens=[Token(value="("), Token(value="true"), Token(value=")")] + ), + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_sequential_function_suffixes_noexcept_return_type() -> None: + content = """ + template auto f() noexcept -> int requires (true); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + has_trailing_return=True, + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + noexcept=Value(tokens=[]), + raw_requires=Value( + tokens=[Token(value="("), Token(value="true"), Token(value=")")] + ), + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_non_template_constraint_still_rejected() -> None: + content = """ + void f() noexcept requires (true); + """ + + err = ":1: parse error evaluating 'requires': unexpected 'requires'" + with pytest.raises(CxxParseError, match=re.escape(err)): + parse_string(content, cleandoc=True) From ff7d730304de2fb21f7504ee8cdf4cdbf225a0ed Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:11:49 +0000 Subject: [PATCH 14/25] Permit function typedefs with inherited C linkage --- cxxheaderparser/parser.py | 2 +- .../test_trivial_extern_c_function_typedef.py | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/test_trivial_extern_c_function_typedef.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index b32eb22..1c93381 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2474,7 +2474,7 @@ def _parse_function( if fn.constexpr: raise CxxParseError("typedef function may not be constexpr") - if fn.extern: + if mods.extern is not None: raise CxxParseError("typedef function may not be extern") if fn.static: raise CxxParseError("typedef function may not be static") diff --git a/tests/test_trivial_extern_c_function_typedef.py b/tests/test_trivial_extern_c_function_typedef.py new file mode 100644 index 0000000..035d053 --- /dev/null +++ b/tests/test_trivial_extern_c_function_typedef.py @@ -0,0 +1,79 @@ +from cxxheaderparser.simple import ( + Function, + NamespaceScope, + ParsedData, + Typedef, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + FunctionType, + FundamentalSpecifier, + NameSpecifier, + PQName, + Type, +) +import re +import pytest +from cxxheaderparser.errors import CxxParseError + + +def test_inherited_linkage_function_typedef() -> None: + content = """ + extern "C" { typedef void F(); void f(); extern int x; } int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + extern='"C"', + ) + ], + typedefs=[ + Typedef( + type=FunctionType( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + parameters=[], + ), + name="F", + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + extern='"C"', + ), + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ], + ) + ) + + +def test_explicit_extern_function_typedef_rejected() -> None: + content = """ + typedef extern void F(); + """ + + err = ":1: parse error evaluating 'typedef': parsing typedef: unexpected 'extern'" + with pytest.raises(CxxParseError, match=re.escape(err)): + parse_string(content, cleandoc=True) From 2670fbb29537e902d9104354b88bbd64ff47d307 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:12:14 +0000 Subject: [PATCH 15/25] Recognize variadic terminators without a preceding comma --- cxxheaderparser/parser.py | 6 +- tests/test_trivial_ellipsis_without_comma.py | 98 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/test_trivial_ellipsis_without_comma.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 1c93381..7f534e8 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2135,7 +2135,11 @@ def _parse_parameters( ) ) - tok = self._next_token_must_be(",", ")") + tok = self._next_token_must_be(",", ")", "ELLIPSIS") + if tok.type == "ELLIPSIS": + vararg = True + self._next_token_must_be(")") + break if tok.value == ")": break diff --git a/tests/test_trivial_ellipsis_without_comma.py b/tests/test_trivial_ellipsis_without_comma.py new file mode 100644 index 0000000..304e70f --- /dev/null +++ b/tests/test_trivial_ellipsis_without_comma.py @@ -0,0 +1,98 @@ +from cxxheaderparser.simple import ( + Function, + NamespaceScope, + ParsedData, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + FundamentalSpecifier, + NameSpecifier, + PQName, + Parameter, + Pointer, + Type, +) + + +def test_ellipsis_without_comma_int() -> None: + content = """ + void f(int x...); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[ + Parameter( + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="x", + ) + ], + vararg=True, + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_ellipsis_without_comma_pointer() -> None: + content = """ + void f(const char *format...); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="void")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[ + Parameter( + type=Pointer( + ptr_to=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="char")] + ), + const=True, + ) + ), + name="format", + ) + ], + vararg=True, + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) From 57af41ec1670f77dc10f6cd2f744e30f315025ff Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:12:32 +0000 Subject: [PATCH 16/25] Accept friend specifiers following return types --- cxxheaderparser/parser.py | 2 +- .../test_trivial_friend_after_return_type.py | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/test_trivial_friend_after_return_type.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 7f534e8..dfde2f2 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2888,7 +2888,7 @@ def _parse_type( break elif tok_type == "const": const = True - elif tok_type == "friend" and pqname is None: + elif tok_type == "friend": mods.friend = tok elif tok_type == "constexpr": mods.constexpr = tok diff --git a/tests/test_trivial_friend_after_return_type.py b/tests/test_trivial_friend_after_return_type.py new file mode 100644 index 0000000..fe32929 --- /dev/null +++ b/tests/test_trivial_friend_after_return_type.py @@ -0,0 +1,68 @@ +from cxxheaderparser.simple import ( + ClassDecl, + ClassScope, + FriendDecl, + Method, + NamespaceScope, + ParsedData, + Variable, + parse_string, +) +from cxxheaderparser.types import FundamentalSpecifier, NameSpecifier, PQName, Type +import re +import pytest +from cxxheaderparser.errors import CxxParseError + + +def test_friend_after_return_type() -> None: + content = """ + struct S { void friend f(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + friends=[ + FriendDecl( + fn=Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + access="public", + ) + ) + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_friend_outside_class_rejected() -> None: + content = """ + void friend f(); + """ + + err = ":1: parse error evaluating 'friend': unexpected 'friend'" + with pytest.raises(CxxParseError, match=re.escape(err)): + parse_string(content, cleandoc=True) From 23831ddf78194df88fae2bd14599cdc21ef724d9 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:12:59 +0000 Subject: [PATCH 17/25] Skip grouped template arguments in constructor initializer ids --- cxxheaderparser/parser.py | 6 + .../test_trivial_base_initializer_decltype.py | 347 ++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 tests/test_trivial_base_initializer_decltype.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index dfde2f2..a139b87 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1567,6 +1567,12 @@ def _discard_ctor_initializer(self) -> None: # each initializer is either foo() or foo{}, so look for that while True: + if tok.type == "<": + # Parentheses in template arguments belong to the + # initializer-id, not to its argument list. + self._consume_balanced_tokens(tok) + tok = get_token() + continue if tok.type not in ("{", "("): tok = get_token() continue diff --git a/tests/test_trivial_base_initializer_decltype.py b/tests/test_trivial_base_initializer_decltype.py new file mode 100644 index 0000000..12fef39 --- /dev/null +++ b/tests/test_trivial_base_initializer_decltype.py @@ -0,0 +1,347 @@ +from cxxheaderparser.simple import ( + ClassDecl, + ClassScope, + Method, + NamespaceScope, + ParsedData, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + BaseClass, + DecltypeSpecifier, + FundamentalSpecifier, + NameSpecifier, + PQName, + Parameter, + TemplateArgument, + TemplateDecl, + TemplateSpecialization, + TemplateTypeParam, + Token, + Type, +) + + +def test_base_initializer_parentheses() -> None: + content = """ + template struct A { A(int); }; struct B : A { B() : A(0) {} void after(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="A")], classkey="struct" + ), + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + ), + methods=[ + Method( + return_type=None, + name=PQName(segments=[NameSpecifier(name="A")]), + parameters=[ + Parameter( + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ) + ) + ], + access="public", + constructor=True, + ) + ], + ), + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="B")], classkey="struct" + ), + bases=[ + BaseClass( + access="public", + typename=PQName( + segments=[ + NameSpecifier( + name="A", + specialization=TemplateSpecialization( + args=[ + TemplateArgument( + arg=Type( + typename=PQName( + segments=[ + DecltypeSpecifier( + tokens=[ + Token( + value="0" + ) + ] + ) + ] + ) + ) + ) + ] + ), + ) + ] + ), + ) + ], + ), + methods=[ + Method( + return_type=None, + name=PQName(segments=[NameSpecifier(name="B")]), + parameters=[], + has_body=True, + access="public", + constructor=True, + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="after")]), + parameters=[], + access="public", + ), + ], + ), + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_base_initializer_braces() -> None: + content = """ + template struct A { A(int); }; struct B : A { B() : A{0} {} void after(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="A")], classkey="struct" + ), + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + ), + methods=[ + Method( + return_type=None, + name=PQName(segments=[NameSpecifier(name="A")]), + parameters=[ + Parameter( + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ) + ) + ], + access="public", + constructor=True, + ) + ], + ), + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="B")], classkey="struct" + ), + bases=[ + BaseClass( + access="public", + typename=PQName( + segments=[ + NameSpecifier( + name="A", + specialization=TemplateSpecialization( + args=[ + TemplateArgument( + arg=Type( + typename=PQName( + segments=[ + DecltypeSpecifier( + tokens=[ + Token( + value="0" + ) + ] + ) + ] + ) + ) + ) + ] + ), + ) + ] + ), + ) + ], + ), + methods=[ + Method( + return_type=None, + name=PQName(segments=[NameSpecifier(name="B")]), + parameters=[], + has_body=True, + access="public", + constructor=True, + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="after")]), + parameters=[], + access="public", + ), + ], + ), + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_base_initializer_nested_parentheses() -> None: + content = """ + template struct A { A(int); }; struct B : A { B() : A(0) {} void after(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="A")], classkey="struct" + ), + template=TemplateDecl( + params=[TemplateTypeParam(typekey="class", name="T")] + ), + ), + methods=[ + Method( + return_type=None, + name=PQName(segments=[NameSpecifier(name="A")]), + parameters=[ + Parameter( + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ) + ) + ], + access="public", + constructor=True, + ) + ], + ), + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="B")], classkey="struct" + ), + bases=[ + BaseClass( + access="public", + typename=PQName( + segments=[ + NameSpecifier( + name="A", + specialization=TemplateSpecialization( + args=[ + TemplateArgument( + arg=Type( + typename=PQName( + segments=[ + DecltypeSpecifier( + tokens=[ + Token( + value="0" + ) + ] + ) + ] + ) + ) + ) + ] + ), + ) + ] + ), + ) + ], + ), + methods=[ + Method( + return_type=None, + name=PQName(segments=[NameSpecifier(name="B")]), + parameters=[], + has_body=True, + access="public", + constructor=True, + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="after")]), + parameters=[], + access="public", + ), + ], + ), + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) From 0d6582571256d8b7a521a6a5ebd6e12e3fc011d2 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:13:27 +0000 Subject: [PATCH 18/25] Consume GNU enumerator attributes with the shared consumer --- cxxheaderparser/parser.py | 4 +- .../test_trivial_gnu_enumerator_attribute.py | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/test_trivial_gnu_enumerator_attribute.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index a139b87..dfd95c5 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1349,9 +1349,7 @@ def _parse_enumerator_list(self) -> typing.List[Enumerator]: if doxygen is None: doxygen = self.lex.get_doxygen_after() - tok = self.lex.token_if("DBL_LBRACKET") - if tok: - attributes = self._consume_attribute_specifier_seq(tok, record=False) + attributes = self._consume_decl_attributes() if doxygen is None: doxygen = self.lex.get_doxygen_after() diff --git a/tests/test_trivial_gnu_enumerator_attribute.py b/tests/test_trivial_gnu_enumerator_attribute.py new file mode 100644 index 0000000..eff8f10 --- /dev/null +++ b/tests/test_trivial_gnu_enumerator_attribute.py @@ -0,0 +1,95 @@ +from cxxheaderparser.simple import ( + EnumDecl, + NamespaceScope, + ParsedData, + Value, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + Attribute, + AttributeStyle, + Enumerator, + FundamentalSpecifier, + NameSpecifier, + PQName, + Token, + Type, +) + + +def test_gnu_enumerator_attributes_implicit() -> None: + content = """ + enum E { a __attribute__((deprecated)) __attribute__((unused)), b }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + enums=[ + EnumDecl( + typename=PQName( + segments=[NameSpecifier(name="E")], classkey="enum" + ), + values=[ + Enumerator( + name="a", + attributes=[ + Attribute(style=AttributeStyle.GCC, name="deprecated"), + Attribute(style=AttributeStyle.GCC, name="unused"), + ], + ), + Enumerator(name="b"), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_gnu_enumerator_attributes_explicit() -> None: + content = """ + enum E { a __attribute__((deprecated)) __attribute__((unused)) = 3, b }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + enums=[ + EnumDecl( + typename=PQName( + segments=[NameSpecifier(name="E")], classkey="enum" + ), + values=[ + Enumerator( + name="a", + value=Value(tokens=[Token(value="3")]), + attributes=[ + Attribute(style=AttributeStyle.GCC, name="deprecated"), + Attribute(style=AttributeStyle.GCC, name="unused"), + ], + ), + Enumerator(name="b"), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) From f73b36fd7509dd9b791a51567c38740fb4afa3e8 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:13:43 +0000 Subject: [PATCH 19/25] Consume attributes within method suffix parsing --- cxxheaderparser/parser.py | 2 + ...t_trivial_gnu_member_function_attribute.py | 243 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 tests/test_trivial_gnu_member_function_attribute.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index dfd95c5..d8c8b7e 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2316,6 +2316,8 @@ def _parse_method_end(self, method: Method) -> None: method.noexcept = self._create_value(toks) elif tok_value == "requires": method.raw_requires = self._parse_requires(tok) + elif tok.type in self._attribute_start_tokens: + method.attributes.extend(self._consume_attribute(tok, record=False)) else: self.lex.return_token(tok) break diff --git a/tests/test_trivial_gnu_member_function_attribute.py b/tests/test_trivial_gnu_member_function_attribute.py new file mode 100644 index 0000000..6210737 --- /dev/null +++ b/tests/test_trivial_gnu_member_function_attribute.py @@ -0,0 +1,243 @@ +from cxxheaderparser.simple import ( + ClassDecl, + ClassScope, + Method, + NamespaceScope, + ParsedData, + Value, + Variable, + parse_string, +) +from cxxheaderparser.types import ( + Attribute, + AttributeStyle, + FundamentalSpecifier, + NameSpecifier, + PQName, + Type, +) + + +def test_gnu_member_function_attributes_definition() -> None: + content = """ + struct S { void f() __attribute__((unused)) {} void g(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + attributes=[ + Attribute(style=AttributeStyle.GCC, name="unused") + ], + has_body=True, + access="public", + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="g")]), + parameters=[], + access="public", + ), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_gnu_member_function_attributes_const() -> None: + content = """ + struct S { void f() __attribute__((unused)) const {} void g(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + attributes=[ + Attribute(style=AttributeStyle.GCC, name="unused") + ], + has_body=True, + access="public", + const=True, + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="g")]), + parameters=[], + access="public", + ), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_gnu_member_function_attributes_noexcept() -> None: + content = """ + struct S { void f() __attribute__((unused)) noexcept {} void g(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + attributes=[ + Attribute(style=AttributeStyle.GCC, name="unused") + ], + has_body=True, + noexcept=Value(tokens=[]), + access="public", + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="g")]), + parameters=[], + access="public", + ), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) + + +def test_gnu_member_function_attributes_declaration() -> None: + content = """ + struct S { void f() __attribute__((unused)) ; void g(); }; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + attributes=[ + Attribute(style=AttributeStyle.GCC, name="unused") + ], + access="public", + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="g")]), + parameters=[], + access="public", + ), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) From 730e5978cc05609bb0ecf7c88263696d748aa635 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:07:26 +0000 Subject: [PATCH 20/25] Preserve consteval function and method metadata --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 6 +- cxxheaderparser/parserstate.py | 9 ++- cxxheaderparser/types.py | 3 + tests/test_fix_consteval.py | 102 +++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 tests/test_fix_consteval.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index 7bc276c..56a74c2 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -88,6 +88,7 @@ class PlyLexer: "concept", "const", "constexpr", + "consteval", "const_cast", "continue", "decltype", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index d8c8b7e..ecf665b 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2404,6 +2404,7 @@ def _parse_function( template=template, operator=op, access=self._current_access, + consteval=mods.consteval is not None, constexpr=mods.constexpr is not None, extern=extern, static=mods.static is not None, @@ -2463,6 +2464,7 @@ def _parse_function( attributes=attributes, template=template, operator=op, + consteval=mods.consteval is not None, constexpr=mods.constexpr is not None, extern=extern, static=mods.static is not None, @@ -2813,7 +2815,7 @@ def _parse_cv_ptr_or_fn( return dtype # Applies to variables and return values - _type_kwd_both = {"const", "constexpr", "extern", "inline", "static"} + _type_kwd_both = {"const", "constexpr", "consteval", "extern", "inline", "static"} # Only found on methods _type_kwd_meth = {"explicit", "virtual"} @@ -2898,6 +2900,8 @@ def _parse_type( mods.friend = tok elif tok_type == "constexpr": mods.constexpr = tok + elif tok_type == "consteval": + mods.consteval = tok elif tok_type == "extern": linkage_tok = self.lex.token_if("STRING_LITERAL") if linkage_tok: diff --git a/cxxheaderparser/parserstate.py b/cxxheaderparser/parserstate.py index 90c7254..cac8168 100644 --- a/cxxheaderparser/parserstate.py +++ b/cxxheaderparser/parserstate.py @@ -13,6 +13,7 @@ class ParsedTypeModifiers: #: Modifiers allowed on variables and functions. constexpr: typing.Optional[LexToken] = None + consteval: typing.Optional[LexToken] = None extern: typing.Optional[LexToken] = None inline: typing.Optional[LexToken] = None static: typing.Optional[LexToken] = None @@ -48,7 +49,13 @@ def validate( raise CxxParseError(f"{msg}: unexpected '{tok.value}'") if not meth_ok and not var_ok: - for tok in (self.constexpr, self.extern, self.inline, self.static): + for tok in ( + self.constexpr, + self.consteval, + self.extern, + self.inline, + self.static, + ): if tok is not None: raise CxxParseError(f"{msg}: unexpected '{tok.value}'") diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index eb25cfb..19ae3b8 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -841,6 +841,9 @@ class Function: #: template int main() requires ... raw_requires: typing.Optional[Value] = None + #: True for an immediate function declared with ``consteval``. + consteval: bool = False + @dataclass class Method(Function): diff --git a/tests/test_fix_consteval.py b/tests/test_fix_consteval.py new file mode 100644 index 0000000..7b9ed11 --- /dev/null +++ b/tests/test_fix_consteval.py @@ -0,0 +1,102 @@ +from cxxheaderparser.simple import ClassScope, NamespaceScope, ParsedData, parse_string +from cxxheaderparser.types import ( + ClassDecl, + Function, + FundamentalSpecifier, + Method, + NameSpecifier, + PQName, + Type, +) + + +def test_consteval_functions_and_methods() -> None: + content = """ + consteval int f(); + constexpr int g(); + int ordinary(); + struct S { + consteval int f() const { return 1; } + static consteval int g() { return 2; } + int ordinary(); + }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + has_body=True, + consteval=True, + access="public", + const=True, + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name=PQName(segments=[NameSpecifier(name="g")]), + parameters=[], + static=True, + has_body=True, + consteval=True, + access="public", + ), + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name=PQName(segments=[NameSpecifier(name="ordinary")]), + parameters=[], + access="public", + ), + ], + ) + ], + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + consteval=True, + ), + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="g")]), + parameters=[], + constexpr=True, + ), + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="ordinary")]), + parameters=[], + ), + ], + ) + ) From 8e342d8b9582202e67dfaf478ce1b5a227b85b5e Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:07:55 +0000 Subject: [PATCH 21/25] Preserve standard and GNU thread-local storage --- cxxheaderparser/lexer.py | 1 + cxxheaderparser/parser.py | 15 ++- cxxheaderparser/parserstate.py | 3 +- cxxheaderparser/types.py | 6 ++ tests/test_fix_thread_local.py | 188 +++++++++++++++++++++++++++++++++ 5 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 tests/test_fix_thread_local.py diff --git a/cxxheaderparser/lexer.py b/cxxheaderparser/lexer.py index 56a74c2..77aac52 100644 --- a/cxxheaderparser/lexer.py +++ b/cxxheaderparser/lexer.py @@ -151,6 +151,7 @@ class PlyLexer: "template", "this", "thread_local", + "__thread", "throw", "true", "try", diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index ecf665b..4a3f35f 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1710,6 +1710,7 @@ def _parse_field( attributes=attributes, constexpr=mods.constexpr is not None, mutable=mods.mutable is not None, + thread_local=mods.thread_local is not None, static=mods.static is not None, inline=mods.inline is not None, ) @@ -1725,6 +1726,7 @@ def _parse_field( attributes=attributes, constexpr=mods.constexpr is not None, extern=self._make_extern_value(mods), + thread_local=mods.thread_local is not None, static=mods.static is not None, inline=mods.inline is not None, ) @@ -2815,7 +2817,16 @@ def _parse_cv_ptr_or_fn( return dtype # Applies to variables and return values - _type_kwd_both = {"const", "constexpr", "consteval", "extern", "inline", "static"} + _type_kwd_both = { + "const", + "constexpr", + "consteval", + "extern", + "inline", + "static", + "thread_local", + "__thread", + } # Only found on methods _type_kwd_meth = {"explicit", "virtual"} @@ -2902,6 +2913,8 @@ def _parse_type( mods.constexpr = tok elif tok_type == "consteval": mods.consteval = tok + elif tok_type in ("thread_local", "__thread"): + mods.thread_local = tok elif tok_type == "extern": linkage_tok = self.lex.token_if("STRING_LITERAL") if linkage_tok: diff --git a/cxxheaderparser/parserstate.py b/cxxheaderparser/parserstate.py index cac8168..bf96c3e 100644 --- a/cxxheaderparser/parserstate.py +++ b/cxxheaderparser/parserstate.py @@ -20,6 +20,7 @@ class ParsedTypeModifiers: #: Modifiers only allowed on variables/fields. mutable: typing.Optional[LexToken] = None + thread_local: typing.Optional[LexToken] = None #: Modifiers only allowed on methods. explicit: typing.Optional[LexToken] = None @@ -39,7 +40,7 @@ def validate( ) -> None: # Almost there! Do any checks the caller asked for if not var_ok: - for tok in (self.mutable,): + for tok in (self.mutable, self.thread_local): if tok is not None: raise CxxParseError(f"{msg}: unexpected '{tok.value}'") diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index 19ae3b8..e492220 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -950,6 +950,9 @@ class Variable: #: Any attributes attached to this variable attributes: typing.List[Attribute] = field(default_factory=list) + #: Thread-local storage (``thread_local`` or GNU ``__thread``). + thread_local: bool = False + @dataclass class Field: @@ -975,6 +978,9 @@ class Field: #: Any attributes attached to this field attributes: typing.List[Attribute] = field(default_factory=list) + #: Thread-local storage (``thread_local`` or GNU ``__thread``). + thread_local: bool = False + @dataclass class UsingDecl: diff --git a/tests/test_fix_thread_local.py b/tests/test_fix_thread_local.py new file mode 100644 index 0000000..53e0d60 --- /dev/null +++ b/tests/test_fix_thread_local.py @@ -0,0 +1,188 @@ +from cxxheaderparser.simple import ClassScope, NamespaceScope, ParsedData, parse_string +from cxxheaderparser.types import ( + ClassDecl, + Field, + FundamentalSpecifier, + NameSpecifier, + PQName, + Type, + Variable, +) + + +def test_thread_storage_variant_01() -> None: + content = """ + thread_local int x; + static thread_local int local; + extern thread_local int shared; + thread_local static int reversed; + int ordinary; + struct S { static thread_local int member; static int ordinary; }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + fields=[ + Field( + access="public", + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="member", + static=True, + thread_local=True, + ), + Field( + access="public", + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="ordinary", + static=True, + ), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="local")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + static=True, + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="shared")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + extern=True, + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="reversed")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + static=True, + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="ordinary")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ], + ) + ) + + +def test_thread_storage_variant_02() -> None: + content = """ + __thread int x; + static __thread int local; + extern __thread int shared; + __thread static int reversed; + int ordinary; + struct S { static __thread int member; static int ordinary; }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ) + ), + fields=[ + Field( + access="public", + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="member", + static=True, + thread_local=True, + ), + Field( + access="public", + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="ordinary", + static=True, + ), + ], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="x")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="local")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + static=True, + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="shared")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + extern=True, + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="reversed")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + static=True, + thread_local=True, + ), + Variable( + name=PQName(segments=[NameSpecifier(name="ordinary")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ), + ], + ) + ) From 4c0476c8adaf4a99d5b108b818b7b487788ad485 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:08:54 +0000 Subject: [PATCH 22/25] Preserve using-declaration pack expansions --- cxxheaderparser/parser.py | 7 ++- cxxheaderparser/types.py | 3 + tests/test_fix_using_pack.py | 106 +++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 tests/test_fix_using_pack.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 4a3f35f..5466744 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1192,7 +1192,12 @@ def _parse_using_declaration( typename, _ = self._parse_pqname( tok, fn_ok=True, compound_ok=True, fund_ok=True ) - decl = UsingDecl(typename, self._current_access, doxygen) + decl = UsingDecl( + typename, + self._current_access, + doxygen, + param_pack=self.lex.token_if("ELLIPSIS") is not None, + ) self.visitor.on_using_declaration(self.state, decl) diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index e492220..333ff18 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -998,6 +998,9 @@ class UsingDecl: #: Documentation if present doxygen: typing.Optional[str] = None + #: True if the using-declaration is a pack expansion. + param_pack: bool = False + @dataclass class UsingAlias: diff --git a/tests/test_fix_using_pack.py b/tests/test_fix_using_pack.py new file mode 100644 index 0000000..de59464 --- /dev/null +++ b/tests/test_fix_using_pack.py @@ -0,0 +1,106 @@ +from cxxheaderparser.simple import ClassScope, NamespaceScope, ParsedData, parse_string +from cxxheaderparser.types import ( + BaseClass, + ClassDecl, + FundamentalSpecifier, + Method, + NameSpecifier, + PQName, + TemplateDecl, + TemplateTypeParam, + Type, + UsingDecl, +) + + +def test_using_pack_expansion() -> None: + content = """ + template struct S : T... { using T::f...; }; + struct B { void f(); }; + struct D : B { using B::f; }; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="S")], classkey="struct" + ), + bases=[ + BaseClass( + access="public", + typename=PQName(segments=[NameSpecifier(name="T")]), + param_pack=True, + ) + ], + template=TemplateDecl( + params=[ + TemplateTypeParam( + typekey="class", name="T", param_pack=True + ) + ] + ), + ), + using=[ + UsingDecl( + typename=PQName( + segments=[ + NameSpecifier(name="T"), + NameSpecifier(name="f"), + ] + ), + access="public", + param_pack=True, + ) + ], + ), + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="B")], classkey="struct" + ) + ), + methods=[ + Method( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="void")] + ) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + access="public", + ) + ], + ), + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="D")], classkey="struct" + ), + bases=[ + BaseClass( + access="public", + typename=PQName(segments=[NameSpecifier(name="B")]), + ) + ], + ), + using=[ + UsingDecl( + typename=PQName( + segments=[ + NameSpecifier(name="B"), + NameSpecifier(name="f"), + ] + ), + access="public", + ) + ], + ), + ] + ) + ) From 7fdc83bc04beca6090cc51fd4216f077c1f2c3d2 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:15:13 +0000 Subject: [PATCH 23/25] Retain attributes on typedefs following struct definitions --- cxxheaderparser/parser.py | 2 + tests/test_fix_typedef_attributes.py | 66 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/test_fix_typedef_attributes.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 5466744..d1b5e9e 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1674,6 +1674,8 @@ def _parse_field( bits = self._parse_bitfield() + attributes.extend(self._consume_decl_attributes()) + # default values tok = self.lex.token_if("=") if tok: diff --git a/tests/test_fix_typedef_attributes.py b/tests/test_fix_typedef_attributes.py new file mode 100644 index 0000000..ed6dcde --- /dev/null +++ b/tests/test_fix_typedef_attributes.py @@ -0,0 +1,66 @@ +from cxxheaderparser.simple import ClassScope, NamespaceScope, ParsedData, parse_string +from cxxheaderparser.types import ( + AnonymousName, + Attribute, + AttributeStyle, + ClassDecl, + Field, + FundamentalSpecifier, + NameSpecifier, + PQName, + Type, + Typedef, + Variable, +) + + +def test_gnu_typedef_struct_attribute() -> None: + content = """ + typedef struct { int x; } S __attribute__((aligned)); int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[AnonymousName(id=1)], classkey="struct" + ) + ), + fields=[ + Field( + access="public", + type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="int")] + ) + ), + name="x", + ) + ], + ) + ], + typedefs=[ + Typedef( + type=Type( + typename=PQName( + segments=[AnonymousName(id=1)], classkey="struct" + ) + ), + name="S", + attributes=[Attribute(style=AttributeStyle.GCC, name="aligned")], + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + ) From 8b155cddb099c93b274975c64e80706dae585e9b Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:09:45 +0000 Subject: [PATCH 24/25] Preserve standard and GNU alias attributes --- cxxheaderparser/parser.py | 14 +++++- cxxheaderparser/types.py | 3 ++ tests/test_fix_alias_attributes.py | 71 ++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/test_fix_alias_attributes.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index d1b5e9e..81f931b 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1211,9 +1211,18 @@ def _parse_using_typealias( alias_declaration: "using" IDENTIFIER "=" type_id ";" """ + alias_attributes = self._take_pending_attributes() dtype = self._parse_type_id(None, "parsing typealias") + alias_attributes.extend(self._take_pending_attributes()) - alias = UsingAlias(id_tok.value, dtype, template, self._current_access, doxygen) + alias = UsingAlias( + id_tok.value, + dtype, + template, + self._current_access, + doxygen, + alias_attributes, + ) self.visitor.on_using_alias(self.state, alias) @@ -1229,6 +1238,9 @@ def _parse_using( "NAME", "DBL_COLON", "namespace", "typename", "enum" ) + if tok.type == "NAME": + self._record_attributes(self._consume_decl_attributes()) + if tok.type == "namespace": if template: raise CxxParseError( diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index 333ff18..1a7b9cc 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -1025,6 +1025,9 @@ class UsingAlias: #: Documentation if present doxygen: typing.Optional[str] = None + #: Attributes attached to this alias declaration + attributes: typing.List[Attribute] = field(default_factory=list) + @dataclass class DeductionGuide: diff --git a/tests/test_fix_alias_attributes.py b/tests/test_fix_alias_attributes.py new file mode 100644 index 0000000..d1c29a8 --- /dev/null +++ b/tests/test_fix_alias_attributes.py @@ -0,0 +1,71 @@ +from cxxheaderparser.simple import NamespaceScope, ParsedData, parse_string +from cxxheaderparser.types import ( + Attribute, + AttributeStyle, + FundamentalSpecifier, + NameSpecifier, + PQName, + Type, + UsingAlias, + Variable, +) + + +def test_alias_attribute_variant_01() -> None: + content = """ + using T __attribute__((deprecated)) = int; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + using_alias=[ + UsingAlias( + alias="T", + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + attributes=[Attribute(style=AttributeStyle.GCC, name="deprecated")], + ) + ], + ) + ) + + +def test_alias_attribute_variant_02() -> None: + content = """ + using T [[deprecated]] = int; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + using_alias=[ + UsingAlias( + alias="T", + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + attributes=[Attribute(style=AttributeStyle.CXX, name="deprecated")], + ) + ], + ) + ) From c1ee11bdf3377163dd13bd970a478e5cb6e7432b Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 11 Oct 2026 01:15:38 +0000 Subject: [PATCH 25/25] Consume function type attributes before trailing returns --- cxxheaderparser/parser.py | 2 + tests/test_fix_trailing_return_attributes.py | 47 ++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/test_fix_trailing_return_attributes.py diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 81f931b..00fd9bf 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -2250,6 +2250,8 @@ def _parse_fn_end(self, fn: Function) -> None: toks = self._consume_balanced_tokens(otok)[1:-1] fn.noexcept = self._create_value(toks) + fn.attributes.extend(self._consume_decl_attributes()) + if self.lex.token_if("ARROW"): return_type = self._parse_trailing_return_type(fn.return_type) fn.has_trailing_return = True diff --git a/tests/test_fix_trailing_return_attributes.py b/tests/test_fix_trailing_return_attributes.py new file mode 100644 index 0000000..36ab488 --- /dev/null +++ b/tests/test_fix_trailing_return_attributes.py @@ -0,0 +1,47 @@ +from cxxheaderparser.simple import NamespaceScope, ParsedData, parse_string +from cxxheaderparser.types import ( + Attribute, + AttributeStyle, + Function, + FundamentalSpecifier, + NameSpecifier, + PQName, + Type, + Value, + Variable, +) + + +def test_noexcept_attribute_before_trailing_return() -> None: + content = """ + auto f() noexcept [[vendor::tag]] -> int; int after; + """ + + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + functions=[ + Function( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName(segments=[NameSpecifier(name="f")]), + parameters=[], + attributes=[ + Attribute(style=AttributeStyle.CXX, name="vendor::tag") + ], + has_trailing_return=True, + noexcept=Value(tokens=[]), + ) + ], + variables=[ + Variable( + name=PQName(segments=[NameSpecifier(name="after")]), + type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + ) + ], + ) + )