From e1067e50feeb88a9fbf211b49679b6adf5d83e14 Mon Sep 17 00:00:00 2001 From: II Assistant Date: Sat, 8 Nov 2025 12:30:42 +0000 Subject: [PATCH] Fix: Support __attribute__ within struct declarations This commit fixes issue #75 where __attribute__ specifiers within struct declarations were not parsed correctly, leading to a ParseError. Example code that now parses correctly: typedef struct { __attribute__((__deprecated__)) int test1; } test2; Changes: - Extended specifier_qualifier_list rule in GnuCParser to handle function_specifier nodes, allowing __attribute__ within structs - Added _p_specifier_qualifier_list_left_recursion method to correctly handle AttributeSpecifier nodes in the funcspec list - Modified _generate_decl in GnuCGenerator to handle funcspec list with AttributeSpecifier nodes - Improved AttributeSpecifier.__eq__ to use structural comparison of AST nodes instead of object identity - Added round-trip test to verify the fix Fixes #75 --- pycparserext/ext_c_generator.py | 10 ++-- pycparserext/ext_c_parser.py | 87 ++++++++++++++++++++++++++++++++- test/test_pycparserext.py | 10 ++++ 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/pycparserext/ext_c_generator.py b/pycparserext/ext_c_generator.py index 72a8d69..8aca8ec 100644 --- a/pycparserext/ext_c_generator.py +++ b/pycparserext/ext_c_generator.py @@ -127,6 +127,11 @@ def _generate_type(self, n, modifiers=None, emit_declname=True): else: return self.visit(n) + def visit_AttributeSpecifier(self, n): + return "__attribute__((" + self.visit(n.exprlist) + "))" + + +class GnuCGenerator(AsmAndAttributesMixin, CGeneratorBase): def _generate_decl(self, n): """ Generation from a Decl node. """ @@ -145,11 +150,6 @@ def funcspec_to_str(i): s += self._generate_type(n.type) return s - def visit_AttributeSpecifier(self, n): - return " __attribute__((" + self.visit(n.exprlist) + "))" - - -class GnuCGenerator(AsmAndAttributesMixin, CGeneratorBase): def visit_TypeOfDeclaration(self, n): return "%s(%s)" % (n.typeof_keyword, self.visit(n.declaration)) diff --git a/pycparserext/ext_c_parser.py b/pycparserext/ext_c_parser.py index feb6797..5b22546 100644 --- a/pycparserext/ext_c_parser.py +++ b/pycparserext/ext_c_parser.py @@ -1,4 +1,3 @@ - import pycparser.c_ast as c_ast import pycparser.c_parser @@ -59,6 +58,39 @@ class AttributeSpecifier(c_ast.Node): def __init__(self, exprlist): self.exprlist = exprlist + def __eq__(self, other): + if not isinstance(other, AttributeSpecifier): + return False + # Recursively compare the exprlist nodes + return self._compare_ast_nodes(self.exprlist, other.exprlist) + + def _compare_ast_nodes(self, node1, node2): + """Recursively compare two AST nodes for structural equality.""" + if type(node1) is not type(node2): + return False + + # Compare attributes + if hasattr(node1, "attr_names"): + for attr in node1.attr_names: + val1 = getattr(node1, attr) + val2 = getattr(node2, attr) + if val1 != val2: + return False + + # Compare children + if hasattr(node1, "children"): + children1 = node1.children() + children2 = node2.children() + if len(children1) != len(children2): + return False + for (name1, child1), (name2, child2) in zip(children1, children2): + if name1 != name2: + return False + if not self._compare_ast_nodes(child1, child2): + return False + + return True + def children(self): return [("exprlist", self.exprlist)] @@ -71,6 +103,32 @@ def __iter__(self): attr_names = () +class DeclExt(c_ast.Decl): + @staticmethod + def from_pycparser(decl): + assert isinstance(decl, c_ast.Decl) + new_decl = DeclExt( + name=decl.name, + quals=decl.quals, + align=decl.align, + storage=decl.storage, + funcspec=decl.funcspec, + type=decl.type, + init=decl.init, + bitsize=decl.bitsize, + coord=decl.coord, + ) + if hasattr(decl, "attributes"): + new_decl.attributes = decl.attributes + return new_decl + + def children(self): + nodelist = super().children() + if hasattr(self, "attributes"): + nodelist = (*nodelist, ("attributes", self.attributes)) + return nodelist + + class Asm(c_ast.Node): def __init__(self, asm_keyword, template, output_operands, input_operands, clobbered_regs, coord=None): @@ -580,6 +638,33 @@ def p_gnu_unary_expression(self, p): p[2] if len(p) == 3 else p[3], self._token_coord(p, 1)) + def p_specifier_qualifier_list_fs(self, p): + """ specifier_qualifier_list : function_specifier specifier_qualifier_list + """ + self._p_specifier_qualifier_list_left_recursion(p) + + def _p_specifier_qualifier_list_left_recursion(self, p): + # The PLY documentation says that left-recursive rules are supported, + # but it keeps complaining about reduce/reduce conflicts. + # + # See `_p_specifier_qualifier_list_right_recursion` for a non-complaining + # version. + spec = p[1] + spec_dict = p[2] + + if isinstance(spec, AttributeSpecifier): + spec_dict["function"].append(spec) + elif isinstance(spec, str): + spec_dict["qual"].append(spec) + elif isinstance(spec, c_ast.Node): + if "type" not in spec_dict: + spec_dict["type"] = [] + spec_dict["type"].append(spec) + else: + raise TypeError(f"Unknown specifier {spec!r} of type {type(spec)}") + + p[0] = spec_dict + def p_statement(self, p): """ statement : labeled_statement | expression_statement diff --git a/test/test_pycparserext.py b/test/test_pycparserext.py index cea4570..49e6b6e 100644 --- a/test/test_pycparserext.py +++ b/test/test_pycparserext.py @@ -755,6 +755,16 @@ def test_packed_anonymous_struct_in_struct_after(): assert _round_trip_matches(code) +def test_attribute_in_struct(): + # https://github.com/inducer/pycparserext/issues/75 + src = """ + typedef struct { + __attribute__((__deprecated__)) int test1; + } test2; + """ + assert _round_trip_matches(src) + + if __name__ == "__main__": import sys if len(sys.argv) > 1: