diff --git a/c_parser/parser.py b/c_parser/parser.py index 3501abe9f..48056899a 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -78,6 +78,8 @@ "alignas", "_Atomic(", ) +_CXX_DECLARATION_KEYWORDS = {"using", "namespace", "template", "class"} +_CXX_ACCESS_SPECIFIERS = {"public", "private", "protected"} _PRIMITIVE_WORDS = { "void", "char", @@ -213,6 +215,20 @@ def _is_source_key(key: str) -> bool: return PurePosixPath(key).suffix.lower() == ".c" +def _looks_like_cxx_declaration(text: str) -> bool: + stripped = text.lstrip() + identifier = _IDENTIFIER_RE.match(stripped) + if identifier is None: + return False + + word = identifier.group(0) + if word in _CXX_DECLARATION_KEYWORDS: + return True + if word in _CXX_ACCESS_SPECIFIERS: + return stripped[identifier.end() :].lstrip().startswith(":") + return False + + class CParser: """C parser entrypoint for the currently implemented C subset. @@ -1423,6 +1439,72 @@ def _declarator_diagnostic(self, segment: CTopLevelSegment, message: str) -> CDi unit_name=None, ) + def _union_by_value_names(self, type_: CType) -> set[str]: + if isinstance(type_, CUnion): + return {type_.reference_name} + if isinstance(type_, CTypedef) and type_.type is not None: + return self._union_by_value_names(type_.type) + if isinstance(type_, CFunctionType): + names = set() + names.update(self._union_by_value_names(type_.result_type)) + for parameter_type in type_.parameter_types: + names.update(self._union_by_value_names(parameter_type)) + return names + if isinstance(type_, CComposedType): + names = set() + protected_by_indirection = False + for component in type_.components: + if isinstance(component, (CPointer, CArray)): + protected_by_indirection = True + continue + if isinstance(component, CFunctionType): + names.update(self._union_by_value_names(component)) + protected_by_indirection = False + continue + if isinstance(component, CUnion) and not protected_by_indirection: + names.add(component.reference_name) + protected_by_indirection = False + return names + return set() + + def _union_by_value_diagnostics(self, function: CFunction) -> list[CDiagnostic]: + union_names = self._union_by_value_names(function.result_type) + for parameter in function.parameters: + union_names.update(self._union_by_value_names(parameter.type)) + if not union_names: + return [] + + formatted = ", ".join(sorted(union_names)) + return [ + CDiagnostic( + code="C_UNION_BY_VALUE", + message=( + f"Function {function.name!r} uses union type(s) by value: {formatted}. " + "Use an explicit pointer or defer wrapper policy to the semantic layer." + ), + severity="warning", + location=function.source_location, + unit_kind="function", + unit_name=function.name, + ) + ] + + def _append_union_by_value_diagnostics( + self, + function: CFunction, + diagnostics: list[CDiagnostic], + ) -> None: + for diagnostic in self._union_by_value_diagnostics(function): + already_present = any( + existing.code == diagnostic.code + and existing.unit_kind == diagnostic.unit_kind + and existing.unit_name == diagnostic.unit_name + and existing.location == diagnostic.location + for existing in diagnostics + ) + if not already_present: + diagnostics.append(diagnostic) + def _field_diagnostic( self, segment: CTopLevelSegment, @@ -1676,6 +1758,7 @@ def _parse_declaration( or "}" in text or text.startswith("_Static_assert") or self._has_unsupported_declaration_marker(text) + or _looks_like_cxx_declaration(text) ): return [], [], [], [] @@ -1708,6 +1791,9 @@ def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDia if self._is_braced_initializer_declaration(segment): kind = "braced_initializer_declaration" message = "Braced or designated initializer declarations are not supported yet." + elif _looks_like_cxx_declaration(text): + kind = "cxx_declaration" + message = "C++ declaration syntax is not supported by the C parser." elif text.startswith("struct "): kind = "struct_definition" message = "Struct definitions are not supported yet." @@ -1785,6 +1871,11 @@ def _parse_translation_unit( ) ) continue + if _looks_like_cxx_declaration(segment.text): + unsupported = self._unsupported_declaration_diagnostic(segment) + if unsupported is not None: + diagnostics.append(unsupported) + continue tag_definition = self._parse_tag_definition(segment) if tag_definition is not None: aggregate, parsed_functions, parsed_typedefs, parsed_variables, parsed_diagnostics = tag_definition @@ -1812,6 +1903,7 @@ def _parse_translation_unit( continue if function is not None: functions.append(function) + self._append_union_by_value_diagnostics(function, diagnostics) continue unsupported = self._unsupported_declaration_diagnostic(segment) if unsupported is not None: @@ -1830,6 +1922,8 @@ def _parse_translation_unit( functions.extend(parsed_functions) typedefs.extend(parsed_typedefs) variables.extend(parsed_variables) + for function in parsed_functions: + self._append_union_by_value_diagnostics(function, diagnostics) diagnostics.extend(declarator_diagnostics) if ( not parsed_functions @@ -1877,6 +1971,8 @@ def _build_project(self, parsed_files: dict[str, CFile]) -> CProject: function.name: function for function in self._deduplicate_functions(all_functions, project.diagnostics) } + for function in project.functions.values(): + self._append_union_by_value_diagnostics(function, project.diagnostics) return project def _index_struct( diff --git a/docs/c_parser/c_parser_architecture.md b/docs/c_parser/c_parser_architecture.md index 31d1dd789..f5c21b13a 100644 --- a/docs/c_parser/c_parser_architecture.md +++ b/docs/c_parser/c_parser_architecture.md @@ -31,8 +31,9 @@ Implemented now: helpers that track braces, parentheses, brackets, literals, and function-definition end locations. - `c_parser.preprocessor` records raw `#include` directives, simple object-like - macros, `#undef` directives, conditional/pragma directive provenance, and - unsupported function-like macro diagnostics without expanding macros. + macros, `#undef` directives, conditional/pragma directive provenance + including OpenMP declaration pragmas, and unsupported function-like macro + diagnostics without expanding macros. - `c_parser.parser` parses variables, typedefs, incomplete `struct`/`union` tags, basic struct/union/enum definitions, function prototypes, and function-definition signatures while skipping bodies. Declarator handling @@ -46,7 +47,9 @@ Implemented now: 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. + produce `C_INVALID_FLEXIBLE_ARRAY_MEMBER` diagnostics. Function signatures + that use unions by value produce `C_UNION_BY_VALUE` diagnostics while + pointer-to-union signatures remain parsed normally. 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 @@ -56,8 +59,8 @@ Implemented now: unexpanded object-like macros are deferred as macro dependencies rather than misreported as invalid type sequences. Selected unsupported declaration forms, including attributes, alignment specifiers, - `_Atomic(type)`, nested aggregate member definitions, and static assertions, - are reported as diagnostics with + `_Atomic(type)`, C++-shaped declarations, nested aggregate member + definitions, and static assertions, are reported as diagnostics with explicit `unit_kind` values. A declarator must be fully consumed before a concrete object is returned; unknown suffixes become diagnostics. Primitive specifier order is normalized, and invalid combinations such as diff --git a/docs/c_parser/c_parser_implementation_checklist.md b/docs/c_parser/c_parser_implementation_checklist.md index c4357772d..4eac2ca0f 100644 --- a/docs/c_parser/c_parser_implementation_checklist.md +++ b/docs/c_parser/c_parser_implementation_checklist.md @@ -13,8 +13,9 @@ now parsed. Declarators use a recursive grammar-style parser for pointer, array, function, and parenthesized combinations. Declaration types are concrete `CType` subclasses combined by `CComposedType`; aggregate members are `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. +extensions and C++-shaped declarations 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. Function parameters preserve written array/function forms in `declared_type` @@ -36,7 +37,7 @@ stable. ## Progress Snapshot - Last updated: 2026-05-24 -- Checklist progress: 622/872 checked (71.3%). +- Checklist progress: 630/872 checked (72.2%). - 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 @@ -51,16 +52,19 @@ stable. 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. Array and function - parameter declarations preserve their source form in `declared_type` while - their effective `type` applies C parameter-to-pointer adjustment. Raw - conditional directives and macro-shaped declaration dependencies, including - object-like declaration prefixes, are recorded as metadata. `parse_c_project` + diagnostics for invalid placement or union use, and function signatures that + use unions by value produce conservative parser diagnostics. Array and + function parameter declarations preserve their source form in `declared_type` + while their effective `type` applies C parameter-to-pointer adjustment. Raw + conditional directives, pragmas including OpenMP declaration pragmas, and + macro-shaped declaration dependencies, including object-like declaration + prefixes, are recorded as metadata. `parse_c_project` returns project include/index facts and resolves basic cross-file typedef and tag references while preserving unresolved references for later diagnostics. Top-level compatible redeclarations are merged, matching prototypes plus definitions prefer the - definition while preserving declaration locations, and duplicate/conflicting + definition while preserving declaration locations, C++-shaped declarations + are diagnosed instead of modeled as C objects, and duplicate/conflicting top-level declarations produce diagnostics. ## Global Rules @@ -550,7 +554,7 @@ Scope: - [x] Decide whether sets serialize as sorted lists. - [x] Ensure dataclass defaults produce stable JSON. - [x] Add tests for empty `CFile` serialization. -- [ ] Add tests for each model's minimal JSON shape. +- [x] Add tests for each model's minimal JSON shape. - [x] Add tests for source-location serialization. - [x] Add tests that unknown/unresolved metadata is preserved. @@ -700,7 +704,7 @@ Scope: - [x] Decide whether to tokenize fully now or keep logical records until declarator parsing requires tokens. - [x] Decide whether system headers are recorded only or optionally searched. -- [ ] Decide whether `#pragma` should become diagnostics or metadata. +- [x] Decide whether `#pragma` should become diagnostics or metadata. - [ ] Decide whether compiler invocation belongs in Phase 4 or a later project-resolution phase. @@ -797,7 +801,7 @@ Scope: - [x] Add tests for `struct name`, `union name`, and `enum name` references in variables and parameters. - [x] Add tests for multidimensional arrays. -- [ ] Add diagnostics for declarations ignored by the current partial parser. +- [x] Add diagnostics for declarations ignored by the current partial parser. - [ ] Add structured source facts for declarations that depend on macros. ### Top-Level Redeclaration Tasks @@ -822,7 +826,7 @@ Known declaration implementation gaps, with representative syntax: - preprocessed declarations with line mapping: `#define API(ret) ret` followed by `API(int) run(void);` -Represented shapes still needing dedicated active regression tests: +Represented shapes with dedicated active regression tests: - multi-level qualifier placement: `const int * const * volatile chain;` @@ -933,9 +937,9 @@ Scope: ### Phase 6 Risks And Open Questions -- [ ] Decide whether inline functions in headers are definitions or prototypes +- [x] Decide whether inline functions in headers are definitions or prototypes for wrapper purposes. -- [ ] Decide how to handle attributes in function declarations before full +- [x] Decide how to handle attributes in function declarations before full extension support exists. ## Phase 7: Structs, Unions, Enums, And Typedefs @@ -980,10 +984,10 @@ Scope: - [x] Parse typedef unions. - [x] Parse union members with shared declaration backend. - [x] Retain union member ownership through the containing `CUnion`. -- [ ] Add diagnostics for by-value unions if unsafe. +- [x] Add diagnostics for by-value unions if unsafe. - [x] Add tests for named unions. - [x] Add tests for typedef unions. -- [ ] Add tests for union diagnostics. +- [x] Add tests for union diagnostics. ### Enum Tasks @@ -1012,7 +1016,7 @@ Scope: - [x] Add tests for typedef chains. - [x] Add tests for primitive typedefs. - [x] Add tests for opaque handle typedefs. -- [ ] Add tests for function pointer typedef diagnostics. +- [x] Add tests for function pointer typedef diagnostics. ### Phase 7 Definition Of Done diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index cd81ce893..912adbe82 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -83,6 +83,8 @@ Implemented: - 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 +- conservative parser diagnostics for function signatures that use unions by + value, while pointer-to-union signatures remain ordinary parser facts - inline tag typedef aliases and trailing tag object declarators as separate concrete models - simple function prototype extraction @@ -167,7 +169,9 @@ Raw-source mode means source normalization plus directive metadata: - record `#include` directives as structured include dependencies - record simple object-like `#define` directives as macro metadata - record `#undef` directives as macro provenance -- record conditional and pragma directives as raw provenance metadata +- record conditional and pragma directives as raw provenance metadata, + including OpenMP declaration pragmas such as `#pragma omp declare simd` and + `#pragma omp declare target` - record function-like macros as metadata with unsupported/deferred diagnostics - record function-like wrappers and object-like declaration prefixes as macro-dependency metadata without claiming they were parsed @@ -290,8 +294,9 @@ 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. +attributes, alignment specifiers, `_Atomic(type)`, C++-shaped declarations, +and nested aggregate member definitions, are reported in `diagnostics` with +explicit `unit_kind` values. Unconsumed declarator suffixes are also diagnosed instead of producing partial objects. Functions include `prototype_style`, currently `"prototype"` for @@ -506,8 +511,8 @@ Active declaration tests currently cover: - concrete-type JSON serialization, source locations, and cycle-safe aggregate references - diagnostics for selected unsupported attributes, alignment, `_Atomic(type)`, - nested aggregate definitions, K&R definitions, and trailing declarator - extensions + C++-shaped declarations, nested aggregate definitions, K&R definitions, and + trailing declarator extensions - fatal diagnostics for invalid primitive-specifier combinations while unresolved single typedef-name uses remain deferred @@ -524,18 +529,17 @@ declarations. | Preprocessed declarations | `#define API(ret) ret` followed by `API(int) run(void);` | Raw mode records macro metadata and does not claim the expanded declaration; preprocessed input with line mapping is not implemented. | Accept compiler-expanded input and map each declaration back through `#line` markers. | | Additional extension families | `int run(void) __attribute__((visibility("default")));` | Known attribute/alignment/`_Atomic(type)` forms are diagnosed; broader compiler extensions are not modeled. | Add fixture-driven support or a focused diagnostic for each required extension family. | -### Represented But Requiring Stronger Tests +### Represented With Focused Tests -These forms are not absent from the model, but need explicit active regression -tests before they can be treated as stable: +These forms are represented by the current parser and have dedicated active +regression tests: ```c const int * const * volatile chain; ``` The current parser creates distinct qualified `CPointer` components for -`chain`; dedicated active regression coverage for that multi-level qualifier -shape remains to be added. +`chain`, preserving each qualifier on the exact component it qualifies. Fixture layout should be separate from Fortran: diff --git a/tests/parser/c/fixtures/stb/stb_connected_components.json b/tests/parser/c/fixtures/stb/stb_connected_components.json index 6804bf073..6307c898a 100644 --- a/tests/parser/c/fixtures/stb/stb_connected_components.json +++ b/tests/parser/c/fixtures/stb/stb_connected_components.json @@ -6811,6 +6811,19 @@ }, "unit_kind": "union_field", "unit_name": null + }, + { + "code": "C_UNION_BY_VALUE", + "message": "Function 'stbcc__clump_find' uses union type(s) by value: union@stb/stb_connected_components.h:229:1. Use an explicit pointer or defer wrapper policy to the semantic layer.", + "severity": "warning", + "location": { + "filename": "stb/stb_connected_components.h", + "line": 321, + "column": 1, + "source_line": "static stbcc__global_clumpid stbcc__clump_find(stbcc_grid *g, stbcc__global_clumpid n)" + }, + "unit_kind": "function", + "unit_name": "stbcc__clump_find" } ] } diff --git a/tests/parser/c/fixtures/stb/stb_ds.json b/tests/parser/c/fixtures/stb/stb_ds.json index 65c97b266..2613585bc 100644 --- a/tests/parser/c/fixtures/stb/stb_ds.json +++ b/tests/parser/c/fixtures/stb/stb_ds.json @@ -7944,8 +7944,8 @@ "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -7953,12 +7953,12 @@ "column": 1, "source_line": "template static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -7966,12 +7966,12 @@ "column": 1, "source_line": "template static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -7979,12 +7979,12 @@ "column": 1, "source_line": "template static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmput_default_wrapper(T *a, size_t elemsize)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -7992,12 +7992,12 @@ "column": 1, "source_line": "template static T * stbds_hmput_default_wrapper(T *a, size_t elemsize) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -8005,12 +8005,12 @@ "column": 1, "source_line": "template static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -8018,7 +8018,7 @@ "column": 1, "source_line": "template static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode){" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { @@ -13614,8 +13614,8 @@ "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -13623,12 +13623,12 @@ "column": 1, "source_line": "template static T * stbds_arrgrowf_wrapper(T *a, size_t elemsize, size_t addlen, size_t min_cap) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -13636,12 +13636,12 @@ "column": 1, "source_line": "template static T * stbds_hmget_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -13649,12 +13649,12 @@ "column": 1, "source_line": "template static T * stbds_hmget_key_ts_wrapper(T *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmput_default_wrapper(T *a, size_t elemsize)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -13662,12 +13662,12 @@ "column": 1, "source_line": "template static T * stbds_hmput_default_wrapper(T *a, size_t elemsize) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -13675,12 +13675,12 @@ "column": 1, "source_line": "template static T * stbds_hmput_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, int mode) {" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATOR", - "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode)'.", + "code": "C_UNSUPPORTED_DECLARATION", + "message": "C++ declaration syntax is not supported by the C parser.", "severity": "warning", "location": { "filename": "stb/stb_ds.h", @@ -13688,7 +13688,7 @@ "column": 1, "source_line": "template static T * stbds_hmdel_key_wrapper(T *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode){" }, - "unit_kind": "declarator", + "unit_kind": "cxx_declaration", "unit_name": null }, { diff --git a/tests/parser/c/test_c_declarations_and_declarators.py b/tests/parser/c/test_c_declarations_and_declarators.py index 2814d8660..8f0440882 100644 --- a/tests/parser/c/test_c_declarations_and_declarators.py +++ b/tests/parser/c/test_c_declarations_and_declarators.py @@ -143,6 +143,22 @@ def test_pointer_qualifiers_belong_to_the_component_they_qualify(): assert dst.components[0].qualifiers == [CRestrict()] +def test_multi_level_qualifiers_stay_on_their_exact_type_components(): + from c_parser import CComposedType, CConst, CInt, CPointer, CVolatile, parse_c_file + + parsed = parse_c_file( + "const int * const * volatile chain;\n", + filename="multi_level_qualifiers.h", + ) + + chain = parsed.variables[0].type + assert isinstance(chain, CComposedType) + assert [type(component) for component in chain.components] == [CPointer, CPointer, CInt] + assert chain.components[0].qualifiers == [CVolatile()] + assert chain.components[1].qualifiers == [CConst()] + assert chain.components[2].qualifiers == [CConst()] + + def test_array_parameters_preserve_declarations_and_expose_adjusted_pointer_types(): from c_parser import CArray, CComposedType, CConst, CDouble, CInt, CPointer, parse_c_file @@ -456,6 +472,22 @@ def test_function_type_discards_placeholder_parameter_names(): assert not hasattr(signature, "parameters") +def test_conflicting_function_pointer_typedefs_report_diagnostic(): + from c_parser import parse_c_file + + parsed = parse_c_file( + "typedef int (*callback_fn)(int);\n" + "typedef double (*callback_fn)(double);\n", + filename="callback_typedef_conflict.h", + ) + + assert [typedef.name for typedef in parsed.typedefs] == ["callback_fn"] + assert [ + (diagnostic.code, diagnostic.unit_kind, diagnostic.unit_name) + for diagnostic in parsed.diagnostics + ] == [("C_CONFLICTING_TYPEDEF", "typedef", "callback_fn")] + + def test_recursive_compositions_cover_tables_callback_arrays_and_function_results(): from c_parser import CArray, CFunctionType, CInt, CPointer, parse_c_file @@ -509,6 +541,35 @@ def test_unimplemented_declaration_extensions_are_diagnosed_not_partially_modele ] +@pytest.mark.parametrize( + "source", + [ + "using size_type = int;\n", + "using namespace api;\n", + "namespace api { int run(void); }\n", + "namespace api = other;\n", + "template T identity(T value);\n", + "class widget;\n", + "public:\n", + ], +) +def test_cxx_declaration_shapes_are_diagnosed_not_partially_modeled(source): + from c_parser import parse_c_file + + parsed = parse_c_file(source, filename="cxx_shapes.h") + + assert parsed.functions == [] + assert parsed.structs == [] + assert parsed.unions == [] + assert parsed.enums == [] + assert parsed.typedefs == [] + assert parsed.variables == [] + assert [ + (diagnostic.code, diagnostic.unit_kind, diagnostic.location.line) + for diagnostic in parsed.diagnostics + ] == [("C_UNSUPPORTED_DECLARATION", "cxx_declaration", 1)] + + def test_braced_initializer_declarations_are_diagnosed_not_declarator_failures(): from c_parser import parse_c_file diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index 9131d368e..4167e8bb3 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -183,6 +183,42 @@ def test_matching_prototype_and_definition_merge_and_prefer_definition(): assert parsed.diagnostics == [] +def test_inline_function_body_in_header_is_recorded_as_definition(): + from c_parser import parse_c_file + + parsed = parse_c_file( + "static inline int add_one(int value) { return value + 1; }\n", + filename="inline_api.h", + ) + + function = parsed.functions[0] + assert function.name == "add_one" + assert function.storage == ["static"] + assert function.specifiers == ["inline"] + assert function.is_definition is True + assert function.start.line == 1 + assert function.end.line == 1 + + +def test_function_declaration_attributes_are_diagnosed_until_extension_support_lands(): + from c_parser import parse_c_file + + parsed = parse_c_file( + 'int exported(void) __attribute__((visibility("default")));\n' + "int deprecated(void) [[deprecated]];\n", + filename="function_attributes.h", + ) + + assert parsed.functions == [] + assert [ + (diagnostic.code, diagnostic.unit_kind, diagnostic.location.line) + for diagnostic in parsed.diagnostics + ] == [ + ("C_UNSUPPORTED_DECLARATION", "attribute_declaration", 1), + ("C_UNSUPPORTED_DECLARATION", "attribute_declaration", 2), + ] + + def test_conflicting_function_prototypes_report_diagnostic(): from c_parser import parse_c_file diff --git a/tests/parser/c/test_c_lexer_preprocessor.py b/tests/parser/c/test_c_lexer_preprocessor.py index 8f73b24fa..aff9ad487 100644 --- a/tests/parser/c/test_c_lexer_preprocessor.py +++ b/tests/parser/c/test_c_lexer_preprocessor.py @@ -199,6 +199,53 @@ def test_raw_conditional_directives_do_not_select_active_branches(): ] +def test_raw_mode_records_pragmas_as_metadata_without_hiding_declarations(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +#pragma once +#pragma GCC diagnostic push +int configured(void); +""", + filename="pragmas.h", + preprocessing="raw", + ) + + assert [fn.name for fn in parsed.functions] == ["configured"] + assert [(item.directive, item.argument) for item in parsed.raw_directives] == [ + ("pragma", "once"), + ("pragma", "GCC diagnostic push"), + ] + assert parsed.raw_directives[0].source_location.line == 2 + + +def test_raw_mode_openmp_declaration_pragmas_do_not_hide_declarations(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +#pragma omp declare simd +int saxpy(int n, const float *x, float *y); + +#pragma omp declare target +extern int omp_global; +int omp_helper(void); +#pragma omp end declare target +""", + filename="openmp_pragmas.h", + preprocessing="raw", + ) + + assert [fn.name for fn in parsed.functions] == ["saxpy", "omp_helper"] + assert [variable.name for variable in parsed.variables] == ["omp_global"] + assert [(item.directive, item.argument) for item in parsed.raw_directives] == [ + ("pragma", "omp declare simd"), + ("pragma", "omp declare target"), + ("pragma", "omp end declare target"), + ] + + def test_raw_mode_records_macro_dependency_metadata_for_macro_shaped_declarations(): from c_parser import parse_c_file diff --git a/tests/parser/c/test_c_model_serialization.py b/tests/parser/c/test_c_model_serialization.py new file mode 100644 index 000000000..29be555d2 --- /dev/null +++ b/tests/parser/c/test_c_model_serialization.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +"""Minimal JSON-shape coverage for C parser model dataclasses.""" + +from dataclasses import is_dataclass +import inspect + +import c_parser.models as models + + +def _type_payload(model: str, **extra): + payload = {"model": model, "qualifiers": [], "source_text": ""} + payload.update(extra) + return payload + + +def test_each_c_model_has_minimal_json_shape(): + cases = { + "CSourceLocation": ( + models.CSourceLocation(), + {"filename": None, "line": None, "column": None, "source_line": None}, + ), + "CDiagnostic": ( + models.CDiagnostic(code="C_TEST", message="test message"), + { + "code": "C_TEST", + "message": "test message", + "severity": "warning", + "location": None, + "unit_kind": None, + "unit_name": None, + }, + ), + "CQualifier": (models.CQualifier("const"), "const"), + "CConst": (models.CConst(), "const"), + "CVolatile": (models.CVolatile(), "volatile"), + "CRestrict": (models.CRestrict(), "restrict"), + "CAtomic": (models.CAtomic(), "_Atomic"), + "CType": (models.CType(), _type_payload("CType")), + "CUnknownType": (models.CUnknownType(), _type_payload("CUnknownType", spelling="unknown")), + "CVoid": (models.CVoid(), _type_payload("CVoid")), + "CBool": (models.CBool(), _type_payload("CBool")), + "CChar": (models.CChar(), _type_payload("CChar")), + "CSignedChar": (models.CSignedChar(), _type_payload("CSignedChar")), + "CUnsignedChar": (models.CUnsignedChar(), _type_payload("CUnsignedChar")), + "CShort": (models.CShort(), _type_payload("CShort")), + "CUnsignedShort": (models.CUnsignedShort(), _type_payload("CUnsignedShort")), + "CInt": (models.CInt(), _type_payload("CInt")), + "CUnsignedInt": (models.CUnsignedInt(), _type_payload("CUnsignedInt")), + "CLong": (models.CLong(), _type_payload("CLong")), + "CUnsignedLong": (models.CUnsignedLong(), _type_payload("CUnsignedLong")), + "CLongLong": (models.CLongLong(), _type_payload("CLongLong")), + "CUnsignedLongLong": (models.CUnsignedLongLong(), _type_payload("CUnsignedLongLong")), + "CFloat": (models.CFloat(), _type_payload("CFloat")), + "CDouble": (models.CDouble(), _type_payload("CDouble")), + "CLongDouble": (models.CLongDouble(), _type_payload("CLongDouble")), + "CFloatComplex": (models.CFloatComplex(), _type_payload("CFloatComplex")), + "CDoubleComplex": (models.CDoubleComplex(), _type_payload("CDoubleComplex")), + "CLongDoubleComplex": (models.CLongDoubleComplex(), _type_payload("CLongDoubleComplex")), + "CPointer": (models.CPointer(), _type_payload("CPointer")), + "CArray": ( + models.CArray(), + _type_payload( + "CArray", + bound=None, + is_static_minimum=False, + is_variable_length=False, + is_flexible=False, + ), + ), + "CFunctionType": ( + models.CFunctionType(), + _type_payload( + "CFunctionType", + result_type=_type_payload("CVoid"), + parameter_types=[], + is_variadic=False, + prototype_style=None, + ), + ), + "CComposedType": ( + models.CComposedType(), + _type_payload("CComposedType", components=[]), + ), + "CParameter": ( + models.CParameter(), + { + "name": None, + "type": _type_payload("CVoid"), + "declared_type": None, + "source_location": None, + "callback_policy": None, + }, + ), + "CFunction": ( + models.CFunction(name="run"), + { + "name": "run", + "result_type": _type_payload("CVoid"), + "parameters": [], + "storage": [], + "specifiers": [], + "is_variadic": False, + "is_definition": False, + "prototype_style": None, + "source_location": None, + "start": None, + "end": None, + "declaration_locations": [], + }, + ), + "CStruct": ( + models.CStruct(), + _type_payload( + "CStruct", + name=None, + members=[], + anonymous_id=None, + is_incomplete=False, + source_location=None, + ), + ), + "CUnion": ( + models.CUnion(), + _type_payload( + "CUnion", + name=None, + members=[], + anonymous_id=None, + is_incomplete=False, + source_location=None, + ), + ), + "CEnumerator": ( + models.CEnumerator(name="STATUS_OK"), + {"name": "STATUS_OK", "value": None, "source_location": None}, + ), + "CEnum": ( + models.CEnum(), + _type_payload("CEnum", name=None, constants=[], anonymous_id=None, source_location=None), + ), + "CTypedef": ( + models.CTypedef(name="api_int"), + _type_payload( + "CTypedef", + name="api_int", + type=None, + source_location=None, + declaration_locations=[], + ), + ), + "CInitializer": (models.CInitializer(source_text="42"), {"source_text": "42"}), + "CVariable": ( + models.CVariable(name="value"), + { + "name": "value", + "type": _type_payload("CVoid"), + "storage": [], + "initializer": None, + "bit_width": None, + "source_location": None, + "callback_policy": None, + "declaration_locations": [], + }, + ), + "CMacro": ( + models.CMacro(name="API"), + { + "name": "API", + "value": None, + "function_like": False, + "directive": "define", + "source_location": None, + }, + ), + "CRawDirective": ( + models.CRawDirective(directive="include"), + {"directive": "include", "argument": None, "source_location": None}, + ), + "CMacroDependency": ( + models.CMacroDependency(name="API"), + { + "name": "API", + "context": "declaration", + "source_location": None, + "source_text": "", + }, + ), + "CInclude": ( + models.CInclude(target="api.h"), + {"target": "api.h", "kind": "local", "resolved_path": None, "source_location": None}, + ), + "CFile": ( + models.CFile(filename="api.h"), + { + "filename": "api.h", + "language": "c", + "parser_status": "partial", + "preprocessing": "raw", + "functions": [], + "structs": [], + "unions": [], + "enums": [], + "typedefs": [], + "variables": [], + "macros": [], + "includes": [], + "raw_directives": [], + "macro_dependencies": [], + "diagnostics": [], + }, + ), + "CProject": ( + models.CProject(), + { + "files": {}, + "functions": {}, + "structs": {}, + "unions": {}, + "enums": {}, + "typedefs": {}, + "variables": {}, + "macros": {}, + "includes": {}, + "functions_by_file": {}, + "enum_constants": {}, + "include_graph": {}, + "system_includes": {}, + "unresolved_includes": {}, + "header_source_pairs": {}, + "diagnostics": [], + }, + ), + } + + model_dataclasses = { + name + for name, obj in inspect.getmembers(models, inspect.isclass) + if obj.__module__ == models.__name__ and is_dataclass(obj) + } + assert set(cases) == model_dataclasses + + for model_name, (model, expected) in cases.items(): + assert models.c_model_to_dict(model) == expected, model_name 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 aa56c04a5..bd0f35826 100644 --- a/tests/parser/c/test_c_structs_unions_enums_typedefs.py +++ b/tests/parser/c/test_c_structs_unions_enums_typedefs.py @@ -90,6 +90,51 @@ def test_anonymous_union_typedef_refers_to_the_concrete_union_object(): assert parsed.typedefs[0].type is parsed.unions[0] +def test_function_signatures_using_unions_by_value_report_diagnostics(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +union value { int i; double d; }; +int consume(union value value); +union value make_value(void); +void consume_pointer(union value *value); +""", + filename="union_by_value.h", + ) + + assert [function.name for function in parsed.functions] == [ + "consume", + "make_value", + "consume_pointer", + ] + assert [ + (diagnostic.code, diagnostic.unit_kind, diagnostic.unit_name, diagnostic.location.line) + for diagnostic in parsed.diagnostics + ] == [ + ("C_UNION_BY_VALUE", "function", "consume", 3), + ("C_UNION_BY_VALUE", "function", "make_value", 4), + ] + + +def test_project_reports_union_by_value_through_resolved_typedefs(): + from c_parser import parse_c_project + + project = parse_c_project( + { + "value.h": "typedef union value { int i; } value_t;\n", + "api.h": "value_t consume_alias(value_t value);\nvoid consume_alias_pointer(value_t *value);\n", + } + ) + + assert set(project.functions) == {"consume_alias", "consume_alias_pointer"} + assert [ + (diagnostic.code, diagnostic.unit_kind, diagnostic.unit_name, diagnostic.location.line) + for diagnostic in project.diagnostics + if diagnostic.code == "C_UNION_BY_VALUE" + ] == [("C_UNION_BY_VALUE", "function", "consume_alias", 1)] + + def test_incomplete_union_and_tag_typedef_aliases_use_concrete_tag_classes(): from c_parser import CStruct, CUnion, parse_c_file