diff --git a/c_parser/lexer.py b/c_parser/lexer.py index 097dc36d6..89882a771 100644 --- a/c_parser/lexer.py +++ b/c_parser/lexer.py @@ -48,6 +48,7 @@ class CTopLevelSegment: original_end_column: int = 1 original_source_line: str | None = None original_end_source_line: str | None = None + original_source_lines: tuple[str, ...] = field(default_factory=tuple) _TWO_CHAR_OPERATORS = { @@ -82,6 +83,10 @@ def _source_line(lines: list[str], line_number: int) -> str | None: return None +def _source_lines(lines: list[str], start_line: int, end_line: int) -> tuple[str, ...]: + return tuple(lines[start_line - 1 : end_line]) + + def _advance_position(char: str, line: int, column: int) -> tuple[int, int]: if char == "\n": return line + 1, 1 @@ -138,26 +143,33 @@ def _scan_code_states(text: str): stack.pop() -def top_level_split(text: str, delimiter: str = ",") -> list[str]: - """Split on a delimiter that appears outside brackets and literals.""" +def top_level_split_with_offsets(text: str, delimiter: str = ",") -> list[tuple[str, int]]: + """Split outside nested syntax and preserve each trimmed fragment offset.""" if len(delimiter) != 1: - raise ValueError("top_level_split delimiter must be a single character") + raise ValueError("top_level_split_with_offsets delimiter must be a single character") - parts: list[str] = [] + parts: list[tuple[str, int]] = [] start = 0 for index, char, stack, state in _scan_code_states(text): if state == "normal" and not stack and char == delimiter: - part = text[start:index].strip() + raw_part = text[start:index] + part = raw_part.strip() if part: - parts.append(part) + parts.append((part, start + len(raw_part) - len(raw_part.lstrip()))) start = index + 1 - tail = text[start:].strip() + raw_tail = text[start:] + tail = raw_tail.strip() if tail: - parts.append(tail) + parts.append((tail, start + len(raw_tail) - len(raw_tail.lstrip()))) return parts +def top_level_split(text: str, delimiter: str = ",") -> list[str]: + """Split on a delimiter that appears outside brackets and literals.""" + return [part for part, _offset in top_level_split_with_offsets(text, delimiter)] + + def top_level_partition(text: str, delimiter: str = "=") -> tuple[str, str | None]: """Partition once on a top-level delimiter outside brackets and literals.""" if len(delimiter) != 1: @@ -281,6 +293,7 @@ def split_top_level_c_source( original_end_column=column, original_source_line=block_source_line, original_end_source_line=_source_line(source_lines, line), + original_source_lines=_source_lines(source_lines, block_start_line, line), ) ) block_header = None @@ -305,6 +318,7 @@ def split_top_level_c_source( original_end_column=column, original_source_line=_source_line(source_lines, start_line), original_end_source_line=_source_line(source_lines, line), + original_source_lines=_source_lines(source_lines, start_line, line), ) ) start_index = None @@ -329,6 +343,7 @@ def split_top_level_c_source( original_end_column=column, original_source_line=_source_line(source_lines, start_line), original_end_source_line=_source_line(source_lines, line), + original_source_lines=_source_lines(source_lines, start_line, line), ) ) @@ -573,4 +588,5 @@ def lex_c_source(source: str, filename: str | None = None) -> list[CToken]: "strip_c_comments", "top_level_partition", "top_level_split", + "top_level_split_with_offsets", ) diff --git a/c_parser/parser.py b/c_parser/parser.py index 2f2b1c122..dc30e037d 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -12,6 +12,7 @@ strip_c_comments, top_level_partition, top_level_split, + top_level_split_with_offsets, ) from .models import ( CArray, @@ -198,14 +199,27 @@ class CParser: declarators, aggregate declarations, typedefs, and function signatures. """ - def _source_location(self, segment: CTopLevelSegment) -> CSourceLocation: + def _source_location_at(self, segment: CTopLevelSegment, offset: int) -> CSourceLocation: + prefix = segment.text[:offset] + line_offset = prefix.count("\n") + line = segment.original_start_line + line_offset + if line_offset: + column = len(prefix.rsplit("\n", 1)[-1]) + 1 + else: + column = segment.original_start_column + len(prefix) + source_line = segment.original_source_line + if line_offset and line_offset < len(segment.original_source_lines): + source_line = segment.original_source_lines[line_offset] return CSourceLocation( filename=segment.filename, - line=segment.original_start_line, - column=segment.original_start_column, - source_line=segment.original_source_line, + line=line, + column=column, + source_line=source_line, ) + def _source_location(self, segment: CTopLevelSegment) -> CSourceLocation: + return self._source_location_at(segment, 0) + def _has_unsupported_declaration_marker(self, text: str) -> bool: return any(marker in text for marker in _UNSUPPORTED_DECLARATION_MARKERS) @@ -248,13 +262,16 @@ def _invalid_specifier_error( self, segment: CTopLevelSegment, message: str, + *, + offset: int = 0, ) -> CParseError: + location = self._source_location_at(segment, offset) return CParseError( message, - filename=segment.filename, - line_number=segment.original_start_line, - column=segment.original_start_column, - source_line=segment.original_source_line, + filename=location.filename, + line_number=location.line, + column=location.column, + source_line=location.source_line, code="CPARSE003", ) @@ -932,31 +949,79 @@ def _field_diagnostic( segment: CTopLevelSegment, owner_kind: str, message: str, + *, + offset: int = 0, ) -> CDiagnostic: return CDiagnostic( code="C_UNSUPPORTED_FIELD_DECLARATION", message=message, severity="warning", - location=self._source_location(segment), + location=self._source_location_at(segment, offset), unit_kind=f"{owner_kind}_field", unit_name=None, ) + def _incomplete_array_component(self, type_: CType) -> CArray | None: + components = type_.components if isinstance(type_, CComposedType) else [type_] + if not components or not isinstance(components[0], CArray): + return None + array = components[0] + if array.bound is None and not array.is_variable_length: + return array + return None + + def _validate_flexible_members( + self, + members: list[CVariable], + owner_kind: str, + ) -> list[CDiagnostic]: + diagnostics: list[CDiagnostic] = [] + named_members = sum(member.name is not None for member in members) + for index, member in enumerate(members): + array = self._incomplete_array_component(member.type) + if array is None: + continue + if owner_kind == "struct" and index == len(members) - 1 and named_members > 1: + array.is_flexible = True + continue + if owner_kind == "union": + message = "A union member cannot be a flexible array member." + elif index != len(members) - 1: + message = "A flexible array member must be the final member of a struct." + else: + message = "A flexible array member requires a preceding named struct member." + diagnostics.append( + CDiagnostic( + code="C_INVALID_FLEXIBLE_ARRAY_MEMBER", + message=message, + severity="error", + location=member.source_location, + unit_kind=f"{owner_kind}_field", + unit_name=member.name, + ) + ) + return diagnostics + def _parse_fields( self, body: str, segment: CTopLevelSegment, owner_kind: str, + *, + body_offset: int, ) -> tuple[list[CVariable], list[CDiagnostic]]: members: list[CVariable] = [] diagnostics: list[CDiagnostic] = [] - for text in top_level_split(body, ";"): + for text, field_offset in top_level_split_with_offsets(body, ";"): + member_offset = body_offset + field_offset + member_location = self._source_location_at(segment, member_offset) if self._has_unsupported_declaration_marker(text): diagnostics.append( self._field_diagnostic( segment, owner_kind, "Declaration attributes and alignment specifiers are not supported in fields yet.", + offset=member_offset, ) ) continue @@ -966,13 +1031,19 @@ def _parse_fields( segment, owner_kind, "Nested aggregate field definitions are not supported yet.", + offset=member_offset, ) ) continue spec_text, declarator_list = self._split_declaration_specifiers(text) if not spec_text or not declarator_list: diagnostics.append( - self._field_diagnostic(segment, owner_kind, "Unsupported field declaration.") + self._field_diagnostic( + segment, + owner_kind, + "Unsupported field declaration.", + offset=member_offset, + ) ) continue for declarator in top_level_split(declarator_list, ","): @@ -984,23 +1055,31 @@ def _parse_fields( declaration, ) except _InvalidSpecifierSequence as error: - raise self._invalid_specifier_error(segment, str(error)) from None + raise self._invalid_specifier_error(segment, str(error), offset=member_offset) from None except _UnsupportedDeclaratorSyntax as error: - diagnostics.append(self._field_diagnostic(segment, owner_kind, str(error))) + diagnostics.append( + self._field_diagnostic(segment, owner_kind, str(error), offset=member_offset) + ) continue if name is None and bit_width is None: diagnostics.append( - self._field_diagnostic(segment, owner_kind, "Unnamed field type is not supported.") + self._field_diagnostic( + segment, + owner_kind, + "Unnamed field type is not supported.", + offset=member_offset, + ) ) continue members.append( CVariable( name=name, type=type_, - source_location=self._source_location(segment), + source_location=member_location, bit_width=bit_width, ) ) + diagnostics.extend(self._validate_flexible_members(members, owner_kind)) return members, diagnostics def _parse_enumerators(self, body: str, segment: CTopLevelSegment) -> list[CEnumerator]: @@ -1060,7 +1139,12 @@ def _parse_tag_definition( source_location=location, ) else: - members, diagnostics = self._parse_fields(body, segment, kind) + members, diagnostics = self._parse_fields( + body, + segment, + kind, + body_offset=open_index + 1, + ) if kind == "struct": aggregate = CStruct( name=tag_name, diff --git a/docs/c_parser/c_parser_architecture.md b/docs/c_parser/c_parser_architecture.md index c0a1fbaa5..6d9715b30 100644 --- a/docs/c_parser/c_parser_architecture.md +++ b/docs/c_parser/c_parser_architecture.md @@ -42,7 +42,10 @@ Implemented now: Declaration types are represented by concrete `CType` subclasses: primitives, `CPointer`, `CArray`, `CFunctionType`, and `CComposedType`. Aggregate members are `CVariable` objects using that same - type path and preserve arrays, callback candidates, and bit-width text. + type path and preserve arrays, callback candidates, bit-width text, and + member-level source locations. Supported flexible final struct members set + `CArray.is_flexible=True`; invalid flexible-member placement and union use + produce `C_INVALID_FLEXIBLE_ARRAY_MEMBER` diagnostics. Inline tag definitions followed by aliases or objects produce concrete `CTypedef` or `CVariable` records linked to the aggregate object. Function models expose `result_type` and named `parameters`; their derived @@ -73,9 +76,7 @@ Deferred: - typedef/tag resolution beyond an inline aggregate declaration and callback policy metadata, for example resolving `size_t count(void);` to a prior `typedef unsigned long size_t;` -- parameter adjustment and flexible array member classification, for example - `void process(int values[4]);` and - `struct packet { unsigned size; unsigned char data[]; };` +- parameter adjustment, for example `void process(int values[4]);` - nested aggregate member definitions, braced initializers, compiler attributes, alignment specifiers, and `_Atomic(type)` declarations, for example `struct outer { struct { int x; } inner; };` and @@ -302,8 +303,8 @@ Derived and named `CType` subclasses are: - `CPointer`, whose qualifiers apply to that pointer component - `CArray`, with `bound`, `is_static_minimum`, `is_variable_length`, and - `is_flexible`; bound/static/VLA metadata is populated now, while - flexible-array-member parsing and validation are deferred + `is_flexible`; supported final flexible struct members are classified now, + and invalid placement or union use produces a parser diagnostic - `CFunctionType`, the nameless callable signature with `result_type`, `parameter_types`, `is_variadic`, and `prototype_style` - `CComposedType`, whose `components` are read from the declared name outward @@ -326,7 +327,8 @@ Declaration objects are separate from the type components: - `CVariable` has `name`, `type`, `storage`, optional `initializer`, optional `bit_width`, and source/callback metadata. Struct and union `members` are - also `CVariable` objects; there is no separate field class. + also `CVariable` objects with per-member locations; there is no separate + field class. - `CFunction` has `name`, `result_type`, named `parameters`, storage and function specifiers, `is_variadic`, prototype style, and source/definition locations. Its `type` property builds the corresponding nameless diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index 3aca190d1..6c4a8ccae 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -32,7 +32,8 @@ top-level sections: `functions`, `structs`, `unions`, `enums`, `typedefs`, can populate `functions`, `typedefs`, `variables`, `structs`, `unions`, and `enums` in the supported subset. Typedefs, variables, parameters, and aggregate members can include concrete composed types for pointer/array/function forms, -including function pointers and functions returning function pointers. Raw +including function pointers, functions returning function pointers, and +legal final flexible struct members marked with `is_flexible=True`. Raw `includes`, `macros`, and metadata `diagnostics` can also be populated. The object class distinguishes declarations (`CFunction`, `CVariable`, `CTypedef`, `CStruct`, `CUnion`, or `CEnum`), and incomplete tag @@ -40,7 +41,9 @@ declarations set `is_incomplete=True`. Known unsupported declaration forms such as declaration attributes, alignment specifiers, `_Atomic(type)`, nested aggregate member definitions, and static assertions are reported in diagnostics with explicit `unit_kind` values; unconsumed declarator -suffixes are diagnosed instead of silently omitted. The parser reports +suffixes are diagnosed instead of silently omitted. Invalid flexible array +member placement and flexible union members produce +`C_INVALID_FLEXIBLE_ARRAY_MEMBER` error diagnostics at the field location. The parser reports `parser_status: "partial"`. C parse diagnostics, currently including unsupported K&R-style function definitions and invalid primitive-specifier combinations such as `unsigned float`, honor `--no-color` and `NO_COLOR=1`. @@ -399,6 +402,9 @@ The active CLI/parser tests cover the current partial subset: signatures with definition start/end locations are covered by focused C tests. - valid reordered primitive specifiers and fatal invalid primitive-specifier combinations are covered by focused C tests. +- flexible array member classification/validation, per-member source + locations, and named/unnamed/zero-width bit-field source facts are covered + by focused C tests. - `--show-vars` and `--print-limit` are rejected in C mode until C-specific display controls exist. - `--semantics` with `--language c` is rejected until C semantic conversion is @@ -436,6 +442,8 @@ Completed order: 13. Added order-insensitive primitive specifier validation and `CPARSE003` errors for invalid primitive combinations while retaining unresolved typedef-name references for later resolution. +14. Added field-level source locations, flexible array member classification + and invalid-use diagnostics, plus explicit bit-field regression coverage. Next implementation work should continue with tag/typedef resolution, preprocessed-input line mapping, compiler extension policy, and project diff --git a/docs/c_parser/c_parser_implementation_checklist.md b/docs/c_parser/c_parser_implementation_checklist.md index 336484264..1016dceb4 100644 --- a/docs/c_parser/c_parser_implementation_checklist.md +++ b/docs/c_parser/c_parser_implementation_checklist.md @@ -13,6 +13,8 @@ array, function, and parenthesized combinations. Declaration types are concrete `CVariable` objects using the same declared-type path. Selected unsupported extensions are diagnosed, and invalid primitive-specifier combinations raise `CParseError` without treating unresolved single typedef-name uses as invalid. +Aggregate members carry their own source locations, and flexible array +members are classified and checked for supported struct/union constraints. This checklist is intentionally detailed so future work can proceed one branch, one checklist item, and one tested capability at a time. The C parser initiative @@ -22,7 +24,7 @@ stable. ## Progress Snapshot - Last updated: 2026-05-24 -- Checklist progress: 513/848 checked (60.5%). +- Checklist progress: 516/848 checked (60.8%). - Current parser status: partial C parser with raw directive metadata, top-level source splitting, simple declarations/variables/typedefs, prototype-style metadata, K&R diagnostics, simple function signatures, and start/end @@ -35,6 +37,9 @@ stable. valid spelling permutations are normalized, invalid combinations raise `CPARSE003`, and functions, variables, typedefs, and aggregates are distinguished by their concrete declaration objects rather than a kind field. + Struct and union fields now preserve per-member locations; legal final + flexible struct members are marked through `CArray.is_flexible`, with error + diagnostics for invalid placement or union use. ## Global Rules @@ -749,8 +754,6 @@ Known declaration implementation gaps, with representative syntax: - parameter array/function adjustment: `void process(int values[4], int callback(int));` -- flexible array member classification and validation: - `struct packet { unsigned size; unsigned char data[]; };` - braced/designated initializer preservation: `int values[3] = {1, 2, 3};` - nested anonymous aggregate members: @@ -764,8 +767,6 @@ Represented shapes still needing dedicated active regression tests: - multi-level qualifier placement: `const int * const * volatile chain;` -- unnamed and zero-width bit-fields: - `struct flags { unsigned : 0; unsigned mode : 3; };` ### Phase 5 Definition Of Done @@ -897,19 +898,20 @@ Scope: - [x] Parse typedef named struct `typedef struct tag name;`. - [x] Parse members with shared declaration backend. - [x] Parse pointer members. -- [x] Parse array members. +- [x] Parse array members, including legal flexible final struct members with + invalid-placement and union diagnostics. - [x] Parse nested anonymous structs as unsupported or metadata. - [x] Parse bit-fields as member metadata with semantic limitations. - [x] Preserve member order. -- [ ] Preserve precise per-member source locations; members currently carry - the enclosing aggregate declaration location. +- [x] Preserve precise per-member source locations. - [x] Mark incomplete structs. - [x] Add tests for named structs. - [x] Add tests for forward declarations. - [x] Add tests for typedef structs. - [x] Add tests for pointer members. - [x] Add tests for array members. -- [ ] Add tests for bitfield diagnostics. +- [x] Add tests for named, unnamed, and zero-width bit-field source facts and + locations; defer ABI/wrappability diagnostics to the semantic layer. ### Union Tasks @@ -957,8 +959,8 @@ Scope: - [x] C composite and typedef models are populated from basic fixtures. - [x] Shared declaration backend handles members and typedefs. -- [ ] Validate remaining bit-field/flexible-member cases beyond the basic - incomplete, anonymous, and named bit-field forms already represented. +- [x] Validate legal and invalid flexible-array-member placement and retain + named, unnamed, and zero-width bit-field source facts with tests. - [ ] JSON goldens cover composite type schema. - [x] Docs list supported and unsupported composite forms. diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index 182afefac..ca53728e7 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -77,8 +77,9 @@ Implemented: - incomplete `struct name;` and `union name;` extraction as concrete tag types with `is_incomplete=True` - named and anonymous struct/union/enum definitions -- aggregate member extraction as `CVariable` objects through the declarator backend, including pointer, - array, callback-pointer, and bitfield source facts +- aggregate member extraction as `CVariable` objects through the declarator + backend, including pointer, array, callback-pointer, flexible-array, and + bit-field source facts with per-member locations - inline tag typedef aliases and trailing tag object declarators as separate concrete models - simple function prototype extraction @@ -94,8 +95,8 @@ Still deferred: - callback policy metadata beyond parser-side callback candidates - nested aggregate member definitions and broad compiler-extension declarators -- parameter array/function adjustment, flexible-array-member validation, and - braced/designated initializer preservation +- parameter array/function adjustment and braced/designated initializer + preservation - cross-declaration and cross-file typedef/tag resolution - project include graph and cross-file type resolution - preprocessed-input parsing with `#line`/linemarker source mapping @@ -264,6 +265,10 @@ Callback-bearing parameters are marked as parser-side callback candidates, without claiming semantic wrappability. Struct and union `members` are `CVariable` objects; optional `bit_width` and `initializer` fields preserve source facts without inventing separate field or valued-variable classes. +Member records carry their own field location. A legal final incomplete array +member in a struct is marked as `CArray(is_flexible=True)`; non-final, +sole-member, and union incomplete-array member forms are retained with +`C_INVALID_FLEXIBLE_ARRAY_MEMBER` error diagnostics. Selected unsupported forms, such as static assertions, attributes, alignment specifiers, `_Atomic(type)`, and nested aggregate member definitions, are reported in `diagnostics` with explicit `unit_kind` values. @@ -452,6 +457,8 @@ Active declaration tests currently cover: - functions, variables, typedefs, struct/union members, enums, incomplete tags, inline aggregate aliases, anonymous aggregate typedefs, and recursive struct pointers +- legal and invalid flexible array members, precise field locations, and + named/unnamed/zero-width bit-field source facts - concrete-type JSON serialization, source locations, and cycle-safe aggregate references - diagnostics for selected unsupported attributes, alignment, `_Atomic(type)`, @@ -468,7 +475,6 @@ declarations. | Capability | C example | Current parser boundary | Needed behavior | | --- | --- | --- | --- | | Parameter adjustment | `void process(int values[4], int callback(int));` | Preserves the declared array and function parameter types; it does not expose C's adjusted pointer parameter type. | Keep `declared_type`, and expose the adjusted effective type (`int *` and pointer-to-function). | -| Flexible array members | `struct packet { unsigned size; unsigned char data[]; };` | Represents `data` as `CArray(bound=None)` but does not set `is_flexible` or validate that it is a legal final struct member. | Mark the flexible member and diagnose illegal placement or union usage. | | Braced/designated initializers | `int values[3] = {1, 2, 3};` and `struct point origin = {.x = 1, .y = 2};` | Simple initializer text such as `int answer = 42;` is preserved; braced forms are not reliably emitted as `CVariable` initializer facts. | Parse or preserve balanced initializer source without treating its braces as an aggregate declaration. | | Nested aggregate members | `struct outer { struct { int x; } inner; };` | Produces an unsupported-member diagnostic and does not model `inner`. | Build an anonymous `CStruct`/`CUnion` type used by the member variable. | | Typedef/tag resolution | `typedef unsigned long size_t; size_t count(void);` and `struct state { int id; }; void step(struct state *s);` | Preserves uses as unresolved `CTypedef` or incomplete tag-type objects unless attached inline. | Link uses to declarations across a file/project and diagnose conflicts. | @@ -482,13 +488,11 @@ tests before they can be treated as stable: ```c const int * const * volatile chain; -struct flags { unsigned : 0; unsigned mode : 3; }; ``` The current parser creates distinct qualified `CPointer` components for -`chain`, and creates a `CVariable(name=None, bit_width="0")` for the unnamed -zero-width bit-field. Tests should lock down those shapes and any later -semantic validation rules. +`chain`; dedicated active regression coverage for that multi-level qualifier +shape remains to be added. Fixture layout should be separate from Fortran: diff --git a/tests/parser/c/test_c_declarations_and_declarators.py b/tests/parser/c/test_c_declarations_and_declarators.py index c53d5c08e..9336967f5 100644 --- a/tests/parser/c/test_c_declarations_and_declarators.py +++ b/tests/parser/c/test_c_declarations_and_declarators.py @@ -93,22 +93,25 @@ def test_valid_reordered_primitive_specifiers_are_normalized(spelling, expected_ @pytest.mark.parametrize( - "source", + ("source", "expected_column"), [ - "unsigned float value;\n", - "void bad(long char value);\n", - "struct bad { signed unsigned value; };\n", - "unsigned float bad(void) { return 0; }\n", + ("unsigned float value;\n", 1), + ("void bad(long char value);\n", 1), + ("struct bad { signed unsigned value; };\n", 14), + ("unsigned float bad(void) { return 0; }\n", 1), ], ) -def test_invalid_primitive_specifier_sequences_raise_parse_errors(source): +def test_invalid_primitive_specifier_sequences_raise_parse_errors(source, expected_column): from c_parser import CParseError, parse_c_file with pytest.raises(CParseError, match="Invalid type specifier sequence") as error: parse_c_file(source, filename="invalid_specifiers.h") assert error.value.code == "CPARSE003" - assert "invalid_specifiers.h:1:1: error[CPARSE003]" in error.value.format_diagnostic(color=False) + assert ( + f"invalid_specifiers.h:1:{expected_column}: error[CPARSE003]" + in error.value.format_diagnostic(color=False) + ) def test_unresolved_single_typedef_name_is_preserved_until_resolution(): diff --git a/tests/parser/c/test_c_structs_unions_enums_typedefs.py b/tests/parser/c/test_c_structs_unions_enums_typedefs.py index 81b0d76d7..43b7c0134 100644 --- a/tests/parser/c/test_c_structs_unions_enums_typedefs.py +++ b/tests/parser/c/test_c_structs_unions_enums_typedefs.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """C aggregate type, enum, and typedef parser tests.""" +import pytest + def test_named_struct_members_are_variables_in_source_order(): from c_parser import CArray, CComposedType, CVariable, parse_c_file @@ -166,6 +168,97 @@ def test_struct_members_use_same_components_for_callbacks_arrays_and_bitfields() assert values.type.components[0].bound == "4" +def test_struct_members_preserve_precise_locations_and_legal_flexible_array_metadata(): + from c_parser import CArray, parse_c_file + + parsed = parse_c_file( + """struct packet { + unsigned size; + unsigned char data[]; +}; +""", + filename="packet.h", + ) + + size, data = parsed.structs[0].members + assert [(member.source_location.line, member.source_location.column) for member in (size, data)] == [ + (2, 5), + (3, 5), + ] + assert data.source_location.source_line == " unsigned char data[];" + assert isinstance(data.type.components[0], CArray) + assert data.type.components[0].is_flexible is True + assert parsed.diagnostics == [] + + +@pytest.mark.parametrize( + ("source", "owner_name", "message"), + [ + ( + """struct bad { + unsigned char data[]; + int tail; +}; +""", + "structs", + "must be the final member", + ), + ( + """struct bad { + unsigned char data[]; +}; +""", + "structs", + "requires a preceding named struct member", + ), + ( + """union bad { + unsigned char data[]; + int code; +}; +""", + "unions", + "cannot be a flexible array member", + ), + ], +) +def test_invalid_flexible_array_members_are_diagnosed(source, owner_name, message): + from c_parser import parse_c_file + + parsed = parse_c_file(source, filename="invalid_flexible.h") + aggregate = getattr(parsed, owner_name)[0] + data = aggregate.members[0] + + assert data.type.components[0].is_flexible is False + assert [(diagnostic.code, diagnostic.severity) for diagnostic in parsed.diagnostics] == [ + ("C_INVALID_FLEXIBLE_ARRAY_MEMBER", "error"), + ] + assert message in parsed.diagnostics[0].message + assert parsed.diagnostics[0].location.line == 2 + assert parsed.diagnostics[0].unit_name == "data" + + +def test_unnamed_and_zero_width_bitfields_preserve_source_facts_and_locations(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """struct flags { + unsigned : 0; + unsigned mode : 3; +}; +""", + filename="bitfields.h", + ) + + zero_width, mode = parsed.structs[0].members + assert [(member.name, member.bit_width) for member in (zero_width, mode)] == [ + (None, "0"), + ("mode", "3"), + ] + assert [member.source_location.line for member in (zero_width, mode)] == [2, 3] + assert parsed.diagnostics == [] + + def test_nested_aggregate_member_definition_is_diagnosed_explicitly(): from c_parser import parse_c_file