diff --git a/c_parser/__init__.py b/c_parser/__init__.py index 36e5ac6d4..04e582071 100644 --- a/c_parser/__init__.py +++ b/c_parser/__init__.py @@ -26,11 +26,13 @@ CLongDoubleComplex, CLongLong, CMacro, + CMacroDependency, CParameter, CParseError, CPointer, CProject, CQualifier, + CRawDirective, CRestrict, CShort, CSignedChar, @@ -76,12 +78,14 @@ "CLongDoubleComplex", "CLongLong", "CMacro", + "CMacroDependency", "CParameter", "CParseError", "CPointer", "CParser", "CProject", "CQualifier", + "CRawDirective", "CRestrict", "CShort", "CSignedChar", diff --git a/c_parser/models.py b/c_parser/models.py index 6698db245..3c6ec5f1e 100644 --- a/c_parser/models.py +++ b/c_parser/models.py @@ -367,6 +367,7 @@ class CFunction: source_location: CSourceLocation | None = None start: CSourceLocation | None = None end: CSourceLocation | None = None + declaration_locations: list[CSourceLocation] = field(default_factory=list) @property def type(self) -> CFunctionType: @@ -428,6 +429,7 @@ class CTypedef(CType): name: str type: CType | None = None source_location: CSourceLocation | None = None + declaration_locations: list[CSourceLocation] = field(default_factory=list) @property def reference_name(self) -> str: @@ -448,6 +450,7 @@ class CVariable: bit_width: str | None = None source_location: CSourceLocation | None = None callback_policy: Any = None + declaration_locations: list[CSourceLocation] = field(default_factory=list) @property def callback_candidate(self) -> bool: @@ -463,6 +466,20 @@ class CMacro: source_location: CSourceLocation | None = None +@dataclass +class CRawDirective: + directive: str + argument: str | None = None + source_location: CSourceLocation | None = None + + +@dataclass +class CMacroDependency: + name: str + context: str = "declaration" + source_location: CSourceLocation | None = None + + @dataclass class CInclude: target: str @@ -485,6 +502,8 @@ class CFile: variables: list[CVariable] = field(default_factory=list) macros: list[CMacro] = field(default_factory=list) includes: list[CInclude] = field(default_factory=list) + raw_directives: list[CRawDirective] = field(default_factory=list) + macro_dependencies: list[CMacroDependency] = field(default_factory=list) diagnostics: list[CDiagnostic] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: @@ -502,6 +521,13 @@ class CProject: variables: dict[str, CVariable] = field(default_factory=dict) macros: dict[str, CMacro] = field(default_factory=dict) includes: dict[str, CInclude] = field(default_factory=dict) + functions_by_file: dict[str, list[str]] = field(default_factory=dict) + enum_constants: dict[str, CEnumerator] = field(default_factory=dict) + include_graph: dict[str, set[str]] = field(default_factory=dict) + system_includes: dict[str, set[str]] = field(default_factory=dict) + unresolved_includes: dict[str, set[str]] = field(default_factory=dict) + header_source_pairs: dict[str, set[str]] = field(default_factory=dict) + diagnostics: list[CDiagnostic] = field(default_factory=list) def to_dict(self) -> dict[str, Any]: return c_model_to_dict(self) diff --git a/c_parser/parser.py b/c_parser/parser.py index 41fde5d6b..fe140f259 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -4,7 +4,7 @@ from dataclasses import dataclass import re from collections.abc import Mapping, Sequence -from pathlib import Path +from pathlib import Path, PurePosixPath from .lexer import ( CTopLevelSegment, @@ -58,8 +58,10 @@ CLongDouble, CLongDoubleComplex, CLongLong, + CMacroDependency, ) from .preprocessor import collect_preprocessor_metadata +from .type_resolver import resolve_project_types _C_SOURCE_SUFFIXES = {".c", ".h"} @@ -192,6 +194,25 @@ def _collect_c_paths(path: Path) -> list[Path]: ) +def _posix_key(path: str | Path | PurePosixPath) -> str: + return PurePosixPath(str(path)).as_posix() + + +def _include_key_from_current(current_key: str, target: str) -> str: + current_parent = PurePosixPath(current_key).parent + if str(current_parent) == ".": + return _posix_key(target) + return _posix_key(current_parent / target) + + +def _is_header_key(key: str) -> bool: + return PurePosixPath(key).suffix.lower() == ".h" + + +def _is_source_key(key: str) -> bool: + return PurePosixPath(key).suffix.lower() == ".c" + + class CParser: """C parser entrypoint for the currently implemented C subset. @@ -220,9 +241,386 @@ def _source_location_at(self, segment: CTopLevelSegment, offset: int) -> CSource def _source_location(self, segment: CTopLevelSegment) -> CSourceLocation: return self._source_location_at(segment, 0) + def _macro_dependencies( + self, + source: str, + filename: str | None, + macro_names: set[str], + ) -> list[CMacroDependency]: + dependencies: list[CMacroDependency] = [] + if not macro_names: + return dependencies + + for segment in split_top_level_c_source(source, filename=filename): + text = segment.text.strip() + if not text: + continue + for macro_name in sorted(macro_names): + if re.search(rf"\b{re.escape(macro_name)}\s*\(", text): + dependencies.append( + CMacroDependency( + name=macro_name, + context="declaration", + source_location=self._source_location(segment), + ) + ) + break + return dependencies + def _has_unsupported_declaration_marker(self, text: str) -> bool: return any(marker in text for marker in _UNSUPPORTED_DECLARATION_MARKERS) + def _redeclaration_diagnostic( + self, + code: str, + message: str, + location: CSourceLocation | None, + unit_kind: str, + unit_name: str | None, + ) -> CDiagnostic: + return CDiagnostic( + code=code, + message=message, + severity="error", + location=location, + unit_kind=unit_kind, + unit_name=unit_name, + ) + + def _append_declaration_location( + self, + locations: list[CSourceLocation], + location: CSourceLocation | None, + ) -> None: + if location is not None and location not in locations: + locations.append(location) + + def _type_key(self, type_: CType, seen: set[int] | None = None) -> tuple: + if seen is None: + seen = set() + object_id = id(type_) + if object_id in seen: + return ("cycle", type(type_).__name__, getattr(type_, "reference_name", None)) + seen.add(object_id) + + qualifiers = tuple(qualifier.spelling for qualifier in getattr(type_, "qualifiers", [])) + if isinstance(type_, CComposedType): + return ( + "CComposedType", + tuple(self._type_key(component, seen) for component in type_.components), + qualifiers, + ) + if isinstance(type_, CFunctionType): + return ( + "CFunctionType", + self._type_key(type_.result_type, seen), + tuple(self._type_key(parameter_type, seen) for parameter_type in type_.parameter_types), + type_.is_variadic, + qualifiers, + ) + if isinstance(type_, CPointer): + return ("CPointer", qualifiers) + if isinstance(type_, CArray): + return ( + "CArray", + type_.bound, + type_.is_static_minimum, + type_.is_variable_length, + type_.is_flexible, + qualifiers, + ) + if isinstance(type_, CTypedef): + if type_.type is not None: + return ("CTypedef", self._type_key(type_.type, seen), qualifiers) + return ("CTypedef", type_.name, qualifiers) + if isinstance(type_, CStruct): + return ("CStruct", type_.name, type_.anonymous_id, qualifiers) + if isinstance(type_, CUnion): + return ("CUnion", type_.name, type_.anonymous_id, qualifiers) + if isinstance(type_, CEnum): + return ("CEnum", type_.name, type_.anonymous_id, qualifiers) + return (type(type_).__name__, qualifiers) + + def _types_compatible(self, left: CType, right: CType) -> bool: + return self._type_key(left) == self._type_key(right) + + def _unspecified_function_declaration(self, function: CFunction) -> bool: + return function.prototype_style == "unspecified" and not function.parameters + + def _functions_compatible(self, left: CFunction, right: CFunction) -> bool: + if not self._types_compatible(left.result_type, right.result_type): + return False + if self._unspecified_function_declaration(left) or self._unspecified_function_declaration(right): + return True + if left.is_variadic != right.is_variadic or len(left.parameters) != len(right.parameters): + return False + return all( + self._types_compatible(left_param.type, right_param.type) + for left_param, right_param in zip(left.parameters, right.parameters) + ) + + def _merge_function_declaration( + self, + existing: CFunction, + incoming: CFunction, + ) -> CFunction: + if incoming.is_definition and not existing.is_definition: + merged = incoming + for location in existing.declaration_locations: + self._append_declaration_location(merged.declaration_locations, location) + self._append_declaration_location(merged.declaration_locations, existing.source_location) + return merged + + for location in incoming.declaration_locations: + self._append_declaration_location(existing.declaration_locations, location) + self._append_declaration_location(existing.declaration_locations, incoming.source_location) + return existing + + def _deduplicate_functions( + self, + functions: list[CFunction], + diagnostics: list[CDiagnostic], + ) -> list[CFunction]: + by_name: dict[str, CFunction] = {} + order: list[str] = [] + + for function in functions: + existing = by_name.get(function.name) + if existing is None: + by_name[function.name] = function + order.append(function.name) + continue + + if not self._functions_compatible(existing, function): + diagnostics.append( + self._redeclaration_diagnostic( + "C_CONFLICTING_FUNCTION_DECLARATION", + f"Conflicting declarations for function {function.name!r}.", + function.source_location, + "function", + function.name, + ) + ) + continue + + if existing.is_definition and function.is_definition: + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_FUNCTION_DEFINITION", + f"Duplicate definition for function {function.name!r}.", + function.source_location, + "function", + function.name, + ) + ) + continue + + by_name[function.name] = self._merge_function_declaration(existing, function) + + return [by_name[name] for name in order] + + def _is_variable_definition(self, variable: CVariable) -> bool: + return variable.initializer is not None + + def _merge_variable_declaration( + self, + existing: CVariable, + incoming: CVariable, + ) -> CVariable: + if self._is_variable_definition(incoming) and not self._is_variable_definition(existing): + merged = incoming + for location in existing.declaration_locations: + self._append_declaration_location(merged.declaration_locations, location) + self._append_declaration_location(merged.declaration_locations, existing.source_location) + return merged + + for location in incoming.declaration_locations: + self._append_declaration_location(existing.declaration_locations, location) + self._append_declaration_location(existing.declaration_locations, incoming.source_location) + return existing + + def _deduplicate_variables( + self, + variables: list[CVariable], + diagnostics: list[CDiagnostic], + ) -> list[CVariable]: + by_name: dict[str, CVariable] = {} + order: list[str] = [] + + for variable in variables: + if variable.name is None: + continue + existing = by_name.get(variable.name) + if existing is None: + by_name[variable.name] = variable + order.append(variable.name) + continue + + if not self._types_compatible(existing.type, variable.type): + diagnostics.append( + self._redeclaration_diagnostic( + "C_CONFLICTING_VARIABLE_DECLARATION", + f"Conflicting declarations for variable {variable.name!r}.", + variable.source_location, + "variable", + variable.name, + ) + ) + continue + + if self._is_variable_definition(existing) and self._is_variable_definition(variable): + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_VARIABLE_DEFINITION", + f"Duplicate definition for variable {variable.name!r}.", + variable.source_location, + "variable", + variable.name, + ) + ) + continue + + by_name[variable.name] = self._merge_variable_declaration(existing, variable) + + return [by_name[name] for name in order] + + def _deduplicate_typedefs( + self, + typedefs: list[CTypedef], + diagnostics: list[CDiagnostic], + ) -> list[CTypedef]: + by_name: dict[str, CTypedef] = {} + order: list[str] = [] + + for typedef in typedefs: + existing = by_name.get(typedef.name) + if existing is None: + by_name[typedef.name] = typedef + order.append(typedef.name) + continue + + if existing.type is None or typedef.type is None or not self._types_compatible(existing.type, typedef.type): + diagnostics.append( + self._redeclaration_diagnostic( + "C_CONFLICTING_TYPEDEF", + f"Conflicting typedef declarations for {typedef.name!r}.", + typedef.source_location, + "typedef", + typedef.name, + ) + ) + continue + + for location in typedef.declaration_locations: + self._append_declaration_location(existing.declaration_locations, location) + self._append_declaration_location(existing.declaration_locations, typedef.source_location) + + return [by_name[name] for name in order] + + def _deduplicate_structs( + self, + structs: list[CStruct], + diagnostics: list[CDiagnostic], + ) -> list[CStruct]: + by_name: dict[str, CStruct] = {} + ordered: list[CStruct] = [] + + for struct in structs: + if struct.name is None: + ordered.append(struct) + continue + existing = by_name.get(struct.name) + if existing is None: + by_name[struct.name] = struct + ordered.append(struct) + continue + if existing.is_incomplete and not struct.is_incomplete: + index = ordered.index(existing) + ordered[index] = struct + by_name[struct.name] = struct + continue + if not existing.is_incomplete and not struct.is_incomplete: + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_TAG_DEFINITION", + f"Duplicate definition for struct tag {struct.name!r}.", + struct.source_location, + "struct", + struct.name, + ) + ) + return ordered + + def _deduplicate_unions( + self, + unions: list[CUnion], + diagnostics: list[CDiagnostic], + ) -> list[CUnion]: + by_name: dict[str, CUnion] = {} + ordered: list[CUnion] = [] + + for union in unions: + if union.name is None: + ordered.append(union) + continue + existing = by_name.get(union.name) + if existing is None: + by_name[union.name] = union + ordered.append(union) + continue + if existing.is_incomplete and not union.is_incomplete: + index = ordered.index(existing) + ordered[index] = union + by_name[union.name] = union + continue + if not existing.is_incomplete and not union.is_incomplete: + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_TAG_DEFINITION", + f"Duplicate definition for union tag {union.name!r}.", + union.source_location, + "union", + union.name, + ) + ) + return ordered + + def _deduplicate_enums( + self, + enums: list[CEnum], + diagnostics: list[CDiagnostic], + ) -> list[CEnum]: + by_name: dict[str, CEnum] = {} + ordered: list[CEnum] = [] + + for enum in enums: + if enum.name is None: + ordered.append(enum) + continue + existing = by_name.get(enum.name) + if existing is None: + by_name[enum.name] = enum + ordered.append(enum) + continue + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_TAG_DEFINITION", + f"Duplicate definition for enum tag {enum.name!r}.", + enum.source_location, + "enum", + enum.name, + ) + ) + return ordered + + def _normalize_redeclarations(self, parsed: CFile) -> None: + parsed.structs = self._deduplicate_structs(parsed.structs, parsed.diagnostics) + parsed.unions = self._deduplicate_unions(parsed.unions, parsed.diagnostics) + parsed.enums = self._deduplicate_enums(parsed.enums, parsed.diagnostics) + parsed.typedefs = self._deduplicate_typedefs(parsed.typedefs, parsed.diagnostics) + parsed.variables = self._deduplicate_variables(parsed.variables, parsed.diagnostics) + parsed.functions = self._deduplicate_functions(parsed.functions, parsed.diagnostics) + def _end_location(self, segment: CTopLevelSegment) -> CSourceLocation: return CSourceLocation( filename=segment.filename, @@ -930,7 +1328,14 @@ def _declarations_from_declarators( type_ = self._use_aggregate_definition(type_, resolved) location = self._source_location(segment) if "typedef" in storage: - typedefs.append(CTypedef(name=name, type=type_, source_location=location)) + typedefs.append( + CTypedef( + name=name, + type=type_, + source_location=location, + source_text=name, + ) + ) elif isinstance(type_, CFunctionType) and direct_function is not None: functions.append( self._function_from_type( @@ -1345,28 +1750,179 @@ def _parse_translation_unit( def _build_project(self, parsed_files: dict[str, CFile]) -> CProject: project = CProject(files=parsed_files) - for file in parsed_files.values(): - for function in file.functions: - project.functions[function.name] = function + all_functions: list[CFunction] = [] + for filename, file in parsed_files.items(): + project.functions_by_file[filename] = [function.name for function in file.functions] + all_functions.extend(file.functions) for struct in file.structs: if struct.name is not None: - project.structs[struct.name] = struct + self._index_struct(project, struct, project.diagnostics) for union in file.unions: if union.name is not None: - project.unions[union.name] = union + self._index_union(project, union, project.diagnostics) for enum in file.enums: if enum.name is not None: - project.enums[enum.name] = enum + self._index_enum(project, enum, project.diagnostics) + for constant in enum.constants: + project.enum_constants[constant.name] = constant for typedef in file.typedefs: project.typedefs[typedef.name] = typedef for variable in file.variables: - project.variables[variable.name] = variable + if variable.name is not None: + project.variables[variable.name] = variable for macro in file.macros: project.macros[macro.name] = macro for include in file.includes: - project.includes[f"{file.filename or ''}:{include.target}"] = include + project.includes[f"{filename}:{include.target}"] = include + project.diagnostics.extend(file.diagnostics) + self._index_file_includes(project, filename, file) + self._index_header_source_pairs(project) + resolve_project_types(project) + project.functions = { + function.name: function + for function in self._deduplicate_functions(all_functions, project.diagnostics) + } return project + def _index_struct( + self, + project: CProject, + struct: CStruct, + diagnostics: list[CDiagnostic], + ) -> None: + if struct.name is None: + return + existing = project.structs.get(struct.name) + if existing is None or (existing.is_incomplete and not struct.is_incomplete): + project.structs[struct.name] = struct + elif not existing.is_incomplete and not struct.is_incomplete: + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_TAG_DEFINITION", + f"Duplicate definition for struct tag {struct.name!r}.", + struct.source_location, + "struct", + struct.name, + ) + ) + + def _index_union( + self, + project: CProject, + union: CUnion, + diagnostics: list[CDiagnostic], + ) -> None: + if union.name is None: + return + existing = project.unions.get(union.name) + if existing is None or (existing.is_incomplete and not union.is_incomplete): + project.unions[union.name] = union + elif not existing.is_incomplete and not union.is_incomplete: + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_TAG_DEFINITION", + f"Duplicate definition for union tag {union.name!r}.", + union.source_location, + "union", + union.name, + ) + ) + + def _index_enum( + self, + project: CProject, + enum: CEnum, + diagnostics: list[CDiagnostic], + ) -> None: + if enum.name is None: + return + existing = project.enums.get(enum.name) + if existing is None: + project.enums[enum.name] = enum + else: + diagnostics.append( + self._redeclaration_diagnostic( + "C_DUPLICATE_TAG_DEFINITION", + f"Duplicate definition for enum tag {enum.name!r}.", + enum.source_location, + "enum", + enum.name, + ) + ) + + def _include_graph_target( + self, + parsed_files: dict[str, CFile], + filename: str, + target: str, + resolved_path: str | None, + ) -> str: + local_key = _include_key_from_current(filename, target) + if local_key in parsed_files: + return local_key + + basename_matches = [key for key in parsed_files if PurePosixPath(key).name == target] + if len(basename_matches) == 1: + return basename_matches[0] + + if resolved_path: + resolved_name = Path(resolved_path).name + resolved_matches = [key for key in parsed_files if PurePosixPath(key).name == resolved_name] + if len(resolved_matches) == 1: + return resolved_matches[0] + return str(Path(resolved_path)) + + return local_key + + def _index_file_includes( + self, + project: CProject, + filename: str, + file: CFile, + ) -> None: + local_targets: set[str] = set() + system_targets: set[str] = set() + unresolved_targets: set[str] = set() + + for include in file.includes: + if include.kind == "system": + system_targets.add(include.target) + continue + + target = self._include_graph_target( + project.files, + filename, + include.target, + include.resolved_path, + ) + local_targets.add(target) + if include.resolved_path is None and target not in project.files: + unresolved_targets.add(include.target) + + project.include_graph[filename] = local_targets + project.system_includes[filename] = system_targets + project.unresolved_includes[filename] = unresolved_targets + + def _index_header_source_pairs(self, project: CProject) -> None: + headers = [key for key in project.files if _is_header_key(key)] + sources = [key for key in project.files if _is_source_key(key)] + + for header in headers: + project.header_source_pairs.setdefault(header, set()) + + for header in headers: + header_path = PurePosixPath(header) + header_stem = header_path.with_suffix("") + for source in sources: + source_path = PurePosixPath(source) + if source_path.with_suffix("") == header_stem: + project.header_source_pairs[header].add(source) + + for source in sources: + for included in project.include_graph.get(source, set()): + if included in project.files and _is_header_key(included): + project.header_source_pairs.setdefault(included, set()).add(source) + def visit_file( self, source_or_path: str | Path, @@ -1378,8 +1934,10 @@ def visit_file( encoding: str = "utf-8", ) -> CFile: del macro_defines + source_path: Path | None = None if _looks_like_existing_source_path(source_or_path): path = Path(source_or_path) + source_path = path if filename is None: filename = str(path) source = path.read_text(encoding=encoding) @@ -1388,13 +1946,22 @@ def visit_file( parsed = CFile(filename=filename, parser_status="partial", preprocessing=preprocessing) if preprocessing == "raw": + effective_include_dirs = list(include_dirs or ()) + if source_path is not None: + effective_include_dirs.insert(0, source_path.parent) metadata = collect_preprocessor_metadata( source, filename=filename, - include_dirs=include_dirs, + include_dirs=effective_include_dirs, ) parsed.includes = metadata.includes parsed.macros = metadata.macros + parsed.raw_directives = metadata.raw_directives + parsed.macro_dependencies = self._macro_dependencies( + source, + filename, + {macro.name for macro in metadata.macros if macro.function_like}, + ) parsed.diagnostics = metadata.diagnostics functions, structs, unions, enums, typedefs, variables, parser_diagnostics = self._parse_translation_unit( source, @@ -1407,6 +1974,7 @@ def visit_file( parsed.typedefs = typedefs parsed.variables = variables parsed.diagnostics.extend(parser_diagnostics) + self._normalize_redeclarations(parsed) return parsed def visit_project( diff --git a/c_parser/preprocessor.py b/c_parser/preprocessor.py index d4c3bdbe0..ab1c9ce2d 100644 --- a/c_parser/preprocessor.py +++ b/c_parser/preprocessor.py @@ -7,18 +7,21 @@ from pathlib import Path from .lexer import CLogicalRecord, NormalizedCSource, normalize_c_source -from .models import CDiagnostic, CInclude, CMacro, CSourceLocation +from .models import CDiagnostic, CInclude, CMacro, CRawDirective, CSourceLocation _INCLUDE_RE = re.compile(r'^\s*#\s*include\s*(?:"([^"]+)"|<([^>]+)>)') _DEFINE_RE = re.compile(r"^\s*#\s*define\s+([A-Za-z_]\w*)(\([^)]*\))?(?:\s+(.*))?$") _UNDEF_RE = re.compile(r"^\s*#\s*undef\s+([A-Za-z_]\w*)\s*$") +_DIRECTIVE_RE = re.compile(r"^\s*#\s*([A-Za-z_]\w*)\b(.*)$") +_RAW_PROVENANCE_DIRECTIVES = {"if", "ifdef", "ifndef", "elif", "else", "endif", "pragma"} @dataclass class CPreprocessorMetadata: includes: list[CInclude] = field(default_factory=list) macros: list[CMacro] = field(default_factory=list) + raw_directives: list[CRawDirective] = field(default_factory=list) diagnostics: list[CDiagnostic] = field(default_factory=list) @@ -66,6 +69,19 @@ def collect_preprocessor_metadata( metadata = CPreprocessorMetadata() for record in normalized.records: + directive_match = _DIRECTIVE_RE.match(record.text) + if directive_match: + directive, argument = directive_match.groups() + if directive in _RAW_PROVENANCE_DIRECTIVES: + metadata.raw_directives.append( + CRawDirective( + directive=directive, + argument=argument.strip() or None, + source_location=_record_location(record), + ) + ) + continue + include_match = _INCLUDE_RE.match(record.text) if include_match: local_target, system_target = include_match.groups() diff --git a/c_parser/type_resolver.py b/c_parser/type_resolver.py index 00c71219d..8637d3312 100644 --- a/c_parser/type_resolver.py +++ b/c_parser/type_resolver.py @@ -1,4 +1,157 @@ # -*- coding: utf-8 -*- -"""C type resolver placeholder.""" +"""Basic C project type resolution for parser models.""" -__all__: tuple[str, ...] = () +from __future__ import annotations + +from .models import ( + CComposedType, + CDiagnostic, + CEnum, + CFunction, + CFunctionType, + CParameter, + CProject, + CStruct, + CType, + CTypedef, + CUnion, + CVariable, +) + + +def resolve_project_types(project: CProject) -> CProject: + """Resolve basic typedef and tag references inside a parsed C project.""" + emitted_cycles: set[tuple[str, ...]] = set() + for typedef in project.typedefs.values(): + _resolve_typedef_definition(project, typedef, [], emitted_cycles) + + for file in project.files.values(): + for function in file.functions: + _resolve_function(project, function, emitted_cycles) + for typedef in file.typedefs: + _resolve_typedef_definition(project, typedef, [], emitted_cycles) + for variable in file.variables: + _resolve_variable(project, variable, emitted_cycles) + for aggregate in [*file.structs, *file.unions]: + for member in aggregate.members: + _resolve_variable(project, member, emitted_cycles) + + return project + + +def _resolve_function( + project: CProject, + function: CFunction, + emitted_cycles: set[tuple[str, ...]], +) -> None: + function.result_type = _resolve_type(project, function.result_type, [], emitted_cycles) + for parameter in function.parameters: + _resolve_parameter(project, parameter, emitted_cycles) + + +def _resolve_parameter( + project: CProject, + parameter: CParameter, + emitted_cycles: set[tuple[str, ...]], +) -> None: + parameter.type = _resolve_type(project, parameter.type, [], emitted_cycles) + if parameter.declared_type is not None: + parameter.declared_type = _resolve_type(project, parameter.declared_type, [], emitted_cycles) + + +def _resolve_variable( + project: CProject, + variable: CVariable, + emitted_cycles: set[tuple[str, ...]], +) -> None: + variable.type = _resolve_type(project, variable.type, [], emitted_cycles) + + +def _resolve_typedef_definition( + project: CProject, + typedef: CTypedef, + stack: list[str], + emitted_cycles: set[tuple[str, ...]], +) -> None: + if typedef.type is None: + return + if typedef.name in stack: + _record_typedef_cycle(project, [*stack, typedef.name], typedef, emitted_cycles) + return + typedef.type = _resolve_type(project, typedef.type, [*stack, typedef.name], emitted_cycles) + + +def _resolve_type( + project: CProject, + type_: CType, + stack: list[str], + emitted_cycles: set[tuple[str, ...]], +) -> CType: + if isinstance(type_, CComposedType): + type_.components = [ + _resolve_type(project, component, stack, emitted_cycles) + for component in type_.components + ] + return type_ + if isinstance(type_, CFunctionType): + type_.result_type = _resolve_type(project, type_.result_type, stack, emitted_cycles) + type_.parameter_types = [ + _resolve_type(project, parameter_type, stack, emitted_cycles) + for parameter_type in type_.parameter_types + ] + return type_ + if isinstance(type_, CTypedef): + return _resolve_typedef_reference(project, type_, stack, emitted_cycles) + if isinstance(type_, CStruct) and type_.name and not type_.qualifiers: + return project.structs.get(type_.name, type_) + if isinstance(type_, CUnion) and type_.name and not type_.qualifiers: + return project.unions.get(type_.name, type_) + if isinstance(type_, CEnum) and type_.name and not type_.qualifiers: + return project.enums.get(type_.name, type_) + return type_ + + +def _resolve_typedef_reference( + project: CProject, + reference: CTypedef, + stack: list[str], + emitted_cycles: set[tuple[str, ...]], +) -> CType: + target = project.typedefs.get(reference.name) + if target is None: + return reference + if target.name in stack: + _record_typedef_cycle(project, [*stack, target.name], target, emitted_cycles) + return reference + _resolve_typedef_definition(project, target, stack, emitted_cycles) + return target + + +def _record_typedef_cycle( + project: CProject, + cycle: list[str], + typedef: CTypedef, + emitted_cycles: set[tuple[str, ...]], +) -> None: + first = cycle[-1] + try: + start = cycle.index(first) + except ValueError: # pragma: no cover - defensive only. + start = 0 + normalized = tuple(cycle[start:]) + if normalized in emitted_cycles: + return + emitted_cycles.add(normalized) + project.diagnostics.append( + CDiagnostic( + code="C_TYPEDEF_CYCLE", + message=f"Typedef cycle detected: {' -> '.join(normalized)}.", + severity="error", + location=typedef.source_location, + unit_kind="typedef", + unit_name=typedef.name, + ) + ) + + +__all__ = ("resolve_project_types",) diff --git a/docs/c_parser/c_parser_architecture.md b/docs/c_parser/c_parser_architecture.md index d7776026a..d4083dc85 100644 --- a/docs/c_parser/c_parser_architecture.md +++ b/docs/c_parser/c_parser_architecture.md @@ -2,8 +2,9 @@ Status: partial parser plus raw directive metadata implemented. The `c_parser` package, typed parser models, public entrypoints, explicit -`x2py --language c --parse` CLI path, raw include/macro/undef metadata -collection, top-level source splitting, and a first simple +`x2py --language c --parse` CLI path, raw include/macro/undef/conditional +metadata collection, top-level redeclaration handling, project include/index +reporting, top-level source splitting, and a first simple declaration/function subset with function-definition start/end locations and aggregate declarations exist. Declarators are parsed through a recursive grammar-style path for pointer, array, function, and parenthesized combinations. @@ -30,8 +31,8 @@ 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, and unsupported function-like macro diagnostics - without expanding macros. + macros, `#undef` directives, conditional/pragma directive provenance, 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 @@ -62,22 +63,28 @@ Implemented now: unresolved typedef-like name remains deferred. Definitions preserve direct `start` and `end` locations from the signature start through the closing brace; and K&R-style function definitions raise focused diagnostics. +- Top-level redeclaration handling merges compatible repeated declarations, + completes incomplete struct/union tags when a later definition is parsed, + prefers compatible function or variable definitions over earlier + declarations, preserves related declaration locations, and reports duplicate + definitions or incompatible redeclarations as diagnostics. Local declarations + inside function bodies remain out of scope because bodies are skipped. - `c_parser.cli` provides C-specific partial report formatting. - `x2py.cli` dispatches `--language c --parse` to the C parser path. - `--language c --semantics`, `--language c --pyi`, and C wrap-readiness are rejected until semantic conversion exists. -- Focused partial CLI/API, declaration/function, diagnostic color, and raw - lexer/directive tests are unskipped while broader roadmap tests remain - skipped. +- Focused partial CLI/API, declaration/function, diagnostic color, project + include/index, and raw lexer/directive tests are unskipped while broader + roadmap tests remain skipped. - `tests/data/c/` contains C fixture scaffolding and general fixtures modeled after the Fortran general fixture themes, with additional C-specific API shapes. 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;` +- full typedef/tag resolution policy beyond basic project-level link-up and + callback policy metadata, for example conflict diagnostics, active + conditional branches, and semantic wrappability decisions - nested aggregate member definitions, braced initializers, compiler attributes, alignment specifiers, and `_Atomic(type)` declarations, for example `struct outer { struct { int x; } inner; };` and @@ -328,24 +335,34 @@ int *(*table)[4]; # CComposedType([CPointer(), CArray(bound="4"), CPointer() 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 with per-member locations; there is no separate - field class. + `bit_width`, source/callback metadata, and related declaration locations. + Struct and union `members` are 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 - `CFunctionType`. + locations plus related declaration locations. Its `type` property builds the + corresponding nameless `CFunctionType`. - `CParameter` has a source name, written `declared_type`, and effective `type`; outer array parameters and direct function parameters adjust to pointer `type` values while their source form is retained. - `CInitializer` preserves initializer source text without claiming evaluation. - `CStruct` and `CUnion` expose `members` and `is_incomplete`; `CEnum` exposes `constants`; `CEnumerator` preserves enumerator name and value text. -- `CTypedef` has its alias name and declared `type`. +- `CTypedef` has its alias name, declared `type`, and related declaration + locations. - `CMacro` - `name` - `value` - `function_like` + - `directive` + - `source_location` +- `CRawDirective` + - `directive` + - `argument` + - `source_location` +- `CMacroDependency` + - `name` + - `context` - `source_location` - `CInclude` - `target` @@ -365,6 +382,8 @@ Declaration objects are separate from the type components: - `variables` - `macros` - `includes` + - `raw_directives` + - `macro_dependencies` - `diagnostics` - `CProject` - `files` @@ -376,6 +395,13 @@ Declaration objects are separate from the type components: - `variables` - `macros` - `includes` + - `functions_by_file` + - `enum_constants` + - `include_graph` + - `system_includes` + - `unresolved_includes` + - `header_source_pairs` + - `diagnostics` Serialization uses `"model"` to identify concrete `CType` nodes; `"type"` is reserved for semantic type relationships such as `CVariable.type` and @@ -383,8 +409,8 @@ reserved for semantic type relationships such as `CVariable.type` and spellings, such as `"const"`. Reused aggregate/typedef objects serialize as references to avoid cycles. -Future parser phases can add symbol links, conditional region metadata, -include graphs, and project diagnostics when the corresponding +Future parser phases can deepen symbol links, duplicate/conflict diagnostics, +conditional region ownership, and project diagnostics when the corresponding behavior lands. Additions should be documented and tested with stable serialization expectations. @@ -530,18 +556,25 @@ Current behavior: - Returned `CProject` objects contain `CFile` parser models with raw include, macro, metadata diagnostics, and supported declarations populated per file. - Basic project-level indexes are populated for parsed functions, typedefs, - variables, macros, and includes. -- Include graphs, duplicate analysis, and type resolution are not populated yet. + variables, macros, includes, functions by file, and enum constants. +- Quoted local includes are recorded in an `include_graph`; system includes + are recorded separately. Unresolved quoted includes remain diagnostics + rather than hard failures, and include cycles are represented as graph edges + without recursive traversal. +- Likely header/source pairs are reported by matching stems and direct source + includes. +- Basic cross-file typedef and struct/union/enum tag references are linked to + project index objects after all files are parsed. Original type spelling is + preserved in the model `source_text`. +- Duplicate/conflict analysis is not populated yet. Planned behavior after project-resolution phases: - Collect `.c`, `.h`, and eventually `.i` files from explicit paths or directories. - Parse headers and sources into `CFile` models. -- Build an include graph keyed by normalized path. -- Preserve unresolved includes as diagnostics. -- Associate likely header/source pairs by basename and include relation. -- Resolve typedefs, structs, unions, enums, and constants across parsed files. +- Deepen include graph behavior where normalized paths are ambiguous. +- Deepen duplicate/conflict diagnostics. - Track duplicate symbols by C namespace: - ordinary identifiers - typedef names diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index ed0a1f384..4043c2203 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -2,7 +2,9 @@ Status: C parser partial subset plus raw directive metadata implemented. The CLI command shape exists and parse reports can include raw includes, simple -macros, `#undef` provenance, metadata diagnostics, variables, typedefs, +macros, `#undef` and conditional directive provenance, top-level +redeclaration diagnostics, project include/index metadata, diagnostics, +variables, typedefs, aggregate declarations, function prototypes, prototype-style metadata, and function-definition signatures with start/end locations. Declarator output can represent parenthesized pointer/array precedence through concrete @@ -37,8 +39,13 @@ including function pointers, functions returning function pointers, and legal final flexible struct members marked with `is_flexible=True`. Array and function parameters preserve written `declared_type` forms while effective `type` values use pointer adjustment. Raw -`includes`, `macros`, and metadata `diagnostics` can also be -populated. The object class distinguishes declarations (`CFunction`, +`includes`, `macros`, `raw_directives`, `macro_dependencies`, and metadata +`diagnostics` can also be populated. `parse_c_project(...)` additionally +populates project-level include/index fields such as `include_graph`, +`system_includes`, `unresolved_includes`, `functions_by_file`, +`enum_constants`, and `header_source_pairs`. Those fields require project +context; a single-file parse only records the local file facts. +The object class distinguishes declarations (`CFunction`, `CVariable`, `CTypedef`, `CStruct`, `CUnion`, or `CEnum`), and incomplete tag declarations set `is_incomplete=True`. Known unsupported declaration forms such as declaration attributes, alignment @@ -52,7 +59,12 @@ unsupported K&R-style function definitions and invalid primitive-specifier combinations such as `unsigned float`, honor `--no-color` and `NO_COLOR=1`. Function definitions do not store executable body text; they preserve a direct `start` location and `end` location from the signature start through the -closing brace. +closing brace. Compatible repeated top-level declarations are merged; +prototype-plus-definition records prefer the definition and preserve prototype +locations in `declaration_locations`. File-scope tentative declarations such +as `int i; int i;` are also merged. Duplicate definitions and incompatible +top-level redeclarations are reported as diagnostics. Local declarations inside +function bodies are not parsed. Unsupported C stages: @@ -232,7 +244,8 @@ JSON output for a file without raw directives: "prototype_style": "prototype", "source_location": {"filename": "include/example.h", "line": 1, "...": "..."}, "start": {"filename": "include/example.h", "line": 1, "...": "..."}, - "end": null + "end": null, + "declaration_locations": [] } ], "structs": [], @@ -242,6 +255,8 @@ JSON output for a file without raw directives: "variables": [], "macros": [], "includes": [], + "raw_directives": [], + "macro_dependencies": [], "diagnostics": [] } } @@ -251,12 +266,15 @@ The parser should not claim C files are wrappable. If C readiness is added later, it should follow the semantics-owned readiness boundary used elsewhere in x2py, not become parser JSON. -For raw directives, the same JSON shape is used, but `includes`, `macros`, and -`diagnostics` may contain populated model dictionaries. Function-like macros -are recorded as macro metadata and also produce a non-fatal -`C_UNSUPPORTED_FUNCTION_LIKE_MACRO` diagnostic. Local quoted includes are -resolved relative to the current file when possible; unresolved local includes -produce `C_UNRESOLVED_INCLUDE` diagnostics instead of hard failures. +For raw directives, the same JSON shape is used, but `includes`, `macros`, +`raw_directives`, `macro_dependencies`, and `diagnostics` may contain populated +model dictionaries. Function-like macros are recorded as macro metadata and +also produce a non-fatal `C_UNSUPPORTED_FUNCTION_LIKE_MACRO` diagnostic. +Macro-shaped declarations are marked through `macro_dependencies` without +being parsed as expanded declarations. Local quoted includes are resolved +relative to the current file or configured include dirs when possible; +unresolved local includes produce `C_UNRESOLVED_INCLUDE` diagnostics instead +of hard failures. Raw mode must not claim support for macro-generated declarations. If macros affect function names, types, parameters, attributes, storage classes, calling @@ -284,6 +302,8 @@ typedefs variables macros includes +raw_directives +macro_dependencies diagnostics ``` diff --git a/docs/c_parser/c_parser_implementation_checklist.md b/docs/c_parser/c_parser_implementation_checklist.md index f89538795..9982edae4 100644 --- a/docs/c_parser/c_parser_implementation_checklist.md +++ b/docs/c_parser/c_parser_implementation_checklist.md @@ -2,8 +2,10 @@ Status: implementation checklist with Phase 1 skeleton, selected Phase 2 fixture scaffolding, selected Phase 3 model/error work, Phase 4 raw -lexer/directive metadata, and a first Phase 5/6 partial -declaration/function subset complete. The `c_parser` package and explicit C +lexer/directive metadata, a first Phase 5/6 partial declaration/function +subset including top-level redeclaration handling, and selected Phase 8 project +include/index work complete. The +`c_parser` package and explicit C parse path exist, and simple variables, typedefs, function prototypes, function-definition signatures, function-definition start/end locations, and incomplete `struct`/`union` declarations and basic aggregate definitions are @@ -16,7 +18,14 @@ extensions are diagnosed, and invalid primitive-specifier combinations raise 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` -while exposing C-adjusted pointer forms in `type`. +while exposing C-adjusted pointer forms in `type`. Raw conditional directives +and macro-shaped declarations are stored as metadata. Project parsing now +records include graphs, system and unresolved includes, functions by file, +enum constants, header/source pairings, and basic cross-file typedef/tag +resolution with incomplete tag completion. Compatible top-level prototypes, +tentative file-scope variables, and repeated typedefs are merged; duplicate +definitions and incompatible redeclarations produce diagnostics. Local +declarations inside function bodies are intentionally ignored. 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 @@ -26,7 +35,7 @@ stable. ## Progress Snapshot - Last updated: 2026-05-24 -- Checklist progress: 517/849 checked (60.9%). +- Checklist progress: 586/856 checked (68.5%). - 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 @@ -43,7 +52,14 @@ stable. 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. + their effective `type` applies C parameter-to-pointer adjustment. Raw + conditional directives and macro-shaped declaration dependencies 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 + top-level declarations produce diagnostics. ## Global Rules @@ -340,7 +356,7 @@ Scope: - [x] When a skipped test is unblocked, replace placeholder expectations with the exact implemented model fields if the final schema differs. - [x] Keep the skipped C suite separate from existing Fortran tests. -- [ ] Keep Fortran tests green whenever C tests are unskipped. +- [x] Keep Fortran tests green whenever C tests are unskipped. ### Skipped Roadmap Test Files @@ -401,7 +417,7 @@ Scope: - [x] C fixture directory structure is present. - [ ] C golden update workflow is documented. - [x] Partial parser and metadata tests pass against current behavior. -- [ ] Fortran tests still pass. +- [x] Fortran tests still pass. - [x] No real parser claims are made without tests. ### Phase 2 Test Expectations @@ -513,7 +529,7 @@ Scope: - [x] Add tests for empty `CFile` serialization. - [ ] Add tests for each model's minimal JSON shape. - [x] Add tests for source-location serialization. -- [ ] Add tests that unknown/unresolved metadata is preserved. +- [x] Add tests that unknown/unresolved metadata is preserved. ### Public API Skeleton And Partial Parser Tasks @@ -597,16 +613,16 @@ Scope: - [x] Recognize function-like `#define NAME(...) body`. - [x] Store function-like macros as unsupported/deferred metadata. - [x] Recognize `#undef`. -- [ ] Record conditional directive presence (`#ifdef`, `#ifndef`, `#if`, +- [x] Record conditional directive presence (`#ifdef`, `#ifndef`, `#if`, `#elif`, `#else`, `#endif`) as provenance metadata if needed. - [x] Do not select active branches in raw mode. -- [ ] Do not implement a parser-side `defined(NAME)`, `&&`, `||`, `!`, `0`, +- [x] Do not implement a parser-side `defined(NAME)`, `&&`, `||`, `!`, `0`, and `1` evaluator for C API extraction unless a later design explicitly justifies it. - [x] Mark macro-shaped declarations as unsupported/deferred in raw mode. -- [ ] Store macro-dependency metadata in C parser models. +- [x] Store macro-dependency metadata in C parser models. - [x] Store preprocessing mode metadata in `CFile`. -- [ ] Store raw directive metadata separately from compiler-preprocessor +- [x] Store raw directive metadata separately from compiler-preprocessor configuration metadata. - [x] Do not implement general macro expansion. - [x] Do not expand token-paste or stringify macros. @@ -643,7 +659,7 @@ Scope: - [x] Lexer/preprocessor preserves source locations. - [x] Includes and macros are collected as metadata. -- [ ] Raw conditional directives are handled as metadata/provenance only, not +- [x] Raw conditional directives are handled as metadata/provenance only, not parser-side branch selection. - [x] No arbitrary macro expansion is attempted. - [x] Compiler-assisted preprocessing has a documented design path for @@ -756,6 +772,17 @@ Scope: - [ ] Add diagnostics for declarations ignored by the current partial parser. - [ ] Add structured source facts for declarations that depend on macros. +### Top-Level Redeclaration Tasks + +- [x] Merge compatible repeated file-scope tentative variable declarations. +- [x] Prefer initialized file-scope definitions over earlier tentative + declarations. +- [x] Diagnose duplicate initialized file-scope variable definitions. +- [x] Diagnose conflicting file-scope variable redeclarations. +- [x] Complete incomplete struct tags from later same-file definitions. +- [x] Complete incomplete union tags from later same-file definitions. +- [x] Keep local declarations inside function bodies ignored in v1. + Known declaration implementation gaps, with representative syntax: - braced/designated initializer preservation: @@ -855,16 +882,16 @@ Scope: ### Function Deduplication Tasks -- [ ] Merge matching prototype and definition in the same file. -- [ ] Prefer definition metadata where useful. -- [ ] Preserve both source locations if helpful. -- [ ] Detect conflicting declarations. -- [ ] Detect duplicate definitions. +- [x] Merge matching prototype and definition in the same file. +- [x] Prefer definition metadata where useful. +- [x] Preserve both source locations if helpful. +- [x] Detect conflicting declarations. +- [x] Detect duplicate definitions. - [ ] Allow same function under mutually exclusive preprocessor branches. -- [ ] Add tests for prototype plus definition. -- [ ] Add tests for conflicting prototypes. -- [ ] Add tests for duplicate definitions. -- [ ] Preserve declaration order before deduplicating prototypes and +- [x] Add tests for prototype plus definition. +- [x] Add tests for conflicting prototypes. +- [x] Add tests for duplicate definitions. +- [x] Preserve declaration order before deduplicating prototypes and definitions. ### Phase 6 Definition Of Done @@ -953,7 +980,7 @@ Scope: - [x] Parse function pointer typedefs. - [x] Parse struct/union/enum typedefs. - [x] Preserve alias chains before resolution. -- [ ] Detect duplicate typedefs in same scope. +- [x] Detect duplicate typedefs in same scope. - [x] Add tests for typedef chains. - [x] Add tests for primitive typedefs. - [x] Add tests for opaque handle typedefs. @@ -1000,68 +1027,68 @@ Scope: ### Include Resolution Tasks -- [ ] Resolve quoted includes relative to current file. -- [ ] Resolve quoted includes through `include_dirs`. -- [ ] Record unresolved quoted includes. -- [ ] Record system includes without requiring local resolution by default. -- [ ] Build `include_graph`. -- [ ] Detect include cycles without crashing. -- [ ] Preserve include spelling and resolved path separately. -- [ ] Add tests for local includes. -- [ ] Add tests for include dirs. -- [ ] Add tests for missing includes. -- [ ] Add tests for include cycles. +- [x] Resolve quoted includes relative to current file. +- [x] Resolve quoted includes through `include_dirs`. +- [x] Record unresolved quoted includes. +- [x] Record system includes without requiring local resolution by default. +- [x] Build `include_graph`. +- [x] Detect include cycles without crashing. +- [x] Preserve include spelling and resolved path separately. +- [x] Add tests for local includes. +- [x] Add tests for include dirs. +- [x] Add tests for missing includes. +- [x] Add tests for include cycles. ### Project Index Tasks - [x] Index functions by name. -- [ ] Index functions by file. +- [x] Index functions by file. - [x] Index typedefs by name. - [x] Index struct tags by tag namespace. - [x] Index union tags by tag namespace. - [x] Index enum tags by tag namespace. -- [ ] Index enum constants in ordinary identifier namespace. +- [x] Index enum constants in ordinary identifier namespace. - [x] Index macros/constants separately. - [x] Index variables by name. -- [ ] Detect duplicate definitions. -- [ ] Distinguish compatible redeclarations from conflicts. -- [ ] Add tests for duplicate handling. -- [ ] Add tests for project-level function indexes. -- [ ] Add tests for project-level typedef indexes. -- [ ] Add tests for project-level file-scope variable indexes. -- [ ] Add tests for project-level macro indexes. +- [x] Detect duplicate definitions. +- [x] Distinguish compatible redeclarations from conflicts. +- [x] Add tests for duplicate handling. +- [x] Add tests for project-level function indexes. +- [x] Add tests for project-level typedef indexes. +- [x] Add tests for project-level file-scope variable indexes. +- [x] Add tests for project-level macro indexes. ### Type Resolution Tasks -- [ ] Resolve typedef chains. -- [ ] Detect typedef cycles. -- [ ] Resolve struct tag references. -- [ ] Resolve union tag references. -- [ ] Resolve enum tag references. -- [ ] Resolve opaque pointer typedefs. -- [ ] Preserve unresolved references for later semantic diagnostics. -- [ ] Do not lose original spelling during resolution. -- [ ] Add tests for cross-file typedef resolution. -- [ ] Add tests for cross-file struct resolution. -- [ ] Add tests for opaque handles. -- [ ] Add tests for unresolved references. +- [x] Resolve typedef chains. +- [x] Detect typedef cycles. +- [x] Resolve struct tag references. +- [x] Resolve union tag references. +- [x] Resolve enum tag references. +- [x] Resolve opaque pointer typedefs. +- [x] Preserve unresolved references for later semantic diagnostics. +- [x] Do not lose original spelling during resolution. +- [x] Add tests for cross-file typedef resolution. +- [x] Add tests for cross-file struct resolution. +- [x] Add tests for opaque handles. +- [x] Add tests for unresolved references. ### Header/Source Pairing Tasks -- [ ] Pair `foo.c` with `foo.h` by basename. -- [ ] Pair source with headers it includes. -- [ ] Preserve many-to-many relationships. -- [ ] Use pairings for reporting, not for hidden behavior. -- [ ] Add tests for header/source pairing. +- [x] Pair `foo.c` with `foo.h` by basename. +- [x] Pair source with headers it includes. +- [x] Preserve many-to-many relationships. +- [x] Use pairings for reporting, not for hidden behavior. +- [x] Add tests for header/source pairing. ### Phase 8 Definition Of Done -- [ ] `parse_c_project` returns a populated `CProject`. -- [ ] Include graph is stable and serialized. -- [ ] Cross-file typedef/tag resolution works for basic projects. -- [ ] Missing project context becomes parser or semantic diagnostics as +- [x] `parse_c_project` returns a populated `CProject`. +- [x] Include graph is stable and serialized. +- [x] Cross-file typedef/tag resolution works for basic projects. +- [x] Missing project context becomes parser or semantic diagnostics as appropriate. -- [ ] Tests cover directory and file-list parsing. +- [x] Tests cover directory and file-list parsing. ### Phase 8 Risks And Open Questions @@ -1350,13 +1377,13 @@ Scope: ### Regression Hardening Tasks -- [ ] Run full parser tests. +- [x] Run full parser tests. - [ ] Run semantic tests. - [ ] Run `.pyi` tests. - [ ] Run C corpus parse-only tests. - [x] Run CLI tests. - [ ] Run golden fixture tests. -- [ ] Confirm Fortran tests still pass. +- [x] Confirm Fortran tests still pass. - [ ] Audit JSON schema stability. - [ ] Audit error diagnostic stability. - [x] Audit docs for implemented behavior. diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index 03f621059..2419fab1e 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -162,7 +162,10 @@ 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 function-like macros as metadata with unsupported/deferred diagnostics +- record macro-shaped declarations as macro-dependency metadata without + claiming they were parsed - parse only declarations that are already visible as ordinary C without macro expansion - do not select active conditional branches from `#if`/`#ifdef` in raw mode @@ -290,8 +293,24 @@ include `prototype_style`, currently `"prototype"` for typed or explicit `void` parameter lists and `"unspecified"` for empty parameter lists such as `int f()`. Function definitions do not store executable body text; they include direct `start` and `end` locations. +Compatible top-level function redeclarations are merged, and a matching +prototype plus definition prefers the definition while retaining the prototype +location in `declaration_locations`. File-scope tentative variable +declarations such as `int i; int i;` are merged; a later initialized +definition such as `int i = 1;` is preferred over an earlier tentative +declaration. Duplicate initialized variables, duplicate function definitions, +duplicate complete tag definitions, and incompatible top-level redeclarations +produce diagnostics. Local declarations inside function bodies are ignored +because body contents are intentionally skipped. Re-export from `x2py` is still deferred; users should import from `c_parser`. +Project-level facts require `parse_c_project(...)`, not just +`parse_c_file(...)`. A single file can report its own raw directives, includes, +macros, declarations, diagnostics, and unresolved typedef/tag references. A +project parse sees multiple files together and can populate include graphs, +system include records, unresolved include sets, functions by file, enum +constants, likely header/source pairs, and basic cross-file typedef/tag links. + `macro_defines` is reserved for future compiler-assisted preprocessing configuration. It must not mean that raw mode evaluates C preprocessor conditionals or expands macros internally. @@ -447,10 +466,12 @@ and skipped roadmap tests under `tests/parser/c/`. The active tests cover public entrypoints, empty model serialization, CLI discovery, JSON/output-file behavior, unsupported C stages, comment stripping, line-continuation folding, top-level splitting, include collection, simple macro collection, macro-shaped -declaration deferral, raw conditional branch non-selection, simple declarations, -variables, typedefs, recursive declarator composition, aggregate definitions, -members, enums, and simple function prototypes/definitions, including -function-definition start/end locations. The broader roadmap tests remain skipped +declaration deferral, raw conditional branch non-selection and provenance, +macro-dependency metadata, project include/index behavior, simple declarations, +variables, typedefs, top-level redeclaration diagnostics, recursive declarator +composition, aggregate definitions, members, enums, and simple function +prototypes/definitions, including function-definition start/end locations. The +broader roadmap tests remain skipped until their matching implementation branches land. Future implementation branches should unskip only the tests for the capability they implement, then merge those branches back into `c-parser/main`. diff --git a/tests/parser/c/test_c_declarations_and_declarators.py b/tests/parser/c/test_c_declarations_and_declarators.py index ae08db982..25914b116 100644 --- a/tests/parser/c/test_c_declarations_and_declarators.py +++ b/tests/parser/c/test_c_declarations_and_declarators.py @@ -220,6 +220,60 @@ def test_typedefs_and_typedef_references_are_concrete_types(): assert isinstance(parsed.functions[1].parameters[0].type, CTypedef) +def test_repeated_file_scope_tentative_variable_declarations_merge(): + from c_parser import parse_c_file + + parsed = parse_c_file("int i;\nint i;\n", filename="tentative.c") + + assert [variable.name for variable in parsed.variables] == ["i"] + assert parsed.variables[0].initializer is None + assert [location.line for location in parsed.variables[0].declaration_locations] == [2] + assert parsed.diagnostics == [] + + +def test_tentative_variable_declaration_followed_by_definition_prefers_definition(): + from c_parser import parse_c_file + + parsed = parse_c_file("int i;\nint i = 1;\n", filename="definition.c") + + assert [variable.name for variable in parsed.variables] == ["i"] + assert parsed.variables[0].initializer.source_text == "1" + assert parsed.variables[0].source_location.line == 2 + assert [location.line for location in parsed.variables[0].declaration_locations] == [1] + assert parsed.diagnostics == [] + + +def test_duplicate_initialized_file_scope_variables_report_diagnostic(): + from c_parser import parse_c_file + + parsed = parse_c_file("int i = 1;\nint i = 2;\n", filename="duplicate_variables.c") + + assert [variable.name for variable in parsed.variables] == ["i"] + assert parsed.variables[0].initializer.source_text == "1" + assert any(diag.code == "C_DUPLICATE_VARIABLE_DEFINITION" for diag in parsed.diagnostics) + + +def test_conflicting_file_scope_variable_declarations_report_diagnostic(): + from c_parser import parse_c_file + + parsed = parse_c_file("int i;\ndouble i;\n", filename="conflicting_variables.c") + + assert [variable.name for variable in parsed.variables] == ["i"] + assert any(diag.code == "C_CONFLICTING_VARIABLE_DECLARATION" for diag in parsed.diagnostics) + + +def test_compatible_repeated_typedefs_merge_but_conflicting_typedefs_diagnose(): + from c_parser import parse_c_file + + compatible = parse_c_file("typedef int count_t;\ntypedef int count_t;\n", filename="typedefs.h") + conflicting = parse_c_file("typedef int count_t;\ntypedef double count_t;\n", filename="bad_typedefs.h") + + assert [typedef.name for typedef in compatible.typedefs] == ["count_t"] + assert [location.line for location in compatible.typedefs[0].declaration_locations] == [2] + assert compatible.diagnostics == [] + assert any(diag.code == "C_CONFLICTING_TYPEDEF" for diag in conflicting.diagnostics) + + def test_variables_preserve_initializer_text_arrays_and_concrete_tag_types(): from c_parser import CArray, CComposedType, CEnum, CInt, CStruct, CUnion, parse_c_file diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index ea31a4cdf..9f7c20040 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -139,3 +139,53 @@ def test_function_returning_pointer_to_const_struct_is_preserved(): assert isinstance(result.components[0], CPointer) assert isinstance(result.components[1], CStruct) assert result.components[1].qualifiers == [CConst()] + + +def test_matching_prototype_and_definition_merge_and_prefer_definition(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +int solve(int value); +int solve(int value) +{ + return value; +} +""", + filename="redeclarations.c", + ) + + assert [function.name for function in parsed.functions] == ["solve"] + function = parsed.functions[0] + assert function.is_definition is True + assert function.source_location.line == 3 + assert [location.line for location in function.declaration_locations] == [2] + assert parsed.diagnostics == [] + + +def test_conflicting_function_prototypes_report_diagnostic(): + from c_parser import parse_c_file + + parsed = parse_c_file( + "int work(int value);\ndouble work(double value);\n", + filename="conflicting_functions.h", + ) + + assert [function.name for function in parsed.functions] == ["work"] + assert any(diag.code == "C_CONFLICTING_FUNCTION_DECLARATION" for diag in parsed.diagnostics) + + +def test_duplicate_function_definitions_report_diagnostic(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +int value(void) { return 1; } +int value(void) { return 2; } +""", + filename="duplicate_functions.c", + ) + + assert [function.name for function in parsed.functions] == ["value"] + assert parsed.functions[0].is_definition is True + assert any(diag.code == "C_DUPLICATE_FUNCTION_DEFINITION" for diag in parsed.diagnostics) diff --git a/tests/parser/c/test_c_lexer_preprocessor.py b/tests/parser/c/test_c_lexer_preprocessor.py index efa5d8589..8237d76e8 100644 --- a/tests/parser/c/test_c_lexer_preprocessor.py +++ b/tests/parser/c/test_c_lexer_preprocessor.py @@ -191,6 +191,29 @@ def test_raw_conditional_directives_do_not_select_active_branches(): ) assert {fn.name for fn in parsed.functions} == {"run_fast", "run_slow"} + assert [(item.directive, item.argument) for item in parsed.raw_directives] == [ + ("ifdef", "USE_FAST"), + ("else", None), + ("endif", None), + ] + + +def test_raw_mode_records_macro_dependency_metadata_for_macro_shaped_declarations(): + from c_parser import parse_c_file + + parsed = parse_c_file( + """ +#define API_DECL(ret) ret +API_DECL(int) exported(void); +""", + filename="macro_dependency.h", + preprocessing="raw", + ) + + assert [(item.name, item.context) for item in parsed.macro_dependencies] == [ + ("API_DECL", "declaration") + ] + assert parsed.macro_dependencies[0].source_location.line == 3 @pytest.mark.skip(reason="compiler-preprocessed mode lands after raw metadata collection.") diff --git a/tests/parser/c/test_c_project_resolution.py b/tests/parser/c/test_c_project_resolution.py new file mode 100644 index 000000000..d7244c737 --- /dev/null +++ b/tests/parser/c/test_c_project_resolution.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +"""Active coverage for current C project include/index behavior.""" + +from pathlib import Path + + +def test_project_include_graph_tracks_local_system_missing_and_cycles(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "a.h").write_text('#include "b.h"\n#include "missing.h"\n', encoding="utf-8") + (tmp_path / "b.h").write_text('#include "a.h"\n#include \n', encoding="utf-8") + + project = parse_c_project(tmp_path) + + assert project.include_graph["a.h"] == {"b.h", "missing.h"} + assert project.include_graph["b.h"] == {"a.h"} + assert project.system_includes["b.h"] == {"stddef.h"} + assert project.unresolved_includes["a.h"] == {"missing.h"} + assert any(diag.code == "C_UNRESOLVED_INCLUDE" for diag in project.files["a.h"].diagnostics) + + +def test_project_resolves_quoted_includes_through_include_dirs(tmp_path: Path): + from c_parser import parse_c_project + + include_dir = tmp_path / "include" + src_dir = tmp_path / "src" + include_dir.mkdir() + src_dir.mkdir() + types = include_dir / "types.h" + api = src_dir / "api.h" + types.write_text("typedef int api_int;\n", encoding="utf-8") + api.write_text('#include "types.h"\napi_int answer(void);\n', encoding="utf-8") + + project = parse_c_project([api], include_dirs=[include_dir]) + + include = project.files[str(api)].includes[0] + assert include.target == "types.h" + assert include.resolved_path == str(types) + assert project.unresolved_includes[str(api)] == set() + + +def test_project_indexes_functions_by_file_and_enum_constants(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "api.h").write_text( + "enum status { STATUS_OK = 0, STATUS_ERROR = -1 };\n" + "int run(void);\n" + "int stop(void);\n", + encoding="utf-8", + ) + + project = parse_c_project(tmp_path) + + assert project.functions_by_file["api.h"] == ["run", "stop"] + assert set(project.enum_constants) == {"STATUS_OK", "STATUS_ERROR"} + assert project.enum_constants["STATUS_OK"].value == "0" + + +def test_project_indexes_file_scope_variables_and_macros(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "api.h").write_text( + "#define API_VERSION 3\n" + "extern int global_count;\n", + encoding="utf-8", + ) + + project = parse_c_project(tmp_path) + + assert project.variables["global_count"].storage == ["extern"] + assert project.macros["API_VERSION"].value == "3" + + +def test_project_function_index_prefers_definition_over_compatible_prototype(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "api.h").write_text("int solve(int value);\n", encoding="utf-8") + (tmp_path / "api.c").write_text("int solve(int value) { return value; }\n", encoding="utf-8") + + project = parse_c_project(tmp_path) + + assert project.functions["solve"].is_definition is True + assert project.functions["solve"].declaration_locations + assert not any(diag.code.startswith("C_CONFLICTING") for diag in project.diagnostics) + + +def test_project_reports_conflicting_function_declarations(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "a.h").write_text("int work(int value);\n", encoding="utf-8") + (tmp_path / "b.h").write_text("double work(double value);\n", encoding="utf-8") + + project = parse_c_project(tmp_path) + + assert any(diag.code == "C_CONFLICTING_FUNCTION_DECLARATION" for diag in project.diagnostics) + + +def test_project_resolves_typedefs_and_struct_tags_across_files(tmp_path: Path): + from c_parser import CComposedType, CTypedef, parse_c_project + + (tmp_path / "types.h").write_text( + "typedef unsigned long api_size;\n" + "struct state { int id; };\n", + encoding="utf-8", + ) + (tmp_path / "api.h").write_text( + '#include "types.h"\n' + "api_size count(void);\n" + "void step(struct state *s);\n", + encoding="utf-8", + ) + + project = parse_c_project(tmp_path) + + assert isinstance(project.functions["count"].result_type, CTypedef) + assert project.functions["count"].result_type is project.typedefs["api_size"] + param_type = project.functions["step"].parameters[0].type + assert isinstance(param_type, CComposedType) + assert param_type.components[-1] is project.structs["state"] + + +def test_project_completes_forward_struct_tags_regardless_of_file_order(): + from c_parser import CComposedType, parse_c_project + + project = parse_c_project( + { + "forward.h": "struct state;\nvoid step(struct state *s);\n", + "definition.h": "struct state { int id; };\n", + } + ) + + assert project.structs["state"].is_incomplete is False + assert project.structs["state"].members[0].name == "id" + param_type = project.functions["step"].parameters[0].type + assert isinstance(param_type, CComposedType) + assert param_type.components[-1] is project.structs["state"] + + +def test_project_keeps_complete_union_definition_when_forward_seen_later(): + from c_parser import CComposedType, parse_c_project + + project = parse_c_project( + { + "definition.h": "union value { int i; };\n", + "forward.h": "union value;\nvoid set_value(union value *v);\n", + } + ) + + assert project.unions["value"].is_incomplete is False + assert project.unions["value"].members[0].name == "i" + param_type = project.functions["set_value"].parameters[0].type + assert isinstance(param_type, CComposedType) + assert param_type.components[-1] is project.unions["value"] + + +def test_project_resolves_typedef_chains_while_preserving_alias_objects(tmp_path: Path): + from c_parser import CTypedef, CUnsignedLong, parse_c_project + + (tmp_path / "types.h").write_text( + "typedef unsigned long raw_size;\n" + "typedef raw_size api_size;\n", + encoding="utf-8", + ) + (tmp_path / "api.h").write_text("api_size count(void);\n", encoding="utf-8") + + project = parse_c_project(tmp_path) + + assert project.typedefs["api_size"].type is project.typedefs["raw_size"] + assert isinstance(project.typedefs["raw_size"].type, CUnsignedLong) + assert isinstance(project.functions["count"].result_type, CTypedef) + assert project.functions["count"].result_type is project.typedefs["api_size"] + assert project.functions["count"].result_type.source_text == "api_size" + + +def test_project_reports_typedef_cycles_without_crashing(): + from c_parser import parse_c_project + + project = parse_c_project({"cycle.h": "typedef b a;\ntypedef a b;\n"}) + + assert {diag.code for diag in project.diagnostics} == {"C_TYPEDEF_CYCLE"} + + +def test_project_resolves_union_and_enum_tag_references(tmp_path: Path): + from c_parser import CComposedType, parse_c_project + + (tmp_path / "types.h").write_text( + "union value { int i; };\n" + "enum status { STATUS_OK = 0 };\n", + encoding="utf-8", + ) + (tmp_path / "api.h").write_text( + "void set_value(union value *v);\n" + "enum status current_status(void);\n", + encoding="utf-8", + ) + + project = parse_c_project(tmp_path) + + value_type = project.functions["set_value"].parameters[0].type + assert isinstance(value_type, CComposedType) + assert value_type.components[-1] is project.unions["value"] + assert project.functions["current_status"].result_type is project.enums["status"] + + +def test_project_resolves_opaque_pointer_typedefs_across_files(tmp_path: Path): + from c_parser import CComposedType, CTypedef, parse_c_project + + (tmp_path / "types.h").write_text( + "struct handle;\n" + "typedef struct handle *handle_t;\n", + encoding="utf-8", + ) + (tmp_path / "api.h").write_text("handle_t open_handle(void);\n", encoding="utf-8") + + project = parse_c_project(tmp_path) + + assert project.typedefs["handle_t"].type.components[-1] is project.structs["handle"] + assert project.structs["handle"].is_incomplete is True + assert isinstance(project.functions["open_handle"].result_type, CTypedef) + assert project.functions["open_handle"].result_type is project.typedefs["handle_t"] + assert isinstance(project.typedefs["handle_t"].type, CComposedType) + + +def test_project_preserves_unresolved_type_references_for_later_diagnostics(): + from c_parser import CTypedef, parse_c_project + + project = parse_c_project({"api.h": "missing_type value(void);\n"}) + + assert isinstance(project.functions["value"].result_type, CTypedef) + assert project.functions["value"].result_type.name == "missing_type" + assert project.functions["value"].result_type.type is None + + +def test_project_header_source_pairs_use_matching_stems_and_direct_includes(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "solver.h").write_text("int solve(void);\n", encoding="utf-8") + (tmp_path / "solver.c").write_text('#include "solver.h"\n', encoding="utf-8") + (tmp_path / "driver.h").write_text("int drive(void);\n", encoding="utf-8") + (tmp_path / "main.c").write_text('#include "driver.h"\n', encoding="utf-8") + + project = parse_c_project(tmp_path) + + assert project.header_source_pairs["solver.h"] == {"solver.c"} + assert project.header_source_pairs["driver.h"] == {"main.c"} + + +def test_project_header_source_pairs_preserve_many_to_many_relationships(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "a.h").write_text("int a(void);\n", encoding="utf-8") + (tmp_path / "b.h").write_text("int b(void);\n", encoding="utf-8") + (tmp_path / "one.c").write_text('#include "a.h"\n#include "b.h"\n', encoding="utf-8") + (tmp_path / "two.c").write_text('#include "a.h"\n', encoding="utf-8") + + project = parse_c_project(tmp_path) + + assert project.header_source_pairs["a.h"] == {"one.c", "two.c"} + assert project.header_source_pairs["b.h"] == {"one.c"} + + +def test_project_serialization_keeps_include_indexes_json_stable(tmp_path: Path): + from c_parser import parse_c_project + + (tmp_path / "api.h").write_text("#include \nint run(void);\n", encoding="utf-8") + + payload = parse_c_project(tmp_path).to_dict() + + assert payload["include_graph"] == {"api.h": []} + assert payload["system_includes"] == {"api.h": ["stddef.h"]} + assert payload["functions_by_file"] == {"api.h": ["run"]} diff --git a/tests/parser/c/test_c_public_api_skeleton.py b/tests/parser/c/test_c_public_api_skeleton.py index cc998160b..abf15f214 100644 --- a/tests/parser/c/test_c_public_api_skeleton.py +++ b/tests/parser/c/test_c_public_api_skeleton.py @@ -112,6 +112,8 @@ def test_c_file_serialization_is_json_stable(): "variables": [], "macros": [], "includes": [], + "raw_directives": [], + "macro_dependencies": [], "diagnostics": [], } @@ -174,6 +176,38 @@ def test_inline_aggregate_typedef_serialization_uses_references_without_cycles() assert payload["typedefs"][0]["type"] == {"reference": "struct node"} +def test_model_json_shapes_cover_directive_include_macro_and_diagnostic_fields(): + from c_parser import parse_c_file + + payload = parse_c_file( + '#ifdef FEATURE\n#include "missing.h"\n#define API(ret) ret\nAPI(int) run(void);\n#endif\n', + filename="metadata.h", + ).to_dict() + + assert payload["raw_directives"][0]["directive"] == "ifdef" + assert payload["raw_directives"][0]["argument"] == "FEATURE" + assert payload["includes"][0]["target"] == "missing.h" + assert payload["includes"][0]["resolved_path"] is None + assert payload["macros"][0]["name"] == "API" + assert payload["macros"][0]["function_like"] is True + assert payload["macro_dependencies"][0]["name"] == "API" + assert {diagnostic["code"] for diagnostic in payload["diagnostics"]} >= { + "C_UNRESOLVED_INCLUDE", + "C_UNSUPPORTED_FUNCTION_LIKE_MACRO", + } + + +def test_unresolved_typedef_reference_metadata_is_preserved_in_json(): + from c_parser import parse_c_file + + payload = parse_c_file("api_size count(void);\n", filename="unresolved.h").to_dict() + + result_type = payload["functions"][0]["result_type"] + assert result_type["model"] == "CTypedef" + assert result_type["name"] == "api_size" + assert result_type["type"] is None + + def test_public_c_parser_entrypoints_do_not_include_parser_side_readiness(): import c_parser 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 43b7c0134..aa56c04a5 100644 --- a/tests/parser/c/test_c_structs_unions_enums_typedefs.py +++ b/tests/parser/c/test_c_structs_unions_enums_typedefs.py @@ -33,6 +33,32 @@ def test_typedef_struct_alias_refers_to_the_concrete_struct_object(): assert parsed.typedefs[0].type is parsed.structs[0] +def test_forward_struct_declaration_is_completed_by_later_definition(): + from c_parser import parse_c_file + + parsed = parse_c_file( + "struct state;\nstruct state { int id; };\n", + filename="complete_struct.h", + ) + + assert [struct.name for struct in parsed.structs] == ["state"] + assert parsed.structs[0].is_incomplete is False + assert parsed.structs[0].members[0].name == "id" + assert parsed.diagnostics == [] + + +def test_duplicate_complete_tag_definitions_report_diagnostics(): + from c_parser import parse_c_file + + parsed = parse_c_file( + "struct state { int id; };\nstruct state { int id; };\n", + filename="duplicate_struct.h", + ) + + assert [struct.name for struct in parsed.structs] == ["state"] + assert any(diag.code == "C_DUPLICATE_TAG_DEFINITION" for diag in parsed.diagnostics) + + def test_anonymous_struct_typedef_gets_stable_anonymous_id(): from c_parser import parse_c_file