From 08be269a3e1df602bf461f3342174007c5ffac1a Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Wed, 27 May 2026 04:02:41 +0100 Subject: [PATCH 1/5] codex: reject file-scope executable statements in Fortran parser --- fortran_parser/parser.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index e2500b581..83ea22ba6 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -1174,9 +1174,6 @@ def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, if self._is_allowed_unparsed_file_scope_line(stripped): index += 1 continue - if self._is_executable_statement_start(stripped): - index += 1 - continue self._raise_invalid_fortran_syntax_line( stripped, context="file scope", From 6fda738c65e73ffc6ee6f6594eecb65ae0750798 Mon Sep 17 00:00:00 2001 From: said Date: Wed, 27 May 2026 04:20:39 +0100 Subject: [PATCH 2/5] update parser --- fortran_parser/parser.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index 83ea22ba6..e9195a99e 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -1165,6 +1165,7 @@ def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, if handled_pp: index += 1 continue + start = self._helper_classify_unit_start(stripped) if start is not None: end_index = self._helper_find_unit_end(lines, index, start[0], filename=filename) @@ -1188,12 +1189,6 @@ def _is_allowed_unparsed_file_scope_line(line: str) -> bool: lowered = stripped.lower() return ( stripped.startswith("#") - or lowered == "contains" - or lowered.startswith("end ") - or lowered.startswith(("endif", "enddo")) - or lowered == "else" - or lowered.startswith(("elseif", "else if")) - or FortranParser._is_ignored_spec_statement(stripped) or FortranParser._is_openmp_directive(stripped) ) From af20308375c501873ffc13c8ae091fbf9d72ce9b Mon Sep 17 00:00:00 2001 From: said Date: Sun, 31 May 2026 00:10:46 +0100 Subject: [PATCH 3/5] upadte error handling --- README.md | 24 +- c_parser/cli.py | 29 +- c_parser/parser.py | 125 ++-- docs/README.md | 5 + docs/c_parser/c_parser_architecture.md | 15 +- docs/c_parser/c_parser_cli_workflow.md | 37 +- docs/c_parser/c_parser_reference.md | 22 +- docs/diagnostic_codes.md | 56 ++ docs/fortran/fortran_parser.md | 30 +- .../parser_implementation_reference.md | 24 +- fortran_parser/cli.py | 4 +- fortran_parser/parser.py | 604 ++++++++++++++---- tests/parser/c/test_c_cli_skeleton.py | 34 +- .../c/test_c_declarations_and_declarators.py | 41 +- tests/parser/c/test_c_functions.py | 18 + tests/parser/test_cli.py | 10 +- .../test_declaration_and_interface_edges.py | 55 +- tests/parser/test_error_handling.py | 121 ++++ ...t_preprocessor_and_execution_boundaries.py | 7 +- x2py/cli.py | 40 +- 20 files changed, 1012 insertions(+), 289 deletions(-) create mode 100644 docs/diagnostic_codes.md diff --git a/README.md b/README.md index 828fb1863..284f54771 100644 --- a/README.md +++ b/README.md @@ -112,9 +112,19 @@ path when `--language` is omitted. C source/header files require explicit `--language fortran` or `--language c`. C parsing, semantic IR, `.pyi` generation, and wrap-readiness are available in explicit C mode. Selecting a frontend that conflicts with a recognized C or Fortran source suffix is an -error. Once selected, a frontend also rejects unmistakable declarations or -program-unit syntax that are not from the selected language outside ignored execution/function -bodies, rather than silently dropping them. +error. Once selected, a frontend validates the grammar regions it models and +rejects unparsed syntax outside intentionally ignored execution/function +bodies, rather than guessing another language from keyword spellings or +silently dropping malformed input. + +Parse failures print a compiler-style diagnostic without a Python traceback. +Use `--debug` to re-raise the parser error and print the traceback; +`--debug-traceback` remains accepted as a compatibility alias. Diagnostic codes +such as `PARSE001`, `CPARSE003`, and `CPARSE_INVALID_SYNTAX` are stable error +category identifiers for tests, tools, and documentation. Their numbers do not +represent the source line, the number of errors, or the process exit status. +The current categories are listed in +[`docs/diagnostic_codes.md`](docs/diagnostic_codes.md). For parse output, `--show-vars` expands scope-level variables that are normally summarized as `vars=N`. Use `--print-limit N` to keep large repeated sections @@ -718,8 +728,12 @@ visitor then parses only its own substring, splits it into header, specification, optional execution, and optional `contains` regions, and recurses into direct child units where that grammar allows children. Shared declaration helpers parse variables, procedure arguments/results, and type fields, then -push them into the active scope. Procedure execution bodies and internal -subprograms are ignored for wrapper metadata; procedure-local interfaces are +push them into the active scope. Nested unit boundaries and placement outside +execution regions are checked even when they do not produce wrapper metadata. +Internal procedures inside a host procedure's `contains` block are +structurally sliced, then their declarations and bodies are skipped. After an +execution boundary is detected, procedure bodies and standalone included +execution fragments are intentionally skipped. Procedure-local interfaces are retained for callback typing. Parameter variables keep both `value` and serialized `symbolic_value` when the parser has that information. `value` is literal/evaluated only; if an diff --git a/c_parser/cli.py b/c_parser/cli.py index c1ebc291f..7134bcfb6 100644 --- a/c_parser/cli.py +++ b/c_parser/cli.py @@ -3,15 +3,26 @@ import argparse import json +import os +import sys from collections.abc import Callable, Sequence from pathlib import Path from typing import Any -from .models import CFile, c_model_to_dict +from .models import CFile, CParseError, c_model_to_dict from .parser import CParser _C_SOURCE_SUFFIXES = {".c", ".h", ".i"} +_TRUE_VALUES = {"1", "true", "yes", "on"} + + +def _env_flag(name: str) -> bool: + return os.getenv(name, "").strip().lower() in _TRUE_VALUES + + +def _diagnostic_color_enabled(*, disabled: bool) -> bool: + return not disabled and "NO_COLOR" not in os.environ def _collect_c_extensions(path: Path) -> list[Path]: @@ -88,9 +99,23 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("paths", nargs="+", help="C source/header file(s) or directory path(s)") parser.add_argument("--json", action="store_true", help="Print JSON to stdout") parser.add_argument("--out", type=str, help="Write parser JSON to a file") + parser.add_argument("--no-color", action="store_true", help="Disable ANSI color in parse diagnostics") + parser.add_argument( + "--debug", + "--debug-traceback", + dest="debug", + action="store_true", + help="Re-raise parser errors so Python prints a traceback for parser debugging.", + ) args = parser.parse_args(argv) - payload = parse_c_report(args.paths) + try: + payload = parse_c_report(args.paths) + except CParseError as exc: + if args.debug or _env_flag("C_PARSER_DEBUG"): + raise + print(exc.format_diagnostic(color=_diagnostic_color_enabled(disabled=args.no_color), debug=False), file=sys.stderr) + return 1 if args.out: Path(args.out).write_text(json.dumps(payload, indent=2), encoding="utf-8") return 0 diff --git a/c_parser/parser.py b/c_parser/parser.py index eaf8da429..c8541695a 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -116,8 +116,6 @@ "_Alignas", "alignas", ) -_CXX_DECLARATION_KEYWORDS = {"using", "namespace", "template", "class"} -_CXX_ACCESS_SPECIFIERS = {"public", "private", "protected"} _RAW_CONDITIONAL_DIRECTIVE_RE = re.compile( r"^\s*#\s*(?Pif|ifdef|ifndef|elif|else|endif)\b" ) @@ -230,6 +228,12 @@ class _InvalidSpecifierSequence(ValueError): pass +class _InvalidCGrammarSyntax(ValueError): + """Raised internally when a nested C grammar region is malformed.""" + + pass + + def _looks_like_existing_source_path(value: object) -> bool: """Return whether `value` can safely be treated as an existing source path.""" if isinstance(value, Path): @@ -274,21 +278,6 @@ def _is_source_key(key: str) -> bool: return PurePosixPath(key).suffix.lower() == ".c" -def _looks_like_cxx_declaration(text: str) -> bool: - """Detect obvious C++ declarations so they become explicit diagnostics.""" - 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: """Parser orchestration object for the partial typed C model. @@ -584,7 +573,7 @@ def _could_start_c_external_declaration(text: str) -> bool: @staticmethod def _raise_for_invalid_top_level_syntax(segment: CTopLevelSegment) -> None: text = segment.text.strip() - if not text or _looks_like_cxx_declaration(text): + if not text: return tokens = lex_c_source(text) has_scope_operator = any( @@ -606,6 +595,25 @@ def _raise_for_invalid_top_level_syntax(segment: CTopLevelSegment) -> None: code="CPARSE_INVALID_SYNTAX", ) + def _invalid_syntax_error( + self, + segment: CTopLevelSegment, + text: str, + *, + context: str, + offset: int = 0, + ) -> CParseError: + """Build the fatal diagnostic used when a C grammar region is invalid.""" + location = self._source_location_at(segment, offset) + return CParseError( + f"Invalid C syntax in {context}: {text.strip()}", + filename=location.filename, + line_number=location.line, + column=location.column, + source_line=location.source_line, + code="CPARSE_INVALID_SYNTAX", + ) + def _macro_dependencies( self, source: str, @@ -1669,7 +1677,7 @@ def _parse_parameter(self, text: str) -> CParameter | None: return None spec_text, declarator = self._split_declaration_specifiers(stripped) if not spec_text: - return None + raise _InvalidCGrammarSyntax(f"Invalid parameter declaration: {stripped}") name, type_, _storage, _function_specifiers, _direct_function = self._build_declared_type( spec_text, declarator, @@ -1724,13 +1732,17 @@ def _parse_parameters(self, parameters_text: str) -> tuple[list[CParameter], boo parameters: list[CParameter] = [] variadic = False - for item in top_level_split(stripped, ","): + items = top_level_split(stripped, ",") + for index, item in enumerate(items): if item == "...": + if variadic or index != len(items) - 1: + raise _InvalidCGrammarSyntax("The variadic marker must be the final function parameter.") variadic = True continue parameter = self._parse_parameter(item) - if parameter is not None: - parameters.append(parameter) + if parameter is None: + raise _InvalidCGrammarSyntax(f"Invalid parameter declaration: {item}") + parameters.append(parameter) return parameters, variadic def _is_knr_definition(self, segment: CTopLevelSegment, parameters_text: str) -> bool: @@ -2196,6 +2208,13 @@ def _parse_fields( """Parse struct/union member declarations through the shared backend.""" members: list[CVariable] = [] diagnostics: list[CDiagnostic] = [] + if body.strip() and not body.rstrip().endswith(";"): + raise self._invalid_syntax_error( + segment, + body, + context=f"{owner_kind} field declaration", + offset=body_offset, + ) for text, field_offset in top_level_split_with_offsets(body, ";"): member_offset = body_offset + field_offset member_location = self._source_location_at(segment, member_offset) @@ -2239,17 +2258,21 @@ def _parse_fields( ) ) continue + if "::" in text: + raise self._invalid_syntax_error( + segment, + text, + context=f"{owner_kind} field declaration", + offset=member_offset, + ) spec_text, declarator_list = self._split_declaration_specifiers(text) if not spec_text or not declarator_list: - diagnostics.append( - self._field_diagnostic( - segment, - owner_kind, - "Unsupported field declaration.", - offset=member_offset, - ) + raise self._invalid_syntax_error( + segment, + text, + context=f"{owner_kind} field declaration", + offset=member_offset, ) - continue for declarator in top_level_split(declarator_list, ","): declaration, _initializer = top_level_partition(declarator, "=") declaration, bit_width = top_level_partition(declaration, ":") @@ -2293,10 +2316,10 @@ def _parse_enumerators(self, body: str, segment: CTopLevelSegment) -> list[CEnum name_text, value = top_level_partition(item, "=") identifier = self._read_identifier(name_text.strip(), 0) if identifier is None: - continue + raise self._invalid_syntax_error(segment, item, context="enum member") name, end = identifier if name_text[end:].strip(): - continue + raise self._invalid_syntax_error(segment, item, context="enum member") constants.append( CEnumerator( name=name, @@ -2402,7 +2425,6 @@ def _parse_declaration( not text or text.startswith("_Static_assert") or self._has_unsupported_declaration_marker(text) - or _looks_like_cxx_declaration(text) ): return [], [], [], [] @@ -2418,13 +2440,10 @@ def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDia if not text: return None - kind = "unsupported_declaration" - message = "Unsupported C declaration form." + kind = "" + message = "" - if _looks_like_cxx_declaration(text): - kind = "cxx_declaration" - message = "C++ declaration syntax is not supported by the C parser." - elif text.startswith("struct "): + if text.startswith("struct "): kind = "struct_definition" message = "Struct definitions are not supported yet." elif text.startswith("union "): @@ -2445,6 +2464,8 @@ def _unsupported_declaration_diagnostic(self, segment: CTopLevelSegment) -> CDia elif "{" in text or "}" in text: kind = "brace_declaration" message = "Unsupported declaration containing braces." + else: + return None return CDiagnostic( code="C_UNSUPPORTED_DECLARATION", @@ -2521,12 +2542,10 @@ 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) + try: + tag_definition = self._parse_tag_definition(segment) + except _InvalidCGrammarSyntax as error: + raise self._invalid_syntax_error(segment, str(error), context="nested declaration") from None if tag_definition is not None: aggregate, parsed_functions, parsed_typedefs, parsed_variables, parsed_diagnostics = tag_definition if isinstance(aggregate, CStruct): @@ -2551,6 +2570,8 @@ def _parse_translation_unit( except _UnsupportedDeclaratorSyntax as error: diagnostics.append(self._declarator_diagnostic(segment, str(error))) continue + except _InvalidCGrammarSyntax as error: + raise self._invalid_syntax_error(segment, str(error), context="function declaration") from None if function is not None: function.condition_set = condition_sets.get( segment.original_start_line, @@ -2562,7 +2583,8 @@ def _parse_translation_unit( unsupported = self._unsupported_declaration_diagnostic(segment) if unsupported is not None: diagnostics.append(unsupported) - continue + continue + raise self._invalid_syntax_error(segment, segment.text, context="top level") forward_tag = self._forward_tag(segment) if isinstance(forward_tag, CStruct): structs.append(forward_tag) @@ -2570,9 +2592,12 @@ def _parse_translation_unit( if isinstance(forward_tag, CUnion): unions.append(forward_tag) continue - parsed_functions, parsed_typedefs, parsed_variables, declarator_diagnostics = self._parse_declaration( - segment - ) + try: + parsed_functions, parsed_typedefs, parsed_variables, declarator_diagnostics = self._parse_declaration( + segment + ) + except _InvalidCGrammarSyntax as error: + raise self._invalid_syntax_error(segment, str(error), context="declaration") from None functions.extend(parsed_functions) for function in parsed_functions: function.condition_set = condition_sets.get( @@ -2593,6 +2618,8 @@ def _parse_translation_unit( unsupported = self._unsupported_declaration_diagnostic(segment) if unsupported is not None: diagnostics.append(unsupported) + else: + raise self._invalid_syntax_error(segment, segment.text, context="top level") return functions, structs, unions, enums, typedefs, variables, diagnostics diff --git a/docs/README.md b/docs/README.md index bd0e4ebd3..5f1b5a297 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,11 @@ The repository [`README.md`](../README.md) is the starting point for usage and examples. Contribution and pull-request requirements remain in [`CONTRIBUTING.md`](../CONTRIBUTING.md). +## Diagnostics + +- [Diagnostic code registry](diagnostic_codes.md): stable parser error and + report-diagnostic categories shared by the frontend documentation. + ## Architecture And Semantic Interfaces - [Semantic multilanguage wrapper runtime architecture](architecture/semantic_multilanguage_wrapper_runtime_architecture.md): diff --git a/docs/c_parser/c_parser_architecture.md b/docs/c_parser/c_parser_architecture.md index 89dde341a..e8c5849b9 100644 --- a/docs/c_parser/c_parser_architecture.md +++ b/docs/c_parser/c_parser_architecture.md @@ -65,13 +65,14 @@ Implemented now: pointer-adjusted effective `type` values. Declarations prefixed by unexpanded object-like macros are deferred as macro dependencies rather than misreported as invalid type sequences. Selected unsupported declaration - forms, including attributes, alignment specifiers, - C++-shaped declarations, 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 - `unsigned float` raise `CParseError` with code `CPARSE003` while a single - unresolved typedef-like name remains deferred. Definitions preserve direct + forms, including attributes, alignment specifiers 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. Grammar-invalid input raises `CParseError` with + `CPARSE_INVALID_SYNTAX`; identifier spellings are not used to guess another + language. Primitive specifier order is normalized, and invalid combinations + such as `unsigned float` raise `CParseError` with code `CPARSE003` while a + single 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, diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index 717b66706..3a5a90071 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -44,10 +44,10 @@ recognizable Fortran files and `.pyi` readiness inputs, but a `.c`, `.h`, or `.i` path fails with guidance to pass `--language c`; directory and unknown-suffix source inputs require an explicit frontend selection. A known C path explicitly passed with `--language fortran` is rejected before parsing, -so it cannot silently produce an empty Fortran interface. The parser also -rejects foreign syntax found in C input, and the Fortran parser -rejects unsupported non-Fortran syntax outside execution -regions that are intentionally not modeled. +so it cannot silently produce an empty Fortran interface. Each parser validates +the grammar regions it models and rejects unparsed syntax outside execution +regions that are intentionally not modeled. It does not guess a different +language from identifier or keyword spellings. The C parser output differs from Fortran parser output by using C-specific top-level sections: `functions`, `structs`, `unions`, `enums`, `typedefs`, @@ -86,8 +86,9 @@ member placement and flexible union members produce `parser_status: "partial"`. C parse diagnostics, currently including unsupported K&R-style function definitions and invalid primitive-specifier combinations such as `unsigned float`, honor `--no-color` and `NO_COLOR=1`. -Active CLI regression tests also verify that `--debug-traceback` and -`C_PARSER_DEBUG=1` re-raise fatal C parse errors for debugging. +Active CLI regression tests also verify that `--debug`, +`--debug-traceback`, and `C_PARSER_DEBUG=1` re-raise fatal C parse errors for +debugging. 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. Compatible repeated top-level declarations are merged; @@ -132,7 +133,8 @@ Important current behaviors to preserve: keeps parse and readiness payloads in separate top-level sections. - Parse diagnostics are compiler-style and go to stderr. - Python tracebacks are hidden by default. -- `--debug-traceback` or parser debug env vars re-raise parse errors. +- `--debug`, its compatibility alias `--debug-traceback`, or parser debug env + vars re-raise parse errors. - Diagnostics use ANSI color by default unless `--no-color` or `NO_COLOR=1` disables it. - Human parse output is a stable tree. @@ -180,7 +182,7 @@ Initial flags: --json --out [PATH] --no-color ---debug-traceback +--debug ``` Current C behavior also accepts `--no-color`. `CParseError` supports @@ -189,6 +191,7 @@ variable. The current grammar subset is tolerant for recoverable unsupported declaration forms, but invalid primitive-specifier combinations raise `CPARSE003`; unresolved single typedef-name uses are deferred until type resolution can determine whether a declaration exists. +`--debug-traceback` remains accepted as a compatibility alias. C-specific flags to add only when needed: @@ -539,10 +542,10 @@ Fatal C syntax errors use `CParseError` with the same user experience as `FortranParseError`: ```text -src/api.h:12:5: error[CPARSE001]: Unsupported declaration. +src/api.h:12:1: error[CPARSE_INVALID_SYNTAX]: Invalid C syntax at top level: @@@; | -12 | __attribute__((vector_size(16))) float v; - | ^ +12 | @@@; + | ^ ``` Default CLI behavior: @@ -563,9 +566,17 @@ src/api.h:12:1: error[CPARSE003]: Invalid type specifier sequence 'unsigned floa | ^ ``` +Grammar-invalid C syntax is also fatal and uses +`CPARSE_INVALID_SYNTAX`. Diagnostic codes are stable category identifiers for +tests, tools, and documentation. A numeric suffix such as the one in +`CPARSE003` is not a source line number, an occurrence counter, or an exit +status. The shared registry is +[`docs/diagnostic_codes.md`](../diagnostic_codes.md). + Debug behavior: -- `--debug-traceback` re-raises the error +- `--debug` re-raises the error +- `--debug-traceback` remains accepted as a compatibility alias - `C_PARSER_DEBUG=1` re-raises C parser errors - `FORTRAN_PARSER_DEBUG` should not control C behavior - a generic `X2PY_DEBUG=1` may be considered later @@ -588,7 +599,7 @@ The active CLI/parser tests cover the current partial subset: include/macro metadata and supported declarations when present. - `--language c --parse --out report.json` writes JSON and suppresses stdout. - `--language c --parse --no-color` is accepted. -- `--language c --parse --debug-traceback` is accepted. +- `--language c --parse --debug` is accepted. - raw comment stripping, line-continuation folding, top-level splitting, include collection, simple macro collection, function-like macro diagnostics, object-like macro declaration-prefix deferral, diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index feeffdd62..45c54c5ad 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -398,9 +398,10 @@ Member records carry their own field location. A legal final incomplete array member in a struct is marked as `CArray(is_flexible=True)`; non-final, sole-member, and union incomplete-array member forms are retained with `C_INVALID_FLEXIBLE_ARRAY_MEMBER` error diagnostics. -Selected unsupported forms, such as static assertions, -attributes, alignment specifiers, and C++-shaped declarations, are reported in `diagnostics` with -explicit `unit_kind` values. +Selected unsupported forms, such as static assertions, attributes, and +alignment specifiers, are reported in `diagnostics` with explicit `unit_kind` +values. Grammar-invalid input raises `CParseError`; identifier spellings are not +used to guess that input belongs to another language. Unconsumed declarator suffixes are also diagnosed instead of producing partial objects. Functions include `prototype_style`, currently `"prototype"` for @@ -490,7 +491,8 @@ x2py path/to/api.h --language c --parse --out report.json There is no separate `--parse-c` alias: `--language c --parse` is the shared language-selection form. Auto-detection remains deferred: a `.c`, `.h`, or `.i` input without `--language c` exits with language-selection guidance. -Explicit C input containing unmistakable non-C syntax raises a fatal parser diagnostic instead of emitting a partial C +Explicit C input containing syntax that cannot be consumed by the modeled C +grammar raises a fatal parser diagnostic instead of emitting a partial C interface. ## Current JSON Output @@ -599,6 +601,10 @@ specifier combinations also raise `CParseError` (`CPARSE003`) because their invalidity does not depend on later typedef resolution. Known unsupported declaration extensions are diagnosed rather than partially modeled; additional syntax diagnostics should be added only with focused tests. +Generic grammar rejection uses `CPARSE_INVALID_SYNTAX`. Diagnostic codes are +stable category identifiers for tests, tools, and documentation; numeric +suffixes are not line numbers, occurrence counters, or exit statuses. The +shared registry is [`docs/diagnostic_codes.md`](../diagnostic_codes.md). ## Testing Workflow @@ -668,10 +674,10 @@ Active declaration tests currently cover: references - `_Atomic int` and `_Atomic(type)` qualifier placement on scalar and pointer declaration forms -- diagnostics for selected unsupported attributes, alignment, C++-shaped - declarations, K&R definitions, and trailing declarator extensions -- fatal diagnostics for invalid primitive-specifier combinations while - unresolved single typedef-name uses remain deferred +- diagnostics for selected unsupported attributes, alignment, K&R definitions, + and trailing declarator extensions +- fatal diagnostics for grammar-invalid syntax and invalid primitive-specifier + combinations while unresolved single typedef-name uses remain deferred This is enough coverage for the currently implemented subset, not for all C declarations. diff --git a/docs/diagnostic_codes.md b/docs/diagnostic_codes.md new file mode 100644 index 000000000..8771886c0 --- /dev/null +++ b/docs/diagnostic_codes.md @@ -0,0 +1,56 @@ +# Diagnostic Codes + +Diagnostic codes are stable category identifiers for users, tests, and tooling. +They are not source line numbers, occurrence counters, or process exit statuses. + +New categories should use explicit symbolic names such as +`PARSE_INVALID_SYNTAX` or `C_UNRESOLVED_INCLUDE`. Existing numbered codes remain +supported for compatibility. Replacing a numbered code should be a deliberate +compatibility change with tests and generated fixtures updated together. + +## Fatal Parser Errors + +Fatal parser errors stop parsing and are rendered by the CLI without a Python +traceback unless `--debug` is used. + +| Code | Frontend | Meaning | +| --- | --- | --- | +| `PARSE001` | Fortran | Compatibility fallback for a Fortran parse error without a more specific code. | +| `PARSE_INVALID_SYNTAX` | Fortran | Syntax cannot be consumed in a modeled Fortran grammar region. | +| `PARSE_WRONG_ENTRYPOINT` | Fortran | A singular public parser API was called for a different source-unit kind. | +| `PARSE_AMBIGUOUS_ENTRYPOINT` | Fortran | A singular public parser API matched more than one source unit. | +| `CPARSE001` | C | Compatibility fallback for a C parse error without a more specific code. | +| `CPARSE002` | C | Unsupported K&R-style function definition. | +| `CPARSE003` | C | Invalid C primitive-specifier sequence. | +| `CPARSE_INVALID_SYNTAX` | C | Syntax cannot be consumed in a modeled C grammar region. | + +`PARSE001`, `CPARSE001`, `CPARSE002`, and `CPARSE003` predate the explicit +category naming rule. Prefer symbolic names for new categories. If the numbered +codes are migrated later, useful replacements would be names such as +`PARSE_UNKNOWN_DATATYPE`, `CPARSE_UNSUPPORTED_KNR_DEFINITION`, and +`CPARSE_INVALID_SPECIFIER_SEQUENCE`. + +## C Report Diagnostics + +The C parser can preserve partial metadata and attach `CDiagnostic` records. +These records do not necessarily stop parsing; inspect each diagnostic's +`severity`. + +| Code | Meaning | +| --- | --- | +| `C_UNRESOLVED_INCLUDE` | A local include could not be resolved. | +| `C_UNSUPPORTED_FUNCTION_LIKE_MACRO` | A function-like macro was recorded but not expanded. | +| `C_MACRO_DEPENDENT_DECLARATION` | Declaration parsing requires macro expansion. | +| `C_UNSUPPORTED_DECLARATION` | Recognized declaration form is outside the modeled subset. | +| `C_UNSUPPORTED_DECLARATOR` | Declarator form is outside the modeled subset. | +| `C_UNSUPPORTED_FIELD_DECLARATION` | Aggregate field form is outside the modeled subset. | +| `C_INVALID_FLEXIBLE_ARRAY_MEMBER` | Flexible array member placement is invalid. | +| `C_UNION_BY_VALUE` | A function uses a union by value and needs wrapper policy review. | +| `C_TYPEDEF_CYCLE` | Typedef resolution found a cycle. | +| `C_CONFLICTING_FUNCTION_DECLARATION` | Function declarations conflict. | +| `C_DUPLICATE_FUNCTION_DEFINITION` | Function has more than one definition. | +| `C_CONFLICTING_VARIABLE_DECLARATION` | File-scope variable declarations conflict. | +| `C_DUPLICATE_VARIABLE_DEFINITION` | File-scope variable has more than one definition. | +| `C_CONFLICTING_TYPEDEF` | Typedef declarations conflict. | +| `C_DUPLICATE_TAG_DEFINITION` | Struct, union, or enum tag has more than one definition. | + diff --git a/docs/fortran/fortran_parser.md b/docs/fortran/fortran_parser.md index 7a03b2046..f7d0244ec 100644 --- a/docs/fortran/fortran_parser.md +++ b/docs/fortran/fortran_parser.md @@ -114,10 +114,13 @@ programs, procedures, derived types, interfaces, and block data are expressed by small visitor decisions and grammar flags rather than separate whole-file parsing loops. -Procedure execution parts are ignored for wrapper metadata, and -procedure-internal subprograms are not exported as file/module procedures. -Procedure-local interface blocks are still visited enough to type callback -dummy arguments and to preserve interface metadata. +Nested unit boundaries and placement outside execution regions are checked even +when they are not exported as wrapper metadata. Internal procedures inside a +host procedure's `contains` block are structurally sliced, then their +declarations and bodies are skipped. Once an execution boundary is detected, +procedure bodies and standalone included execution fragments are intentionally +skipped. Procedure-local interface blocks are still visited enough to type +callback dummy arguments and to preserve interface metadata. ### 2.1 Recursive parser sketch @@ -476,14 +479,16 @@ python -m x2py bad.f90 --no-color NO_COLOR=1 python -m x2py bad.f90 ``` -For parser development, use `--debug-traceback` to re-raise +For parser development, use `--debug` to re-raise `FortranParseError` and let Python print the full traceback showing where the error was raised internally: ```bash -python -m x2py bad.f90 --debug-traceback +python -m x2py bad.f90 --debug ``` +`--debug-traceback` remains accepted as a compatibility alias. + The same developer mode can be enabled with the environment variable `FORTRAN_PARSER_DEBUG=1`: @@ -618,7 +623,13 @@ exception keeps structured metadata for consumers: - `line_number` — 1-based source line where the error was detected, if known - `source_line` — original source text for context, if known - `base_message` — stable error text without location/source context -- `code` — diagnostic code; the default parse diagnostic code is `PARSE001` +- `code` — stable diagnostic category identifier; the default parse diagnostic + code is `PARSE001`, while grammar rejection uses `PARSE_INVALID_SYNTAX` + +Diagnostic codes are for programmatic matching in tests, tools, and +documentation. The numeric suffix in `PARSE001` identifies an error category; +it is not a source line number, an occurrence counter, or the CLI exit status. +The shared registry is [`docs/diagnostic_codes.md`](../diagnostic_codes.md). `str(error)` and `error.format_diagnostic(color=False)` render a compiler-style diagnostic: @@ -642,8 +653,9 @@ disable ANSI output. On Windows, ANSI console compatibility is enabled through For parser development, `format_diagnostic(debug=True)` appends a note with the internal parser file, line, and function that raised the error. The CLI exposes -this through `--debug-traceback` or `FORTRAN_PARSER_DEBUG=1`; normal CLI parse -errors intentionally hide Python tracebacks. +this through `--debug`, its compatibility alias `--debug-traceback`, or +`FORTRAN_PARSER_DEBUG=1`; normal CLI parse errors intentionally hide Python +tracebacks. The sections below list each error category, the triggering condition, and the exact `base_message` format (with `<...>` placeholders for runtime values). diff --git a/docs/fortran/parser_implementation_reference.md b/docs/fortran/parser_implementation_reference.md index 38a7ea923..4f4662fd1 100644 --- a/docs/fortran/parser_implementation_reference.md +++ b/docs/fortran/parser_implementation_reference.md @@ -40,8 +40,9 @@ another source language. - `result(...)` (and tolerant `results(...)`) parsing for function results. - Procedure arguments retained in declared order. - Local variables are ignored for signature argument lists. -- Internal procedures inside `contains` blocks are ignored when parsing a - parent routine signature. +- Internal procedures inside `contains` blocks are structurally sliced, then + their declarations and bodies are ignored when parsing a parent routine + signature. - Interface-contained procedures flagged as `in_interface`. - Procedure-scope `import :: symbol` inside interface bodies is preserved on the parsed interface procedure signature as `import(symbol)`. @@ -299,7 +300,8 @@ Validates command-line behavior for: - JSON file writing - module/free-procedure name collision handling - parse-error diagnostics without tracebacks by default -- developer traceback opt-in through `--debug-traceback` and `FORTRAN_PARSER_DEBUG=1` +- developer traceback opt-in through `--debug`, its compatibility alias + `--debug-traceback`, and `FORTRAN_PARSER_DEBUG=1` - default ANSI color for diagnostics, with `--no-color` and `NO_COLOR=1` opt-out - parser JSON remains parse-only and does not include semantic readiness fields @@ -503,7 +505,9 @@ implemented today: procedure is treated as procedure metadata and emitted as an `import(symbol)` signature attribute, rather than as a module variable declaration. - **Internal procedure scope protection**: nested procedures in a host - `contains` block are not merged into the host routine signature. + `contains` block are structurally sliced to check their unit boundaries and + placement, but their declarations and bodies are not parsed or merged into + the host routine signature. - **Name-reuse safety across scopes**: fixtures/tests cover same identifier reuse in separate host/internal/type scopes to ensure no cross-scope symbol pollution. @@ -669,7 +673,8 @@ When updating parser behavior, keep this fail-fast contract aligned with tests: - `line_number` — 1-based line number in the original source where the error was detected - `source_line` — the original (pre-preprocessed) source line text - `base_message` — the stable error message without source/location context -- `code` — diagnostic code; current parser errors default to `PARSE001` +- `code` — stable diagnostic category identifier; current parser errors default + to `PARSE001`, while grammar rejection uses `PARSE_INVALID_SYNTAX` - `parser_file`, `parser_line_number`, `parser_function` — internal raise-site metadata used only for debug diagnostics The formatted `str()` of `FortranParseError` is a compiler-style diagnostic: @@ -686,14 +691,19 @@ Use `error.format_diagnostic(color=True)` to add ANSI color and line with the internal parser location. `format_diagnostic(debug=None)` also honors `FORTRAN_PARSER_DEBUG=1`. +The numeric suffix in a code such as `PARSE001` identifies an error category +for tests, tools, and documentation. It is not a line number, an occurrence +counter, or an exit status. The shared registry is +[`docs/diagnostic_codes.md`](../diagnostic_codes.md). + CLI contract: - End-user parse failures are caught, rendered to `stderr` with `format_diagnostic(...)`, and return exit status `1`; they do not print Python tracebacks by default. - CLI diagnostics request ANSI color by default when available. - `--no-color` and `NO_COLOR=1` disable ANSI color in CLI diagnostics. -- `--debug-traceback` re-raises `FortranParseError` so Python prints the full - traceback for parser developers. +- `--debug` re-raises `FortranParseError` so Python prints the full traceback + for parser developers. `--debug-traceback` remains a compatibility alias. - `FORTRAN_PARSER_DEBUG=1` enables the same traceback/debug behavior without changing command-line arguments. diff --git a/fortran_parser/cli.py b/fortran_parser/cli.py index d1dc05ae9..17d2d09d7 100644 --- a/fortran_parser/cli.py +++ b/fortran_parser/cli.py @@ -290,7 +290,9 @@ def main() -> int: help="Disable ANSI color in parse diagnostics. Diagnostics are colored by default when available.", ) parser.add_argument( + "--debug", "--debug-traceback", + dest="debug", action="store_true", help="Re-raise parser errors so Python prints a traceback for parser debugging. " "Can also be enabled with FORTRAN_PARSER_DEBUG=1.", @@ -305,7 +307,7 @@ def main() -> int: report = _parse_paths(args.paths) semantic = _semantic_report(args.paths) if (args.semantics or args.pyi) else None except FortranParseError as exc: - if args.debug_traceback or _env_flag("FORTRAN_PARSER_DEBUG"): + if args.debug or _env_flag("FORTRAN_PARSER_DEBUG"): raise print(exc.format_diagnostic(color=_diagnostic_color_enabled(disabled=args.no_color), debug=False), file=sys.stderr) return 1 diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index e9195a99e..90273c938 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -3,6 +3,7 @@ import re import ast +from copy import deepcopy from pathlib import Path from dataclasses import dataclass, replace @@ -60,7 +61,8 @@ its direct children. The contained procedure is dispatched to `visit_procedure_unit`, which creates a procedure scope and visits only its specification part; the execution part and internal subprograms are ignored for -wrapper metadata. +wrapper metadata. Internal subprogram boundaries are still sliced so malformed +unit structure is rejected before their contents are skipped. Scoping follows the same recursion. A helper that parses `integer :: n` or `real :: x(n)` receives a `_ParserScope` argument. The shared declaration parser @@ -562,6 +564,9 @@ def visit_source_unit( return self.visit_interface_unit(unit, parent_scope=parent_scope, filename=filename) if unit.kind == "procedure": return self.visit_procedure_unit(unit, parent_scope=parent_scope, filename=filename) + if unit.kind == "enum": + self._helper_validate_enum_unit(unit, filename=filename) + return None return None def visit_module_unit( @@ -581,6 +586,8 @@ def visit_module_unit( self._helper_visit_spec_part(scope, parts.specification, filename=filename) child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope, filename=filename) + self._helper_validate_child_unit_regions(unit, parts, child_units, filename=filename) + self._helper_validate_contains_lines(scope, parts.contains, filename=filename) self._helper_validate_sibling_units(child_units, parent_scope=scope, filename=filename) signatures = [ self.visit_procedure_unit(child, parent_scope=scope, filename=filename) @@ -597,6 +604,11 @@ def visit_module_unit( for child in child_units if child.kind == "interface" ] + self._helper_validate_ignored_child_units( + [child for child in child_units if child.kind == "enum"], + parent_scope=scope, + filename=filename, + ) module.procedures.extend(sig for sig in signatures if sig.module and sig.module.lower() == module.name.lower() and not sig.in_interface) module.derived_types.extend(dtype for dtype in types if dtype.module and dtype.module.lower() == module.name.lower()) module.interfaces.extend(iface for iface in interfaces if iface.module and iface.module.lower() == module.name.lower()) @@ -621,6 +633,8 @@ def visit_submodule_unit( self._helper_visit_spec_part(scope, parts.specification, filename=filename) child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope, filename=filename) + self._helper_validate_child_unit_regions(unit, parts, child_units, filename=filename) + self._helper_validate_contains_lines(scope, parts.contains, filename=filename) self._helper_validate_sibling_units(child_units, parent_scope=scope, filename=filename) signatures = [ self.visit_procedure_unit(child, parent_scope=scope, filename=filename) @@ -637,6 +651,11 @@ def visit_submodule_unit( for child in child_units if child.kind == "interface" ] + self._helper_validate_ignored_child_units( + [child for child in child_units if child.kind == "enum"], + parent_scope=scope, + filename=filename, + ) submodule.procedures.extend(sig for sig in signatures if sig.module and sig.module.lower() == submodule.name.lower() and not sig.in_interface) submodule.derived_types.extend(dtype for dtype in types if dtype.module and dtype.module.lower() == submodule.name.lower()) submodule.interfaces.extend(iface for iface in interfaces if iface.module and iface.module.lower() == submodule.name.lower()) @@ -658,6 +677,16 @@ def visit_program_unit( scope = self._helper_scope_for_model("program", program, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("program"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) + child_units = self._helper_nonexecution_child_units(unit, parent_scope=scope, filename=filename) + self._helper_validate_child_unit_regions(unit, parts, child_units, filename=filename) + self._helper_validate_contains_lines(scope, parts.contains, filename=filename) + self._helper_validate_ignored_child_units( + child_units, + parent_scope=scope, + filename=filename, + unit=unit, + parts=parts, + ) self._validate_variable_declarations( program.variables, owner_kind="program", @@ -681,6 +710,8 @@ def visit_block_data_source_unit( scope = self._helper_scope_for_model("block_data", block_data, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("block_data"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) + child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope, filename=filename) + self._helper_validate_child_unit_regions(unit, parts, child_units, filename=filename) self._validate_variable_declarations( block_data.variables, owner_kind="block data", @@ -715,6 +746,8 @@ def visit_derived_type_unit( lineno=lineno, source_line=source_line, ) + child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=scope, filename=filename) + self._helper_validate_child_unit_regions(unit, parts, child_units, filename=filename) self._validate_derived_type_fields(dtype, filename) return dtype @@ -732,13 +765,22 @@ def visit_interface_unit( raise FortranParseError("Expected interface unit.", filename=filename, line_number=header[1], source_line=header[2]) interface = FortranInterface(name=interface_name, module=parent_scope.module_owner) scope = self._helper_scope_for_model("interface", interface, parent=parent_scope) + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("interface"), filename=filename) + self._helper_validate_interface_lines(scope, parts.specification, filename=filename) child_units = self._helper_slice_child_units( unit.lines[1:-1], parent_scope=scope, - allowed_kinds={"procedure"}, filename=filename, ) for child in child_units: + if child.kind != "procedure": + self._raise_invalid_fortran_syntax_line( + child.lines[0][0] if child.lines else child.kind, + context=f"interface '{scope.name or ''}'", + filename=filename, + lineno=child.start_line, + source_line=child.lines[0][2] if child.lines else None, + ) sig = self.visit_procedure_unit(child, parent_scope=scope, filename=filename, in_interface=True) self._add_interface_attribute(sig, interface.name) interface.procedures.append(sig) @@ -778,7 +820,17 @@ def visit_procedure_unit( scope = self._helper_scope_for_model("procedure", proc_state["signature"], parent=parent_scope, state=proc_state) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("procedure"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) - self._helper_apply_local_interface_declarations(proc_state, unit, scope, filename=filename) + child_units = self._helper_nonexecution_child_units(unit, parent_scope=scope, filename=filename) + self._helper_validate_child_unit_regions(unit, parts, child_units, filename=filename) + self._helper_validate_contains_lines(scope, parts.contains, filename=filename) + self._helper_validate_ignored_child_units( + [child for child in child_units if child.kind != "interface"], + parent_scope=scope, + filename=filename, + unit=unit, + parts=parts, + ) + self._helper_apply_local_interface_declarations(proc_state, unit, parts, scope, filename=filename) return self._finalize_proc(proc_state) # ------------------------------------------------------------------ @@ -897,7 +949,6 @@ def _helper_prepare_source_units( """ lines = self._preprocessed_lines(code, filename) lines = self._helper_select_active_preprocessor_lines(lines, macro_defines) - self._helper_validate_unit_headers(lines, filename) root_scope = _ParserScope(kind="file", name=None) units = self._helper_slice_child_units(lines, parent_scope=root_scope, filename=filename) self._helper_validate_file_scope_unparsed_lines(lines, filename) @@ -918,8 +969,8 @@ def _collect_interface_source_units( lines, root_scope, _all_units = self._helper_prepare_source_units(code, filename) interfaces: list[tuple[_SourceUnit, _ParserScope]] = [] - def collect(scope: _ParserScope, source_lines: _PreprocessedLines) -> None: - for child in self._helper_slice_child_units(source_lines, parent_scope=scope, filename=filename): + def collect(scope: _ParserScope, child_units: list[_SourceUnit]) -> None: + for child in child_units: if child.kind == "interface": interfaces.append((child, scope)) continue @@ -930,18 +981,24 @@ def collect(scope: _ParserScope, source_lines: _PreprocessedLines) -> None: parent=scope, module_owner=child.name, ) - collect(child_scope, child.lines[1:-1]) + collect( + child_scope, + self._helper_nonexecution_child_units(child, parent_scope=child_scope, filename=filename), + ) continue - if child.kind in {"procedure", "program", "block_data"}: + if child.kind in {"procedure", "program"}: child_scope = _ParserScope( kind=child.kind, name=child.name, parent=scope, module_owner=scope.module_owner, ) - collect(child_scope, child.lines[1:-1]) + collect( + child_scope, + self._helper_nonexecution_child_units(child, parent_scope=child_scope, filename=filename), + ) - collect(root_scope, lines) + collect(root_scope, self._helper_slice_child_units(lines, parent_scope=root_scope, filename=filename)) return interfaces def _collect_derived_type_source_units( @@ -953,8 +1010,8 @@ def _collect_derived_type_source_units( lines, root_scope, _all_units = self._helper_prepare_source_units(code, filename) types: list[tuple[_SourceUnit, _ParserScope]] = [] - def collect(scope: _ParserScope, source_lines: _PreprocessedLines) -> None: - for child in self._helper_slice_child_units(source_lines, parent_scope=scope, filename=filename): + def collect(scope: _ParserScope, child_units: list[_SourceUnit]) -> None: + for child in child_units: if child.kind == "derived_type": types.append((child, scope)) continue @@ -965,18 +1022,24 @@ def collect(scope: _ParserScope, source_lines: _PreprocessedLines) -> None: parent=scope, module_owner=child.name if child.kind in {"module", "submodule"} else scope.module_owner, ) - collect(child_scope, child.lines[1:-1]) + collect( + child_scope, + self._helper_nonexecution_child_units(child, parent_scope=child_scope, filename=filename), + ) continue - if child.kind in {"procedure", "block_data"}: + if child.kind == "procedure": child_scope = _ParserScope( kind=child.kind, name=child.name, parent=scope, module_owner=scope.module_owner, ) - collect(child_scope, child.lines[1:-1]) + collect( + child_scope, + self._helper_nonexecution_child_units(child, parent_scope=child_scope, filename=filename), + ) - collect(root_scope, lines) + collect(root_scope, self._helper_slice_child_units(lines, parent_scope=root_scope, filename=filename)) return types def _helper_select_active_preprocessor_lines( @@ -1094,48 +1157,41 @@ def _handle_procedure_preprocessor_line( def _procedure_preprocessor_condition_set(pp_condition_stack: list[tuple[int, int]]) -> frozenset[str]: return frozenset(f"g{group_id}:b{branch_id}" for group_id, branch_id in pp_condition_stack) - def _helper_validate_unit_headers(self, lines: _PreprocessedLines, filename: str | None) -> None: - """Validate recognizable unit headers before slicing hides bad ones. - - The slicer intentionally ignores lines that are not valid starts. This - helper preserves diagnostics for malformed headers whose first keyword - still shows the user's intent. - - Example: - ``module :: bad_mod`` is not a valid module unit and therefore is - not returned by `_helper_slice_child_units`; this helper raises the - same explicit "malformed module header" error before parsing - continues. - """ - for line, lineno, source_line in lines: - stripped = line.strip() - if not stripped: - continue - self._parse_module_header(stripped, filename, lineno=lineno, source_line=source_line) - if stripped.lower().startswith("end "): - continue - if re.match(r"^module\s+procedure\s*::", stripped, flags=re.IGNORECASE): - continue - if not ( - stripped.lower().startswith("module procedure") - or self._looks_like_procedure_header(stripped) - ): - continue - if self._parse_procedure_header( + def _helper_validate_possible_unit_header( + self, + line: str, + *, + filename: str | None, + lineno: int | None, + source_line: str | None, + ) -> None: + """Validate a line that lexically resembles a source-unit header.""" + stripped = line.strip() + self._parse_module_header(stripped, filename, lineno=lineno, source_line=source_line) + if stripped.lower().startswith("end "): + return + if re.match(r"^module\s+procedure\s*::", stripped, flags=re.IGNORECASE): + return + if not ( + stripped.lower().startswith("module procedure") + or self._looks_like_procedure_header(stripped) + ): + return + if self._parse_procedure_header( + stripped, + None, + False, + filename=filename, + lineno=lineno, + source_line=source_line, + ) is None: + self._raise_if_unparsed_procedure_header( stripped, - None, - False, + in_interface=False, filename=filename, lineno=lineno, source_line=source_line, - ) is None: - self._raise_if_unparsed_procedure_header( - stripped, - in_interface=False, - filename=filename, - lineno=lineno, - source_line=source_line, - ) + ) def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, filename: str | None) -> None: """Reject any non-Fortran syntax outside recognized unit bodies. @@ -1166,12 +1222,23 @@ def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, index += 1 continue + self._helper_validate_possible_unit_header( + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) start = self._helper_classify_unit_start(stripped) if start is not None: end_index = self._helper_find_unit_end(lines, index, start[0], filename=filename) if end_index is not None: index = end_index + 1 continue + if self._is_executable_statement_start(stripped): + # A standalone include fragment can contain executable lines + # without an enclosing procedure. Once execution starts, the + # remaining fragment is intentionally opaque to this parser. + return if self._is_allowed_unparsed_file_scope_line(stripped): index += 1 continue @@ -1186,10 +1253,10 @@ def _helper_validate_file_scope_unparsed_lines(self, lines: _PreprocessedLines, @staticmethod def _is_allowed_unparsed_file_scope_line(line: str) -> bool: stripped = line.strip() - lowered = stripped.lower() return ( stripped.startswith("#") or FortranParser._is_openmp_directive(stripped) + or _REGEX["include"].match(stripped) ) @staticmethod @@ -1216,6 +1283,7 @@ def _helper_slice_child_units( parent_scope: _ParserScope, allowed_kinds: set[str] | None = None, filename: str | None = None, + skip_execution_region: bool = False, ) -> list[_SourceUnit]: """Slice direct child units from a parent source substring. @@ -1237,6 +1305,7 @@ def _helper_slice_child_units( pp_condition_stack: list[tuple[int, int]] = [] pp_active_stack: list[bool] = [] pp_group_counter = 0 + region = "specification" while index < len(lines): line, lineno, _ = lines[index] stripped = line.strip() @@ -1251,6 +1320,16 @@ def _helper_slice_child_units( if handled_pp: index += 1 continue + if skip_execution_region: + if self._is_contains_transition(stripped): + region = "contains" + index += 1 + continue + if region == "specification" and self._is_executable_statement_start(stripped): + region = "execution" + if region == "execution": + index += 1 + continue if parent_scope.kind == "interface" and re.match(r"^module\s+procedure\b", line.strip(), re.IGNORECASE): index += 1 continue @@ -1265,12 +1344,6 @@ def _helper_slice_child_units( end_index = self._helper_find_unit_end(lines, index, kind, filename=filename) if end_index is None: - if kind == "derived_type": - # A `type :: name` statement without a matching `end type` - # is treated as a declaration-like line for compatibility - # with existing tolerant parser behavior. - index += 1 - continue if kind == "interface" and (lines[index][2] or "").strip().lower().startswith("end interface"): index += 1 continue @@ -1326,8 +1399,8 @@ def _helper_find_unit_end( """ start = self._helper_classify_unit_start(lines[start_index][0]) start_name = start[1] if start is not None else None - stack: list[tuple[str, str | None, int | None, str | None]] = [ - (kind, start_name, lines[start_index][1], lines[start_index][2]) + stack: list[tuple[str, str | None, int | None, str | None, str]] = [ + (kind, start_name, lines[start_index][1], lines[start_index][2], "specification") ] idx = start_index + 1 while idx < len(lines): @@ -1336,34 +1409,57 @@ def _helper_find_unit_end( if not line: idx += 1 continue - start = self._helper_classify_unit_start(line) - current_kind, current_name, current_line, current_source = stack[-1] + current_kind, current_name, current_line, current_source, current_region = stack[-1] if current_kind == "interface" and re.match(r"^module\s+procedure\b", line, re.IGNORECASE): idx += 1 continue - if start is not None and self._helper_has_unit_end_ahead(lines, idx, start[0]): - nested_kind, _ = start - stack.append((nested_kind, start[1], lineno, source_line)) - idx += 1 - continue closes_current, end_name = self._helper_parse_unit_end(current_kind, line) if closes_current: - if current_kind != "procedure" and end_name and current_name and end_name.lower() != current_name.lower(): + if end_name and current_name and end_name.lower() != current_name.lower(): + if current_kind == "procedure" and self._helper_has_preferred_unit_end_ahead( + lines, + idx, + current_kind, + current_name, + ): + idx += 1 + continue label = self._helper_unit_label(current_kind) - raise FortranParseError( - f"Mismatched end {label} name '{end_name}' for {label} '{current_name}'.", - filename=filename, - line_number=lineno, - source_line=source_line, - ) + if current_kind != "procedure": + raise FortranParseError( + f"Mismatched end {label} name '{end_name}' for {label} '{current_name}'.", + filename=filename, + line_number=lineno, + source_line=source_line, + ) stack.pop() if not stack: return idx idx += 1 continue - for open_kind, open_name, open_line, open_source in reversed(stack): + grammar = self._helper_unit_grammar(current_kind) + if self._is_contains_transition(line) and grammar.has_contains_part: + stack[-1] = (current_kind, current_name, current_line, current_source, "contains") + idx += 1 + continue + if current_region == "specification" and grammar.has_execution_part and self._is_executable_statement_start(line): + stack[-1] = (current_kind, current_name, current_line, current_source, "execution") + idx += 1 + continue + if current_region == "execution": + idx += 1 + continue + + start = self._helper_classify_unit_start(line) + if start is not None and self._helper_has_unit_end_ahead(lines, idx, start[0]): + nested_kind, _ = start + stack.append((nested_kind, start[1], lineno, source_line, "specification")) + idx += 1 + continue + + for open_kind, open_name, open_line, open_source, _open_region in reversed(stack): closes_open, end_name = self._helper_parse_unit_end(open_kind, line) if not closes_open: continue @@ -1393,16 +1489,30 @@ def _helper_has_unit_end_ahead(self, lines: _PreprocessedLines, start_index: int """ start = self._helper_classify_unit_start(lines[start_index][0]) start_name = start[1] if start is not None else None + if self._helper_has_preferred_unit_end_ahead(lines, start_index, kind, start_name): + return True + if kind != "procedure": + return False for idx in range(start_index + 1, len(lines)): - matched, end_name = self._helper_parse_unit_end(kind, lines[idx][0]) - if not matched: - continue - if kind != "procedure" and start_name and end_name and end_name.lower() != start_name.lower(): - continue + matched, _end_name = self._helper_parse_unit_end(kind, lines[idx][0]) if matched: return True return False + def _helper_has_preferred_unit_end_ahead( + self, + lines: _PreprocessedLines, + start_index: int, + kind: str, + start_name: str | None, + ) -> bool: + """Return whether an exact or unnamed terminator exists later.""" + for idx in range(start_index + 1, len(lines)): + matched, end_name = self._helper_parse_unit_end(kind, lines[idx][0]) + if matched and (not start_name or not end_name or end_name.lower() == start_name.lower()): + return True + return False + def _helper_split_unit_parts( self, unit: _SourceUnit, @@ -1439,10 +1549,23 @@ def _helper_split_unit_parts( index += 1 continue if self._is_contains_transition(stripped): + if not grammar.has_contains_part: + self._raise_invalid_fortran_syntax_line( + stripped, + context=f"{self._helper_unit_label(grammar.kind)} '{unit.name or ''}'", + filename=filename, + lineno=body[index][1], + source_line=body[index][2], + ) region = "contains" index += 1 continue + if grammar.kind == "interface" and re.match(r"^module\s+procedure\b", stripped, re.IGNORECASE): + specification.append(body[index]) + index += 1 + continue + start = self._helper_classify_unit_start(stripped) if start is not None: child_kind, _ = start @@ -1450,6 +1573,8 @@ def _helper_split_unit_parts( if child_end is not None: index = child_end + 1 continue + if grammar.kind == "interface" and child_kind == "procedure": + break if ( region == "specification" @@ -1474,6 +1599,247 @@ def _helper_split_unit_parts( footer=footer, ) + def _helper_child_unit_region( + self, + unit: _SourceUnit, + parts: _UnitParts, + child: _SourceUnit, + ) -> str: + """Return the grammar region containing one direct child unit.""" + child_line = child.start_line + if child_line is None: + return "specification" + contains_line = self._helper_direct_contains_line(unit, filename=None) + if contains_line is not None and child_line > contains_line: + return "contains" + execution_line = next( + (lineno for _line, lineno, _source_line in parts.execution if lineno is not None), + None, + ) + if execution_line is not None and child_line >= execution_line: + return "execution" + return "specification" + + def _helper_nonexecution_child_units( + self, + unit: _SourceUnit, + *, + parent_scope: _ParserScope, + filename: str | None, + ) -> list[_SourceUnit]: + """Return direct nested units outside an intentionally skipped execution part.""" + grammar = self._helper_unit_grammar(unit.kind) + child_units = self._helper_slice_child_units( + unit.lines[1:-1], + parent_scope=parent_scope, + filename=filename, + skip_execution_region=grammar.has_execution_part, + ) + if not grammar.has_execution_part: + return child_units + parts = self._helper_split_unit_parts(unit, grammar, filename=filename) + return [ + child + for child in child_units + if self._helper_child_unit_region(unit, parts, child) != "execution" + ] + + def _helper_direct_contains_line( + self, + unit: _SourceUnit, + *, + filename: str | None, + ) -> int | None: + """Return the direct `contains` transition, skipping nested child units.""" + body = unit.lines[1:-1] + index = 0 + while index < len(body): + line, lineno, _source_line = body[index] + stripped = line.strip() + if self._is_contains_transition(stripped): + return lineno + start = self._helper_classify_unit_start(stripped) + if start is not None: + child_end = self._helper_find_unit_end(body, index, start[0], filename=filename) + if child_end is not None: + index = child_end + 1 + continue + index += 1 + return None + + def _helper_validate_child_unit_regions( + self, + unit: _SourceUnit, + parts: _UnitParts, + child_units: list[_SourceUnit], + *, + filename: str | None, + ) -> None: + """Reject child units that occur outside their parent's grammar region.""" + allowed = { + "module": { + "specification": {"derived_type", "interface", "enum"}, + "contains": {"procedure"}, + }, + "submodule": { + "specification": {"derived_type", "interface", "enum"}, + "contains": {"procedure"}, + }, + "program": { + "specification": {"derived_type", "interface", "enum"}, + "contains": {"procedure"}, + }, + "procedure": { + "specification": {"derived_type", "interface", "enum"}, + "contains": {"procedure"}, + }, + "derived_type": { + "specification": set(), + "contains": set(), + }, + "block_data": { + "specification": set(), + "contains": set(), + }, + "enum": { + "specification": set(), + "contains": set(), + }, + } + grammar_regions = allowed.get(unit.kind, {}) + for child in child_units: + region = self._helper_child_unit_region(unit, parts, child) + if region == "execution": + continue + if child.kind in grammar_regions.get(region, set()): + continue + self._raise_invalid_fortran_syntax_line( + child.lines[0][0] if child.lines else child.kind, + context=( + f"{self._helper_unit_label(unit.kind)} '{unit.name or ''}' " + f"{region} part" + ), + filename=filename, + lineno=child.start_line, + source_line=child.lines[0][2] if child.lines else None, + ) + + def _helper_validate_contains_lines( + self, + scope: _ParserScope, + lines: _PreprocessedLines, + *, + filename: str | None, + ) -> None: + """Validate non-child lines left in a `contains` region.""" + for line, lineno, source_line in lines: + stripped = line.strip() + if not stripped or stripped.startswith("#") or _REGEX["include"].match(stripped): + continue + if self._helper_is_valid_contains_alternative_line(scope, stripped): + continue + self._helper_validate_possible_unit_header( + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + self._raise_invalid_fortran_syntax_line( + stripped, + context=f"{self._helper_unit_label(scope.kind)} '{scope.name or ''}' contains part", + filename=filename, + lineno=lineno, + source_line=source_line, + ) + + def _helper_is_valid_contains_alternative_line(self, scope: _ParserScope, line: str) -> bool: + """Accept syntax from an unselected raw-preprocessor specification alternative.""" + scratch_scope = deepcopy(scope) + try: + self._helper_visit_spec_part(scratch_scope, [(line, None, None)], filename=None) + except FortranParseError: + return False + return True + + def _helper_validate_interface_lines( + self, + scope: _ParserScope, + lines: _PreprocessedLines, + *, + filename: str | None, + ) -> None: + """Validate interface statements that are not nested procedure bodies.""" + for line, lineno, source_line in lines: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if re.match(r"^module\s+procedure\s*(?:::)?\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s*$", stripped, re.IGNORECASE): + continue + if re.match(r"^procedure(?:\s*\([^)]*\))?(?:\s*,\s*[^:]*)?\s*::\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s*$", stripped, re.IGNORECASE): + continue + self._helper_validate_possible_unit_header( + stripped, + filename=filename, + lineno=lineno, + source_line=source_line, + ) + self._raise_invalid_fortran_syntax_line( + stripped, + context=f"interface '{scope.name or ''}'", + filename=filename, + lineno=lineno, + source_line=source_line, + ) + + def _helper_validate_enum_unit(self, unit: _SourceUnit, *, filename: str | None) -> None: + """Validate an interoperability enum block without exporting metadata.""" + parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("enum"), filename=filename) + for line, lineno, source_line in parts.specification: + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + match = re.match(r"^enumerator\s*(?:::)?\s*(?P.+)$", stripped, re.IGNORECASE) + if match and all( + re.match(r"^[A-Za-z_]\w*(?:\s*=\s*.+)?$", item.strip()) + for item in split_csv(match.group("items")) + ): + continue + self._raise_invalid_fortran_syntax_line( + stripped, + context="enum specification part", + filename=filename, + lineno=lineno, + source_line=source_line, + ) + child_units = self._helper_slice_child_units(unit.lines[1:-1], parent_scope=_ParserScope(kind="enum", name=unit.name), filename=filename) + self._helper_validate_child_unit_regions(unit, parts, child_units, filename=filename) + + def _helper_validate_ignored_child_units( + self, + child_units: list[_SourceUnit], + *, + parent_scope: _ParserScope, + filename: str | None, + unit: _SourceUnit | None = None, + parts: _UnitParts | None = None, + ) -> None: + """Check or skip nested units that are intentionally omitted from metadata.""" + for child in child_units: + if unit is not None and parts is not None: + if self._helper_child_unit_region(unit, parts, child) == "execution": + continue + if child.kind == "procedure": + # The slicer has already checked the nested unit boundary and + # the caller has checked its grammar region. Internal procedure + # declarations and bodies do not affect wrapper metadata. + continue + elif child.kind == "interface": + self.visit_interface_unit(child, parent_scope=parent_scope, filename=filename) + elif child.kind == "derived_type": + self.visit_derived_type_unit(child, parent_scope=parent_scope, filename=filename) + elif child.kind == "enum": + self._helper_validate_enum_unit(child, filename=filename) + def _helper_validate_sibling_units( self, units: list[_SourceUnit], @@ -1573,7 +1939,7 @@ def _helper_unit_grammar(self, kind: str) -> _UnitGrammar: has_contains_part=True, declaration_role="type_field", ), - "interface": _UnitGrammar(kind="interface", has_contains_part=True), + "interface": _UnitGrammar(kind="interface"), "block_data": _UnitGrammar(kind="block_data", declaration_role="module_variable"), "file": _UnitGrammar(kind="file", has_contains_part=True), } @@ -2193,7 +2559,13 @@ def _helper_visit_module_like_spec_line( return if _REGEX["derived_type"].match(stripped): - return + parsed_type = self._parse_derived_type_start(stripped) + raise FortranParseError( + f"Missing end derived type for derived type '{parsed_type[0] if parsed_type else ''}'.", + filename=filename, + line_number=lineno, + source_line=source_line, + ) if "::" in stripped: left, right = [x.strip() for x in stripped.split("::", 1)] @@ -2344,7 +2716,12 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename: raise FortranParseError("Derived-type specification scope is missing a target model.", filename=filename) stripped = line.strip() if re.match(r"^type\s*::\s*\w+$", stripped, re.IGNORECASE): - return + raise FortranParseError( + f"Missing end derived type for derived type '{stripped.split('::', 1)[1].strip()}'.", + filename=filename, + line_number=lineno, + source_line=source_line, + ) if stripped.lower() in {"sequence", "private"}: return if self._is_openmp_declarative_directive(stripped): @@ -2366,8 +2743,6 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename: if parsed: return if "::" not in stripped and not self._looks_like_declaration_or_spec(stripped): - if self._is_executable_statement_start(stripped): - return self._raise_invalid_fortran_syntax_line( stripped, context=f"type '{dtype.name}' specification part", @@ -2410,18 +2785,21 @@ def _parse_derived_type_contains_line( dtype.generic_bindings.append({"name": lhs, "targets": rhs, "attrs": attrs}) return - if self._looks_like_declaration_or_spec(line): - raise FortranParseError( - f"Unsupported or malformed type-bound declaration in type '{dtype.name}': {line.strip()}", - filename=filename, - line_number=lineno, - source_line=source_line, - ) + if re.match(r"^final\s*::\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s*$", line, re.IGNORECASE): + return + + raise FortranParseError( + f"Unsupported or malformed type-bound declaration in type '{dtype.name}': {line.strip()}", + filename=filename, + line_number=lineno, + source_line=source_line, + ) def _helper_apply_local_interface_declarations( self, proc_state: dict, unit: _SourceUnit, + parts: _UnitParts, scope: _ParserScope, *, filename: str | None, @@ -2444,34 +2822,14 @@ def _helper_apply_local_interface_declarations( parent_scope=scope, allowed_kinds={"interface"}, filename=filename, + skip_execution_region=True, ) for interface_unit in interface_units: - interface_scope = _ParserScope( - kind="interface", - name=interface_unit.name, - parent=scope, - module_owner=scope.module_owner, - ) - for child in self._helper_slice_child_units( - interface_unit.lines[1:-1], - parent_scope=interface_scope, - allowed_kinds={"procedure"}, - filename=filename, - ): - header = child.lines[0] if child.lines else None - if header is None: - continue - parsed = self._parse_procedure_header( - header[0].strip(), - scope.module_owner, - True, - filename=filename, - lineno=header[1], - source_line=header[2], - ) - if parsed is None: - continue - name = parsed["signature"].name + if self._helper_child_unit_region(unit, parts, interface_unit) == "execution": + continue + interface = self.visit_interface_unit(interface_unit, parent_scope=scope, filename=filename) + for signature in interface.procedures: + name = signature.name if self._proc_scope_symbol_is_declared(proc_state, name): key = self._scope_key(name) else: @@ -2479,8 +2837,8 @@ def _helper_apply_local_interface_declarations( proc_state, name, filename=filename, - line_number=header[1], - source_line=header[2], + line_number=interface_unit.start_line, + source_line=interface_unit.lines[0][2] if interface_unit.lines else None, ) arg = self._proc_scope_get_symbol(proc_state, key) if arg is not None and arg.base_type == "unknown": diff --git a/tests/parser/c/test_c_cli_skeleton.py b/tests/parser/c/test_c_cli_skeleton.py index 2a9b25e6b..13890273b 100644 --- a/tests/parser/c/test_c_cli_skeleton.py +++ b/tests/parser/c/test_c_cli_skeleton.py @@ -300,7 +300,7 @@ def test_cli_c_invalid_primitive_specifier_sequence_is_fatal(tmp_path: Path): assert "\x1b[" not in res.stderr -def test_cli_c_debug_traceback_reraises_parse_errors(tmp_path: Path): +def test_cli_c_debug_reraises_parse_errors(tmp_path: Path): header = tmp_path / "invalid_specifiers.h" header.write_text("unsigned float value;\n", encoding="utf-8") cmd = [ @@ -311,7 +311,7 @@ def test_cli_c_debug_traceback_reraises_parse_errors(tmp_path: Path): "--language", "c", "--parse", - "--debug-traceback", + "--debug", ] res = subprocess.run(cmd, capture_output=True, text=True) @@ -394,6 +394,36 @@ def test_c_parser_module_entrypoint_and_compatibility_exports(tmp_path: Path): assert c_utils.__all__ == () +def test_c_parser_module_formats_parse_errors_without_traceback(tmp_path: Path): + header = tmp_path / "invalid.h" + header.write_text("@@@;\n", encoding="utf-8") + + result = subprocess.run( + [sys.executable, "-m", "c_parser", str(header), "--no-color"], + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "error[CPARSE_INVALID_SYNTAX]" in result.stderr + assert "Traceback" not in result.stderr + + +def test_c_parser_module_debug_reraises_parse_errors(tmp_path: Path): + header = tmp_path / "invalid.h" + header.write_text("@@@;\n", encoding="utf-8") + + result = subprocess.run( + [sys.executable, "-m", "c_parser", str(header), "--debug"], + capture_output=True, + text=True, + ) + + assert result.returncode == 1 + assert "Traceback" in result.stderr + assert "CParseError" in result.stderr + + def test_x2py_c_compiler_source_loader_drives_semantics_and_readiness(tmp_path: Path, monkeypatch): header = tmp_path / "api.h" header.write_text("API(int) add(int a, int b);\n", encoding="utf-8") diff --git a/tests/parser/c/test_c_declarations_and_declarators.py b/tests/parser/c/test_c_declarations_and_declarators.py index e40c54c27..f9b291233 100644 --- a/tests/parser/c/test_c_declarations_and_declarators.py +++ b/tests/parser/c/test_c_declarations_and_declarators.py @@ -571,30 +571,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 +def test_non_c_top_level_grammar_is_rejected_without_language_guessing(source): + from c_parser import CParseError, parse_c_file - parsed = parse_c_file(source, filename="cxx_shapes.h") + with pytest.raises(CParseError, match="Invalid C syntax") as exc_info: + parse_c_file(source, filename="invalid_top_level.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)] + assert exc_info.value.code == "CPARSE_INVALID_SYNTAX" + + +@pytest.mark.parametrize( + ("source", "name", "type_name"), + [ + ("class widget;\n", "widget", "class"), + ("namespace api = other;\n", "api", "namespace"), + ("using size_type = value;\n", "size_type", "using"), + ], +) +def test_identifier_spelling_does_not_trigger_foreign_language_detection(source, name, type_name): + from c_parser import CTypedef, parse_c_file + + parsed = parse_c_file(source, filename="identifier_spelling.h") + + assert [variable.name for variable in parsed.variables] == [name] + assert isinstance(parsed.variables[0].type, CTypedef) + assert parsed.variables[0].type.name == type_name def test_braced_and_designated_initializer_declarations_preserve_source_text(): diff --git a/tests/parser/c/test_c_functions.py b/tests/parser/c/test_c_functions.py index 98f5d8932..df37d328a 100644 --- a/tests/parser/c/test_c_functions.py +++ b/tests/parser/c/test_c_functions.py @@ -165,6 +165,24 @@ def test_c_parser_ignores_invalid_syntax_inside_function_body(): assert [function.name for function in parsed.functions] == ["run"] +@pytest.mark.parametrize( + "source", + [ + "struct bad { @@@; };\n", + "enum bad { OK, @@@ };\n", + "int run(@@@);\n", + "int run(int first, ..., int last);\n", + ], +) +def test_c_parser_rejects_invalid_nested_grammar_units(source): + from c_parser import CParseError, parse_c_file + + with pytest.raises(CParseError, match="Invalid C syntax") as exc_info: + parse_c_file(source, filename="invalid_nested.h") + + assert exc_info.value.code == "CPARSE_INVALID_SYNTAX" + + def test_control_flow_conditions_inside_function_body_do_not_look_like_knr_definitions(): from c_parser import parse_c_file diff --git a/tests/parser/test_cli.py b/tests/parser/test_cli.py index 1a3193098..f292eb796 100644 --- a/tests/parser/test_cli.py +++ b/tests/parser/test_cli.py @@ -193,7 +193,7 @@ def test_cli_formats_parse_errors_without_traceback(tmp_path: Path): assert "2 | weirdtype :: x" in res.stderr -def test_cli_debug_traceback_flag_reraises_parse_errors(tmp_path: Path): +def test_cli_debug_flag_reraises_parse_errors(tmp_path: Path): f90 = tmp_path / "bad.f90" f90.write_text( """subroutine bad(x) @@ -203,7 +203,7 @@ def test_cli_debug_traceback_flag_reraises_parse_errors(tmp_path: Path): encoding="utf-8", ) - cmd = [sys.executable, "-m", "x2py", str(f90), "--parse", "--debug-traceback"] + cmd = [sys.executable, "-m", "x2py", str(f90), "--parse", "--debug"] res = subprocess.run(cmd, capture_output=True, text=True) assert res.returncode == 1 @@ -853,7 +853,7 @@ def test_x2py_cli_rejects_invalid_stage_combinations(extra_args, message): assert message in res.stderr -def test_fortran_parser_cli_debug_traceback_flag_reraises_parse_errors(tmp_path: Path): +def test_fortran_parser_cli_debug_flag_reraises_parse_errors(tmp_path: Path): f90 = tmp_path / "bad.f90" f90.write_text( """subroutine bad(x) @@ -863,7 +863,7 @@ def test_fortran_parser_cli_debug_traceback_flag_reraises_parse_errors(tmp_path: encoding="utf-8", ) - cmd = [sys.executable, "-m", "fortran_parser", str(f90), "--debug-traceback"] + cmd = [sys.executable, "-m", "fortran_parser", str(f90), "--debug"] res = subprocess.run(cmd, capture_output=True, text=True) assert res.returncode == 1 @@ -993,6 +993,6 @@ def fail_parse(_paths, _preprocessing): assert x2py_cli.main() == 1 assert "x2py: error: invalid generated interface" in capsys.readouterr().err - monkeypatch.setattr(sys, "argv", ["x2py", str(source), "--parse", "--debug-traceback"]) + monkeypatch.setattr(sys, "argv", ["x2py", str(source), "--parse", "--debug"]) with pytest.raises(ValueError, match="invalid generated interface"): x2py_cli.main() diff --git a/tests/parser/test_declaration_and_interface_edges.py b/tests/parser/test_declaration_and_interface_edges.py index 5c02bdea6..8dd533547 100644 --- a/tests/parser/test_declaration_and_interface_edges.py +++ b/tests/parser/test_declaration_and_interface_edges.py @@ -324,39 +324,31 @@ def test_local_compile_time_arithmetic_is_folded_for_shapes_and_parameters(): ] assert sig.variables["one"].value == "1" -def test_type_contains_ignores_executable_like_lines_and_rejects_bad_declarations(): - ok_code = """ -module type_contains_ok_mod +def test_type_contains_accepts_bindings_and_rejects_other_lines(): + valid_code = """ +module type_contains_valid_mod type :: state contains - call ignored_statement() + procedure :: update + final :: destroy end type state -end module type_contains_ok_mod +end module type_contains_valid_mod """ - bad_code = """ + + parsed = parse_fortran_file(valid_code, filename="type_contains_valid.f90") + assert parsed.modules[0].derived_types[0].methods == ["update"] + + for invalid_line in ("call ignored_statement()", "!$omp declare target", "integer, public :: bad_binding"): + code = f""" module type_contains_bad_mod type :: state contains -!$omp declare target + {invalid_line} end type state end module type_contains_bad_mod """ - comma_bad_code = """ -module type_contains_comma_bad_mod - type :: state - contains - integer, public :: bad_binding - end type state -end module type_contains_comma_bad_mod -""" - - parsed = parse_fortran_file(ok_code, filename="type_contains_ok.f90") - assert parsed.modules[0].derived_types[0].methods == [] - - with pytest.raises(FortranParseError, match="Unsupported or malformed type-bound declaration"): - parse_fortran_file(bad_code, filename="type_contains_omp.f90") - with pytest.raises(FortranParseError, match="Unsupported or malformed type-bound declaration"): - parse_fortran_file(comma_bad_code, filename="type_contains_comma.f90") + with pytest.raises(FortranParseError, match="Unsupported or malformed type-bound declaration"): + parse_fortran_file(code, filename="type_contains_bad.f90") def test_malformed_type_bound_declaration_raises(): code = """ @@ -387,10 +379,8 @@ def test_type_field_spec_variants_and_empty_entities_from_public_source(): code = """ module type_field_edges_mod type :: state - type :: nested_marker sequence private - call ignored_in_type_spec() integer :: first, , second end type state end module type_field_edges_mod @@ -400,6 +390,19 @@ def test_type_field_spec_variants_and_empty_entities_from_public_source(): assert [field.name for field in dtype.fields] == ["first", "second"] +@pytest.mark.parametrize("invalid_line", ["type :: nested_marker", "call invalid_in_type_spec()"]) +def test_type_field_specification_rejects_invalid_nested_syntax(invalid_line): + code = f""" +module type_field_invalid_mod + type :: state + {invalid_line} + end type state +end module type_field_invalid_mod +""" + + with pytest.raises(FortranParseError): + parse_fortran_file(code, filename="type_field_invalid.f90") + def test_module_like_declaration_edges_from_program_and_module_sources(): module_code = """ module module_spec_edges_mod @@ -411,6 +414,8 @@ def test_module_like_declaration_edges_from_program_and_module_sources(): program_code = """ program type_stmt_program type :: local_state + integer :: marker + end type local_state integer :: kept end program type_stmt_program """ diff --git a/tests/parser/test_error_handling.py b/tests/parser/test_error_handling.py index 68f621586..4c1ca5cba 100644 --- a/tests/parser/test_error_handling.py +++ b/tests/parser/test_error_handling.py @@ -739,6 +739,18 @@ def test_slicer_reports_mismatched_end_unit_name(): parse_fortran_file(code, filename="mismatch_module.f90") +def test_slicer_accepts_mismatched_procedure_end_name_without_preferred_alternative(): + parsed = parse_fortran_file( + """ +subroutine expected_name() +end subroutine alternate_name +""", + filename="mismatch_procedure_raw_alternative.f90", + ) + + assert parsed.procedures[0].name == "expected_name" + + def test_slicer_reports_missing_end_unit(): code = """ module missing_end @@ -821,6 +833,115 @@ def test_fortran_parser_ignores_invalid_syntax_after_execution_boundary(): assert parsed.procedures[0].name == "ignored_body" +def test_fortran_parser_skips_standalone_include_fragment_after_execution_boundary(): + parsed = parse_fortran_file( + """ +include 'fragment.inc' +if (enabled) then + @@@ +else + @@@ +endif +""", + filename="fragment.inc", + ) + + assert parsed.procedures == [] + + +def test_fortran_parser_skips_balanced_internal_procedure_contents(): + parsed = parse_fortran_file( + """ +subroutine host() +contains + subroutine nested() + @@@ + end subroutine nested +end subroutine host +""", + filename="ignored_internal_body.f90", + ) + + assert parsed.procedures[0].name == "host" + + +def test_fortran_parser_rejects_unterminated_internal_procedure_unit(): + with pytest.raises(FortranParseError, match="Missing end procedure"): + parse_fortran_file( + """ +subroutine host() +contains + subroutine nested() +end subroutine host +""", + filename="unterminated_internal_unit.f90", + ) + + +def test_fortran_parser_skips_nested_unit_like_lines_after_execution_boundary(): + parsed = parse_fortran_file( + """ +subroutine host() + call begin_work() + interface + subroutine ignored() + @@@ + end subroutine ignored + end interface +end subroutine host +""", + filename="ignored_nested_execution.f90", + ) + + assert parsed.procedures[0].name == "host" + + +def test_fortran_parser_skips_unterminated_unit_like_lines_after_execution_boundary(): + parsed = parse_fortran_file( + """ +subroutine host() + call begin_work() + subroutine ignored() +end subroutine host +""", + filename="ignored_unterminated_nested_execution.f90", + ) + + assert parsed.procedures[0].name == "host" + + +def test_fortran_parser_rejects_malformed_enum_subunit(): + with pytest.raises(FortranParseError, match="Invalid Fortran syntax") as exc_info: + parse_fortran_file( + """ +module invalid_enum_mod + enum, bind(c) + enumerator :: valid = 1 + @@@ + end enum +end module invalid_enum_mod +""", + filename="invalid_enum.f90", + ) + + assert exc_info.value.code == "PARSE_INVALID_SYNTAX" + + +def test_fortran_parser_rejects_subunit_inside_block_data(): + with pytest.raises(FortranParseError, match="Invalid Fortran syntax") as exc_info: + parse_fortran_file( + """ +block data invalid_block + interface + end interface +end block data invalid_block +""", + filename="invalid_block_data.f90", + ) + + assert exc_info.value.code == "PARSE_INVALID_SYNTAX" + + def test_invalid_syntax_guard_preserves_valid_semicolon_separated_fortran_statements(): parsed = parse_fortran_file( """ diff --git a/tests/parser/test_preprocessor_and_execution_boundaries.py b/tests/parser/test_preprocessor_and_execution_boundaries.py index b98b797b5..e05a63d7e 100644 --- a/tests/parser/test_preprocessor_and_execution_boundaries.py +++ b/tests/parser/test_preprocessor_and_execution_boundaries.py @@ -302,8 +302,9 @@ def test_implicit_mapping_parameter_noise_and_assignment_lines_do_not_break_proc assert sig.arguments[0].name == "x" assert sig.arguments[0].base_type == "real" -def test_stray_end_unit_lines_are_ignored_by_public_file_parse(): - parsed = parse_fortran_file( +def test_stray_end_unit_lines_are_rejected_by_public_file_parse(): + with pytest.raises(FortranParseError, match="Invalid Fortran syntax") as exc_info: + parse_fortran_file( """ end module stray_mod end submodule stray_submod @@ -316,4 +317,4 @@ def test_stray_end_unit_lines_are_ignored_by_public_file_parse(): filename="stray_ends.f90", ) - assert [proc.name for proc in parsed.procedures] == ["kept"] + assert exc_info.value.code == "PARSE_INVALID_SYNTAX" diff --git a/x2py/cli.py b/x2py/cli.py index 81945275c..ec2c59abc 100644 --- a/x2py/cli.py +++ b/x2py/cli.py @@ -27,6 +27,10 @@ _TRUE_VALUES = {"1", "true", "yes", "on"} _FORTRAN_SOURCE_SUFFIXES = {".f", ".for", ".ftn", ".f77", ".f90", ".f95", ".f03", ".f08"} _C_SOURCE_SUFFIXES = {".c", ".h", ".i"} +_SOURCE_SUFFIXES_BY_LANGUAGE = { + "fortran": _FORTRAN_SOURCE_SUFFIXES, + "c": _C_SOURCE_SUFFIXES, +} def _env_flag(name: str) -> bool: @@ -94,21 +98,27 @@ def _resolve_language( requested: str | None, parser: argparse.ArgumentParser, ) -> str: + def language_for_suffix(suffix: str) -> str | None: + return next( + ( + language + for language, suffixes in _SOURCE_SUFFIXES_BY_LANGUAGE.items() + if suffix in suffixes + ), + None, + ) + if requested is not None: for raw in paths: path = Path(raw) if path.is_dir(): continue suffix = path.suffix.lower() - if requested == "fortran" and suffix in _C_SOURCE_SUFFIXES: - parser.error( - f"C input {path} is incompatible with --language fortran; " - "pass --language c. Use --help for examples." - ) - if requested == "c" and suffix in _FORTRAN_SOURCE_SUFFIXES: + detected = language_for_suffix(suffix) + if detected is not None and detected != requested: parser.error( - f"Fortran input {path} is incompatible with --language c; " - "pass --language fortran. Use --help for examples." + f"{detected.capitalize()} input {path} is incompatible with --language {requested}; " + f"pass --language {detected}. Use --help for examples." ) return requested @@ -610,7 +620,13 @@ def main() -> int: parser.add_argument("--json", action="store_true", help="Print JSON to stdout") parser.add_argument("--out", nargs="?", const="", type=str, help="Write stage output to file (optional explicit output filename)") parser.add_argument("--no-color", action="store_true", help="Disable ANSI color in parse diagnostics") - parser.add_argument("--debug-traceback", action="store_true", help="Re-raise parser errors for debug") + parser.add_argument( + "--debug", + "--debug-traceback", + dest="debug", + action="store_true", + help="Re-raise parser errors so Python prints a traceback for parser debugging", + ) args = parser.parse_args() args.language = _resolve_language(args.paths, args.language, parser) preprocessing = _build_preprocessing_config(args, parser) @@ -649,17 +665,17 @@ def main() -> int: readiness_payload = _wrap_readiness_report(args.paths, preprocessing, language=args.language) if args.wrap_readiness else None _attach_wrap_readiness(semantic_payload, readiness_payload) except CParseError as exc: - if args.debug_traceback or _env_flag("C_PARSER_DEBUG"): + if args.debug or _env_flag("C_PARSER_DEBUG"): raise print(exc.format_diagnostic(color=_diagnostic_color_enabled(disabled=args.no_color), debug=False), file=sys.stderr) return 1 except FortranParseError as exc: - if args.debug_traceback or _env_flag("FORTRAN_PARSER_DEBUG"): + if args.debug or _env_flag("FORTRAN_PARSER_DEBUG"): raise print(exc.format_diagnostic(color=_diagnostic_color_enabled(disabled=args.no_color), debug=False), file=sys.stderr) return 1 except (SyntaxError, ValueError) as exc: - if args.debug_traceback or _env_flag("X2PY_DEBUG"): + if args.debug or _env_flag("X2PY_DEBUG"): raise print(f"x2py: error: {exc}", file=sys.stderr) return 1 From c0a5dba2ffb7a2312884509477d30650ef5ca392 Mon Sep 17 00:00:00 2001 From: said Date: Sun, 31 May 2026 00:21:55 +0100 Subject: [PATCH 4/5] fix issues --- tests/parser/c/fixtures/stb/stb_ds.json | 72 ++++++++++++------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/parser/c/fixtures/stb/stb_ds.json b/tests/parser/c/fixtures/stb/stb_ds.json index 2613585bc..65c97b266 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_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "code": "C_UNSUPPORTED_DECLARATOR", + "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmput_default_wrapper(T *a, size_t elemsize)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { @@ -13614,8 +13614,8 @@ "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "code": "C_UNSUPPORTED_DECLARATOR", + "message": "Unsupported declarator syntax after parsed type layers: ' static T * stbds_hmput_default_wrapper(T *a, size_t elemsize)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { - "code": "C_UNSUPPORTED_DECLARATION", - "message": "C++ declaration syntax is not supported by the C parser.", + "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)'.", "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": "cxx_declaration", + "unit_kind": "declarator", "unit_name": null }, { From 612f642895215a04fe385ff73f211a7e3b85710f Mon Sep 17 00:00:00 2001 From: said Date: Sun, 31 May 2026 04:26:31 +0100 Subject: [PATCH 5/5] update errors --- README.md | 7 +- c_parser/models.py | 2 +- c_parser/parser.py | 8 +- docs/c_parser/c_parser_architecture.md | 6 +- docs/c_parser/c_parser_cli_workflow.md | 14 ++-- docs/c_parser/c_parser_reference.md | 13 +-- docs/diagnostic_codes.md | 49 +++++++---- docs/fortran/fortran_parser.md | 82 ++++++------------- .../parser_implementation_reference.md | 14 ++-- fortran_parser/models.py | 2 +- fortran_parser/parser.py | 78 +++++++++++++++--- .../errors/invalid_type_specifiers.h.json | 2 +- tests/parser/c/test_c_cli_skeleton.py | 2 +- .../c/test_c_declarations_and_declarators.py | 4 +- tests/parser/c/test_c_public_api_skeleton.py | 4 +- .../errors/err_duplicate_argument_name.json | 2 +- .../err_duplicate_declaration_procedure.json | 2 +- .../err_duplicate_field_derived_type.json | 2 +- .../errors/err_duplicate_parameter.json | 2 +- .../err_duplicate_procedure_global.json | 2 +- .../err_duplicate_procedure_module.json | 2 +- .../errors/err_duplicate_variable_module.json | 2 +- .../err_implicit_none_undeclared_arg.json | 2 +- .../err_implicit_none_undeclared_result.json | 2 +- ..._parameter_without_type_implicit_none.json | 2 +- .../errors/err_result_shadows_argument.json | 2 +- .../errors/err_unknown_function_result.json | 2 +- .../errors/err_unknown_type_derived_type.json | 2 +- .../errors/err_unknown_type_module.json | 2 +- .../errors/err_unknown_type_procedure.json | 2 +- tests/parser/test_cli.py | 6 +- tests/parser/test_error_handling.py | 6 +- 32 files changed, 184 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 284f54771..f9ad1c663 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,9 @@ silently dropping malformed input. Parse failures print a compiler-style diagnostic without a Python traceback. Use `--debug` to re-raise the parser error and print the traceback; `--debug-traceback` remains accepted as a compatibility alias. Diagnostic codes -such as `PARSE001`, `CPARSE003`, and `CPARSE_INVALID_SYNTAX` are stable error -category identifiers for tests, tools, and documentation. Their numbers do not -represent the source line, the number of errors, or the process exit status. -The current categories are listed in +such as `PARSE_UNSUPPORTED_DECLARATION`, `CPARSE_INVALID_SPECIFIER_SEQUENCE`, +and `CPARSE_INVALID_SYNTAX` are stable, explicit error-category identifiers for +tests, tools, and documentation. The current categories are listed in [`docs/diagnostic_codes.md`](docs/diagnostic_codes.md). For parse output, `--show-vars` expands scope-level variables that are normally diff --git a/c_parser/models.py b/c_parser/models.py index 47d344d93..e5e2d8b55 100644 --- a/c_parser/models.py +++ b/c_parser/models.py @@ -106,7 +106,7 @@ def c_model_to_dict(obj: Any, _seen: set[int] | None = None) -> Any: class CParseError(ValueError): """C parser error with compiler-style diagnostic rendering support.""" - default_code = "CPARSE001" + default_code = "CPARSE_ERROR" def __init__( self, diff --git a/c_parser/parser.py b/c_parser/parser.py index c8541695a..7c0ded12c 100644 --- a/c_parser/parser.py +++ b/c_parser/parser.py @@ -1191,7 +1191,7 @@ def _invalid_specifier_error( line_number=location.line, column=location.column, source_line=location.source_line, - code="CPARSE003", + code="CPARSE_INVALID_SPECIFIER_SEQUENCE", ) def _atomic_type_specifier_parts(self, spec_text: str) -> tuple[str, str] | None: @@ -1815,7 +1815,7 @@ def _raise_for_unsupported_old_style_definitions( line_number=mapping.line if mapping is not None else index + 1, column=max(line.find(name_match.group(0)) + 1, 1), source_line=source_line, - code="CPARSE002", + code="CPARSE_UNSUPPORTED_KNR_DEFINITION", ) if stripped.endswith(";"): saw_old_style_declaration = True @@ -1833,7 +1833,7 @@ def _raise_for_unsupported_old_style_definitions( line_number=mapping.line if mapping is not None else index + 1, column=max(line.find(name_match.group(0)) + 1, 1), source_line=source_line, - code="CPARSE002", + code="CPARSE_UNSUPPORTED_KNR_DEFINITION", ) def _prototype_style(self, parameters_text: str) -> str: @@ -1869,7 +1869,7 @@ def _parse_function(self, segment: CTopLevelSegment) -> CFunction | None: line_number=segment.original_start_line, column=segment.original_start_column, source_line=segment.original_source_line, - code="CPARSE002", + code="CPARSE_UNSUPPORTED_KNR_DEFINITION", ) return self._function_from_type( name, diff --git a/docs/c_parser/c_parser_architecture.md b/docs/c_parser/c_parser_architecture.md index e8c5849b9..f72896544 100644 --- a/docs/c_parser/c_parser_architecture.md +++ b/docs/c_parser/c_parser_architecture.md @@ -71,7 +71,8 @@ Implemented now: become diagnostics. Grammar-invalid input raises `CParseError` with `CPARSE_INVALID_SYNTAX`; identifier spellings are not used to guess another language. Primitive specifier order is normalized, and invalid combinations - such as `unsigned float` raise `CParseError` with code `CPARSE003` while a + such as `unsigned float` raise `CParseError` with code + `CPARSE_INVALID_SPECIFIER_SEQUENCE` while a single 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. @@ -284,7 +285,8 @@ Current and planned responsibilities: helper methods. Function models record prototype-style versus unspecified empty parameter lists, function definitions preserve start/end locations, K&R-style definitions are rejected with `CParseError`, and invalid - primitive-specifier combinations are rejected with `CPARSE003`. Array and + primitive-specifier combinations are rejected with + `CPARSE_INVALID_SPECIFIER_SEQUENCE`. Array and function parameters preserve `declared_type` while effective `type` uses C parameter adjustment. Raw declarations beginning with an object-like macro name are retained as macro-dependent diagnostics. diff --git a/docs/c_parser/c_parser_cli_workflow.md b/docs/c_parser/c_parser_cli_workflow.md index 3a5a90071..ffff2a2c0 100644 --- a/docs/c_parser/c_parser_cli_workflow.md +++ b/docs/c_parser/c_parser_cli_workflow.md @@ -189,7 +189,7 @@ Current C behavior also accepts `--no-color`. `CParseError` supports compiler-style diagnostic formatting and the `C_PARSER_DEBUG` environment variable. The current grammar subset is tolerant for recoverable unsupported declaration forms, but invalid primitive-specifier combinations raise -`CPARSE003`; unresolved single typedef-name uses are deferred until type +`CPARSE_INVALID_SPECIFIER_SEQUENCE`; unresolved single typedef-name uses are deferred until type resolution can determine whether a declaration exists. `--debug-traceback` remains accepted as a compatibility alias. @@ -561,16 +561,14 @@ Invalid primitive-specifier combinations that are independent of symbol resolution are fatal: ```text -src/api.h:12:1: error[CPARSE003]: Invalid type specifier sequence 'unsigned float'. +src/api.h:12:1: error[CPARSE_INVALID_SPECIFIER_SEQUENCE]: Invalid type specifier sequence 'unsigned float'. 12 | unsigned float value; | ^ ``` Grammar-invalid C syntax is also fatal and uses -`CPARSE_INVALID_SYNTAX`. Diagnostic codes are stable category identifiers for -tests, tools, and documentation. A numeric suffix such as the one in -`CPARSE003` is not a source line number, an occurrence counter, or an exit -status. The shared registry is +`CPARSE_INVALID_SYNTAX`. Diagnostic codes are stable, explicit category +identifiers for tests, tools, and documentation. The shared registry is [`docs/diagnostic_codes.md`](../diagnostic_codes.md). Debug behavior: @@ -649,8 +647,8 @@ Completed order: 12. Replaced generic type references and declaration-kind tags with concrete `CType` subclasses, `CComposedType` components, and concrete declaration objects. -13. Added order-insensitive primitive specifier validation and `CPARSE003` - errors for invalid primitive combinations while retaining unresolved +13. Added order-insensitive primitive specifier validation and + `CPARSE_INVALID_SPECIFIER_SEQUENCE` errors for invalid primitive combinations while retaining unresolved typedef-name references for later resolution. 14. Added field-level source locations, flexible array member classification and invalid-use diagnostics, plus explicit bit-field regression coverage. diff --git a/docs/c_parser/c_parser_reference.md b/docs/c_parser/c_parser_reference.md index 45c54c5ad..3b154e6a4 100644 --- a/docs/c_parser/c_parser_reference.md +++ b/docs/c_parser/c_parser_reference.md @@ -71,7 +71,8 @@ Implemented: - raw `#undef` directive provenance in macro metadata - concrete primitive `CType` objects, pointer/array composition, and concrete qualifier objects -- order-insensitive primitive specifier matching with `CPARSE003` errors for +- order-insensitive primitive specifier matching with + `CPARSE_INVALID_SPECIFIER_SEQUENCE` errors for invalid combinations such as `unsigned float` - recursive declarator extraction for parenthesized pointer/array precedence - nameless `CFunctionType` signatures for function pointer typedefs and @@ -357,8 +358,8 @@ type component they qualify. `_Atomic int value;` is stored with a `CAtomic` qualifier; `_Atomic(int) value;` is represented the same way, while `_Atomic(int *) value;` qualifies the pointer component. Equivalent primitive orderings, such as `int unsigned` and `double long`, map to the same concrete type while -invalid combinations, such as `unsigned float`, raise `CParseError` with -code `CPARSE003`. A single unresolved typedef-name use remains a `CTypedef` +invalid combinations, such as `unsigned float`, raise `CParseError` with code +`CPARSE_INVALID_SPECIFIER_SEQUENCE`. A single unresolved typedef-name use remains a `CTypedef` until resolution can establish whether a matching declaration exists. Nested declarators are `CComposedType` objects whose `components` are read @@ -597,13 +598,13 @@ non-fatal metadata diagnostics, such as unresolved local includes or macros that affect declarations but were recorded rather than expanded. K&R-style function definitions now raise `CParseError` because the current function parser only models prototype-style declarations and definitions. Invalid primitive -specifier combinations also raise `CParseError` (`CPARSE003`) because their +specifier combinations also raise `CParseError` +(`CPARSE_INVALID_SPECIFIER_SEQUENCE`) because their invalidity does not depend on later typedef resolution. Known unsupported declaration extensions are diagnosed rather than partially modeled; additional syntax diagnostics should be added only with focused tests. Generic grammar rejection uses `CPARSE_INVALID_SYNTAX`. Diagnostic codes are -stable category identifiers for tests, tools, and documentation; numeric -suffixes are not line numbers, occurrence counters, or exit statuses. The +stable, explicit category identifiers for tests, tools, and documentation. The shared registry is [`docs/diagnostic_codes.md`](../diagnostic_codes.md). ## Testing Workflow diff --git a/docs/diagnostic_codes.md b/docs/diagnostic_codes.md index 8771886c0..90dee7ffb 100644 --- a/docs/diagnostic_codes.md +++ b/docs/diagnostic_codes.md @@ -3,10 +3,8 @@ Diagnostic codes are stable category identifiers for users, tests, and tooling. They are not source line numbers, occurrence counters, or process exit statuses. -New categories should use explicit symbolic names such as -`PARSE_INVALID_SYNTAX` or `C_UNRESOLVED_INCLUDE`. Existing numbered codes remain -supported for compatibility. Replacing a numbered code should be a deliberate -compatibility change with tests and generated fixtures updated together. +Categories use explicit symbolic names such as `PARSE_INVALID_SYNTAX` and +`C_UNRESOLVED_INCLUDE`. The name describes the failure class directly. ## Fatal Parser Errors @@ -15,21 +13,43 @@ traceback unless `--debug` is used. | Code | Frontend | Meaning | | --- | --- | --- | -| `PARSE001` | Fortran | Compatibility fallback for a Fortran parse error without a more specific code. | +| `PARSE_ERROR` | Fortran | Fallback for a manually constructed or defensive Fortran parse error without a narrower category. | | `PARSE_INVALID_SYNTAX` | Fortran | Syntax cannot be consumed in a modeled Fortran grammar region. | | `PARSE_WRONG_ENTRYPOINT` | Fortran | A singular public parser API was called for a different source-unit kind. | | `PARSE_AMBIGUOUS_ENTRYPOINT` | Fortran | A singular public parser API matched more than one source unit. | -| `CPARSE001` | C | Compatibility fallback for a C parse error without a more specific code. | -| `CPARSE002` | C | Unsupported K&R-style function definition. | -| `CPARSE003` | C | Invalid C primitive-specifier sequence. | +| `PARSE_EXPECTED_UNIT` | Fortran | An internal unit visitor received the wrong source-unit kind. | +| `PARSE_MISSING_UNIT_END` | Fortran | A source unit has no closing statement. | +| `PARSE_MISMATCHED_UNIT_END` | Fortran | A named source-unit closing statement does not match its opener. | +| `PARSE_UNEXPECTED_UNIT_END` | Fortran | A closing statement appears while another nested unit is active. | +| `PARSE_DUPLICATE_UNIT` | Fortran | A scope contains duplicate named source units of the same kind. | +| `PARSE_DUPLICATE_PROCEDURE` | Fortran | A scope contains duplicate procedure names. | +| `PARSE_MALFORMED_HEADER` | Fortran | A module or procedure header is unsupported or malformed. | +| `PARSE_UNSUPPORTED_RESULT_TYPE` | Fortran | A function header contains an unsupported result-type prefix. | +| `PARSE_DUPLICATE_DECLARATION` | Fortran | A procedure symbol is declared more than once. | +| `PARSE_UNKNOWN_PARAMETER_TYPE` | Fortran | A `PARAMETER` symbol has no declared type where one is required. | +| `PARSE_DUPLICATE_PARAMETER` | Fortran | A procedure contains duplicate `PARAMETER` declarations. | +| `PARSE_DUPLICATE_SYMBOL` | Fortran | A file or project scope contains a duplicate symbol. | +| `PARSE_UNSUPPORTED_OPENMP_DIRECTIVE` | Fortran | A modeled specification region contains an unsupported OpenMP directive. | +| `PARSE_MISSING_DERIVED_TYPE_END` | Fortran | A derived-type declaration has no matching closing statement. | +| `PARSE_EXECUTABLE_IN_SPECIFICATION` | Fortran | An executable statement appears in a non-executable specification region. | +| `PARSE_UNSUPPORTED_DECLARATION` | Fortran | A declaration-shaped line uses an unsupported datatype form. | +| `PARSE_UNSUPPORTED_TYPE_BOUND_DECLARATION` | Fortran | A derived-type `contains` region has an unsupported binding declaration. | +| `PARSE_UNRESOLVED_ARGUMENT_TYPE` | Fortran | A defensive invariant could not apply a declared argument type. | +| `PARSE_UNKNOWN_FUNCTION_RESULT_TYPE` | Fortran | A function result has no resolvable datatype. | +| `PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL` | Fortran | `implicit none` requires a missing argument or result declaration. | +| `PARSE_MISSING_FUNCTION_RESULT` | Fortran | A defensive invariant found a function without a result variable. | +| `PARSE_RESULT_SHADOWS_ARGUMENT` | Fortran | A function result name shadows an argument. | +| `PARSE_DUPLICATE_VARIABLE` | Fortran | A module-like scope contains conflicting duplicate variable declarations. | +| `PARSE_UNKNOWN_VARIABLE_TYPE` | Fortran | A module variable still has an unknown datatype after parsing. | +| `PARSE_DUPLICATE_FIELD` | Fortran | A derived type contains duplicate fields. | +| `PARSE_UNKNOWN_FIELD_TYPE` | Fortran | A derived-type field still has an unknown datatype after parsing. | +| `PARSE_DUPLICATE_ARGUMENT` | Fortran | A procedure argument list repeats a name. | +| `PARSE_INTERNAL_STATE` | Fortran | A defensive internal parser invariant was violated. | +| `CPARSE_ERROR` | C | Fallback for a manually constructed or defensive C parse error without a narrower category. | +| `CPARSE_UNSUPPORTED_KNR_DEFINITION` | C | Unsupported K&R-style function definition. | +| `CPARSE_INVALID_SPECIFIER_SEQUENCE` | C | Invalid C primitive-specifier sequence. | | `CPARSE_INVALID_SYNTAX` | C | Syntax cannot be consumed in a modeled C grammar region. | -`PARSE001`, `CPARSE001`, `CPARSE002`, and `CPARSE003` predate the explicit -category naming rule. Prefer symbolic names for new categories. If the numbered -codes are migrated later, useful replacements would be names such as -`PARSE_UNKNOWN_DATATYPE`, `CPARSE_UNSUPPORTED_KNR_DEFINITION`, and -`CPARSE_INVALID_SPECIFIER_SEQUENCE`. - ## C Report Diagnostics The C parser can preserve partial metadata and attach `CDiagnostic` records. @@ -53,4 +73,3 @@ These records do not necessarily stop parsing; inspect each diagnostic's | `C_DUPLICATE_VARIABLE_DEFINITION` | File-scope variable has more than one definition. | | `C_CONFLICTING_TYPEDEF` | Typedef declarations conflict. | | `C_DUPLICATE_TAG_DEFINITION` | Struct, union, or enum tag has more than one definition. | - diff --git a/docs/fortran/fortran_parser.md b/docs/fortran/fortran_parser.md index f7d0244ec..732bfc89e 100644 --- a/docs/fortran/fortran_parser.md +++ b/docs/fortran/fortran_parser.md @@ -464,7 +464,7 @@ python -m x2py tests/data/fortran/errors/err_duplicate_argument_name.f90 Example diagnostic shape: ```text -tests/data/fortran/errors/err_duplicate_argument_name.f90:1:1: error[PARSE001]: Duplicate argument name 'x' in procedure 'dup'. +tests/data/fortran/errors/err_duplicate_argument_name.f90:1:1: error[PARSE_DUPLICATE_ARGUMENT]: Duplicate argument name 'x' in procedure 'dup'. | 1 | subroutine dup(x, y, x) | ^ @@ -623,19 +623,19 @@ exception keeps structured metadata for consumers: - `line_number` — 1-based source line where the error was detected, if known - `source_line` — original source text for context, if known - `base_message` — stable error text without location/source context -- `code` — stable diagnostic category identifier; the default parse diagnostic - code is `PARSE001`, while grammar rejection uses `PARSE_INVALID_SYNTAX` +- `code` — stable, explicit diagnostic category identifier; manually + constructed fallback errors use `PARSE_ERROR`, while grammar rejection uses + `PARSE_INVALID_SYNTAX` Diagnostic codes are for programmatic matching in tests, tools, and -documentation. The numeric suffix in `PARSE001` identifies an error category; -it is not a source line number, an occurrence counter, or the CLI exit status. -The shared registry is [`docs/diagnostic_codes.md`](../diagnostic_codes.md). +documentation. The category name states the failure class directly. The shared +registry is [`docs/diagnostic_codes.md`](../diagnostic_codes.md). `str(error)` and `error.format_diagnostic(color=False)` render a compiler-style diagnostic: ```text -::1: error[PARSE001]: +::1: error[]: | | | ^ @@ -682,7 +682,7 @@ end subroutine bad Example error: ``` -bad.f90:2:1: error[PARSE001]: Unknown or unsupported datatype declaration for procedure 'bad': weirdtype :: x +bad.f90:2:1: error[PARSE_UNSUPPORTED_DECLARATION]: Unknown or unsupported datatype declaration for procedure 'bad': weirdtype :: x | 2 | weirdtype :: x | ^ @@ -722,7 +722,7 @@ end subroutine dup Example error: ``` -dup.f90:3:1: error[PARSE001]: Duplicate declaration of symbol 'x' in procedure 'dup'. +dup.f90:3:1: error[PARSE_DUPLICATE_DECLARATION]: Duplicate declaration of symbol 'x' in procedure 'dup'. | 3 | integer :: x | ^ @@ -780,7 +780,7 @@ end subroutine work Example error: ``` -dup.f90:5:1: error[PARSE001]: Duplicate procedure name 'work' in global scope. +dup.f90:5:1: error[PARSE_DUPLICATE_PROCEDURE]: Duplicate procedure name 'work' in global scope. | 5 | subroutine work(n) | ^ @@ -806,62 +806,28 @@ end subroutine dup Example error: ``` -dup_arg.f90:1:1: error[PARSE001]: Duplicate argument name 'x' in procedure 'dup'. +dup_arg.f90:1:1: error[PARSE_DUPLICATE_ARGUMENT]: Duplicate argument name 'x' in procedure 'dup'. | 1 | subroutine dup(x, y, x) | ^ ``` -### 6.5 Star-kind in modern source +### 6.5 Star-kind declarations -Triggered when a legacy `type*N` (e.g. `real*8`) declaration appears in a file -with a modern Fortran extension (`.f90`, `.f95`, `.f03`, `.f08`). - -``` -Unsupported Fortran 77 star-kind declaration '*' in modern source ''. -``` - -Example: +Legacy `type*N` declarations, such as `real*8`, are accepted in both fixed-form +and modern-extension files. The parser preserves the kind metadata: ```fortran -subroutine bad(x) +subroutine accepted(x) real*8 :: x -end subroutine bad +end subroutine accepted ``` -Example error (file `bad.f90`): - -``` -bad.f90:2:1: error[PARSE001]: Unsupported Fortran 77 star-kind declaration 'real*8' in modern source 'bad.f90'. - | -2 | real*8 :: x - | ^ -``` - -### 6.6 Fortran 77 syntax in a `.f77` source file - -Triggered when modern constructs (`module`, `contains`, `interface`, -`class(...)`) appear in a file with extension `.f77`. +### 6.6 Source-form metadata -``` -Unsupported syntax for Fortran 77 source '': -``` - -Example: - -```fortran - module bad_module - end module bad_module -``` - -Example error (file `legacy.f77`): - -``` -legacy.f77:1:1: error[PARSE001]: Unsupported syntax for Fortran 77 source 'legacy.f77': module bad_module - | -1 | module bad_module - | ^ -``` +The parser records source-form metadata from the filename and lexer, but does +not reject a construct solely because a `.f77` suffix was used. Grammar-region +validation still applies after preprocessing. ### 6.7 Implicit none — undeclared argument or result @@ -892,7 +858,7 @@ end subroutine foo Example error: ``` -implicit_none.f90:1:1: error[PARSE001]: Argument 'y' in procedure 'foo' has no type declaration (implicit none is active). +implicit_none.f90:1:1: error[PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL]: Argument 'y' in procedure 'foo' has no type declaration (implicit none is active). | 1 | subroutine foo(x, y) | ^ @@ -919,7 +885,7 @@ end function f Example error: ``` -bad.f90:1:1: error[PARSE001]: Unknown datatype for function result 'res' in procedure 'f'. +bad.f90:1:1: error[PARSE_UNKNOWN_FUNCTION_RESULT_TYPE]: Unknown datatype for function result 'res' in procedure 'f'. | 1 | function f(x) result(res) | ^ @@ -965,7 +931,7 @@ Example: Example error: ``` -legacy.f:4:1: error[PARSE001]: Unknown datatype for PARAMETER symbol 'zero' in procedure 'cst'. +legacy.f:4:1: error[PARSE_UNKNOWN_PARAMETER_TYPE]: Unknown datatype for PARAMETER symbol 'zero' in procedure 'cst'. | 4 | parameter ( zero = 0.0e+0 ) | ^ @@ -992,7 +958,7 @@ end function f Example error: ``` -shadow.f90:1:1: error[PARSE001]: Function result variable 'res' in function 'f' shadows an argument name. +shadow.f90:1:1: error[PARSE_RESULT_SHADOWS_ARGUMENT]: Function result variable 'res' in function 'f' shadows an argument name. | 1 | function f(res) result(res) | ^ diff --git a/docs/fortran/parser_implementation_reference.md b/docs/fortran/parser_implementation_reference.md index 4f4662fd1..347cd983c 100644 --- a/docs/fortran/parser_implementation_reference.md +++ b/docs/fortran/parser_implementation_reference.md @@ -314,7 +314,7 @@ stage separation, and `--semantics --wrap-readiness`. Dedicated tests for the error handling system: - `FortranParseError` attribute presence (`filename`, `line_number`, `source_line`, `base_message`, `code`) -- Compiler-style diagnostic formatting with `PARSE001`, source line context, and caret marker +- Compiler-style diagnostic formatting with explicit categories, source line context, and caret marker - ANSI color formatting and environment-variable debug activation - Error raised for all error categories in all scopes: procedures, modules, derived types, interfaces - Line number accuracy @@ -673,14 +673,15 @@ When updating parser behavior, keep this fail-fast contract aligned with tests: - `line_number` — 1-based line number in the original source where the error was detected - `source_line` — the original (pre-preprocessed) source line text - `base_message` — the stable error message without source/location context -- `code` — stable diagnostic category identifier; current parser errors default - to `PARSE001`, while grammar rejection uses `PARSE_INVALID_SYNTAX` +- `code` — stable, explicit diagnostic category identifier; manually + constructed fallback errors use `PARSE_ERROR`, while grammar rejection uses + `PARSE_INVALID_SYNTAX` - `parser_file`, `parser_line_number`, `parser_function` — internal raise-site metadata used only for debug diagnostics The formatted `str()` of `FortranParseError` is a compiler-style diagnostic: ```text -::1: error[PARSE001]: +::1: error[]: | | | ^ @@ -691,9 +692,8 @@ Use `error.format_diagnostic(color=True)` to add ANSI color and line with the internal parser location. `format_diagnostic(debug=None)` also honors `FORTRAN_PARSER_DEBUG=1`. -The numeric suffix in a code such as `PARSE001` identifies an error category -for tests, tools, and documentation. It is not a line number, an occurrence -counter, or an exit status. The shared registry is +The category name identifies the failure class for tests, tools, and +documentation. The shared registry is [`docs/diagnostic_codes.md`](../diagnostic_codes.md). CLI contract: diff --git a/fortran_parser/models.py b/fortran_parser/models.py index 4e2cd3420..7219f87c6 100644 --- a/fortran_parser/models.py +++ b/fortran_parser/models.py @@ -136,7 +136,7 @@ def _enable_windows_ansi() -> None: # pragma: no cover - Windows-only console s class FortranParseError(ValueError): """Parser error with compiler-style diagnostic rendering support.""" - default_code = "PARSE001" + default_code = "PARSE_ERROR" def __init__( self, diff --git a/fortran_parser/parser.py b/fortran_parser/parser.py index 90273c938..69ec219ac 100644 --- a/fortran_parser/parser.py +++ b/fortran_parser/parser.py @@ -580,7 +580,7 @@ def visit_module_unit( header = unit.lines[0] module = self._parse_module_header(header[0].strip(), filename, lineno=header[1], source_line=header[2]) if module is None: # pragma: no cover - slicer only dispatches module units with module headers. - raise FortranParseError("Expected module unit.", filename=filename, line_number=header[1], source_line=header[2]) + raise FortranParseError("Expected module unit.", filename=filename, line_number=header[1], source_line=header[2], code="PARSE_EXPECTED_UNIT") scope = self._helper_scope_for_model("module", module, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("module"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) @@ -627,7 +627,7 @@ def visit_submodule_unit( header = unit.lines[0] submodule = self._parse_submodule_header(header[0].strip(), filename) if submodule is None: # pragma: no cover - slicer only dispatches submodule units with submodule headers. - raise FortranParseError("Expected submodule unit.", filename=filename, line_number=header[1], source_line=header[2]) + raise FortranParseError("Expected submodule unit.", filename=filename, line_number=header[1], source_line=header[2], code="PARSE_EXPECTED_UNIT") scope = self._helper_scope_for_model("submodule", submodule, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("submodule"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) @@ -673,7 +673,7 @@ def visit_program_unit( header = unit.lines[0] program = self._parse_program_header(header[0].strip(), filename) if program is None: # pragma: no cover - slicer only dispatches program units with program headers. - raise FortranParseError("Expected program unit.", filename=filename, line_number=header[1], source_line=header[2]) + raise FortranParseError("Expected program unit.", filename=filename, line_number=header[1], source_line=header[2], code="PARSE_EXPECTED_UNIT") scope = self._helper_scope_for_model("program", program, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("program"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) @@ -706,7 +706,7 @@ def visit_block_data_source_unit( header = unit.lines[0] block_data = self._parse_block_data_header(header[0].strip(), filename) if block_data is None: # pragma: no cover - slicer only dispatches block-data units with block-data headers. - raise FortranParseError("Expected block data unit.", filename=filename, line_number=header[1], source_line=header[2]) + raise FortranParseError("Expected block data unit.", filename=filename, line_number=header[1], source_line=header[2], code="PARSE_EXPECTED_UNIT") scope = self._helper_scope_for_model("block_data", block_data, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("block_data"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) @@ -731,7 +731,7 @@ def visit_derived_type_unit( header = unit.lines[0] dtype = self._init_derived_type(header[0].strip(), current_module=parent_scope.module_owner) if dtype is None: # pragma: no cover - slicer only dispatches derived-type units with type headers. - raise FortranParseError("Expected derived-type unit.", filename=filename, line_number=header[1], source_line=header[2]) + raise FortranParseError("Expected derived-type unit.", filename=filename, line_number=header[1], source_line=header[2], code="PARSE_EXPECTED_UNIT") scope = self._helper_scope_for_model("derived_type", dtype, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("derived_type"), filename=filename) self._helper_visit_spec_part(scope, parts.specification, filename=filename) @@ -762,7 +762,7 @@ def visit_interface_unit( header = unit.lines[0] starts_interface, interface_name = self._parse_interface_header(header[0].strip()) if not starts_interface: # pragma: no cover - slicer only dispatches interface units with interface headers. - raise FortranParseError("Expected interface unit.", filename=filename, line_number=header[1], source_line=header[2]) + raise FortranParseError("Expected interface unit.", filename=filename, line_number=header[1], source_line=header[2], code="PARSE_EXPECTED_UNIT") interface = FortranInterface(name=interface_name, module=parent_scope.module_owner) scope = self._helper_scope_for_model("interface", interface, parent=parent_scope) parts = self._helper_split_unit_parts(unit, self._helper_unit_grammar("interface"), filename=filename) @@ -812,7 +812,7 @@ def visit_procedure_unit( lineno=header[1], source_line=header[2], ) - raise FortranParseError("Expected procedure unit.", filename=filename, line_number=header[1], source_line=header[2]) + raise FortranParseError("Expected procedure unit.", filename=filename, line_number=header[1], source_line=header[2], code="PARSE_EXPECTED_UNIT") proc_state["filename"] = filename proc_state["header_lineno"] = header[1] proc_state["header_source_line"] = header[2] @@ -1361,6 +1361,7 @@ def _helper_slice_child_units( filename=filename, line_number=lineno, source_line=lines[index][2], + code="PARSE_MISSING_UNIT_END", ) end_line = lines[end_index][1] @@ -1432,6 +1433,7 @@ def _helper_find_unit_end( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_MISMATCHED_UNIT_END", ) stack.pop() if not stack: @@ -1470,6 +1472,7 @@ def _helper_find_unit_end( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNEXPECTED_UNIT_END", ) idx += 1 return None @@ -1884,12 +1887,14 @@ def _helper_validate_sibling_units( filename=filename, line_number=unit.start_line, source_line=unit.lines[0][2] if unit.lines else None, + code="PARSE_DUPLICATE_PROCEDURE", ) raise FortranParseError( f"Duplicate {unit.kind.replace('_', ' ')} name '{unit.name}' in {parent_scope.kind} scope.", filename=filename, line_number=unit.start_line, source_line=unit.lines[0][2] if unit.lines else None, + code="PARSE_DUPLICATE_UNIT", ) seen.setdefault(key, []).append(unit) @@ -2089,6 +2094,7 @@ def _parse_module_header( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_MALFORMED_HEADER", ) return None return FortranModule(name=module_match.group("name"), filename=filename) @@ -2220,6 +2226,7 @@ def _parse_procedure_header( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_RESULT_TYPE", ) if parsed_prefix: result.base_type, result.kind = parsed_prefix @@ -2261,6 +2268,7 @@ def _raise_if_unparsed_procedure_header( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_MALFORMED_HEADER", ) if FortranParser._looks_like_procedure_header(stripped): raise FortranParseError( @@ -2268,6 +2276,7 @@ def _raise_if_unparsed_procedure_header( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_MALFORMED_HEADER", ) @staticmethod @@ -2380,6 +2389,7 @@ def _proc_scope_mark_declared_symbol( filename=filename, line_number=line_number, source_line=source_line, + code="PARSE_DUPLICATE_DECLARATION", ) proc_state["typed_symbols"].add(key) return key @@ -2425,6 +2435,7 @@ def _proc_scope_add_local_parameter( filename=filename, line_number=line_number, source_line=source_line, + code="PARSE_UNKNOWN_PARAMETER_TYPE", ) if key in proc_state["local_params"]: raise FortranParseError( @@ -2432,6 +2443,7 @@ def _proc_scope_add_local_parameter( filename=filename, line_number=line_number, source_line=source_line, + code="PARSE_DUPLICATE_PARAMETER", ) proc_state["local_params"][key] = value if register_implicit_if_missing and not self._proc_scope_symbol_is_declared(proc_state, key): @@ -2449,7 +2461,11 @@ def _insert_unique_scope_symbol( filename: str | None = None, ) -> None: if key in scope: - raise FortranParseError(f"Duplicate symbol '{key}' in {label}.", filename=filename) + raise FortranParseError( + f"Duplicate symbol '{key}' in {label}.", + filename=filename, + code="PARSE_DUPLICATE_SYMBOL", + ) scope[key] = value # ------------------------------------------------------------------ @@ -2531,7 +2547,11 @@ def _helper_visit_module_like_spec_line( """ target = scope.model if target is None: # pragma: no cover - internal helper misuse. - raise FortranParseError("Module-like specification scope is missing a target model.", filename=filename) + raise FortranParseError( + "Module-like specification scope is missing a target model.", + filename=filename, + code="PARSE_INTERNAL_STATE", + ) stripped = line.strip() lower = stripped.lower() @@ -2542,6 +2562,7 @@ def _helper_visit_module_like_spec_line( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_OPENMP_DIRECTIVE", ) if scope.kind == "module": @@ -2565,6 +2586,7 @@ def _helper_visit_module_like_spec_line( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_MISSING_DERIVED_TYPE_END", ) if "::" in stripped: @@ -2594,6 +2616,7 @@ def _helper_visit_module_like_spec_line( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_EXECUTABLE_IN_SPECIFICATION", ) return @@ -2622,6 +2645,7 @@ def _helper_visit_module_like_spec_line( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_DECLARATION", ) def _helper_visit_procedure_spec_line(self, line: str, proc_state: dict, filename: str | None = None, lineno: int | None = None, source_line: str | None = None) -> None: @@ -2646,6 +2670,7 @@ def _helper_visit_procedure_spec_line(self, line: str, proc_state: dict, filenam filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_OPENMP_DIRECTIVE", ) if self._handle_proc_implicit_line(stripped, proc_state): return @@ -2713,7 +2738,11 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename: """ dtype = scope.model if dtype is None: # pragma: no cover - internal helper misuse. - raise FortranParseError("Derived-type specification scope is missing a target model.", filename=filename) + raise FortranParseError( + "Derived-type specification scope is missing a target model.", + filename=filename, + code="PARSE_INTERNAL_STATE", + ) stripped = line.strip() if re.match(r"^type\s*::\s*\w+$", stripped, re.IGNORECASE): raise FortranParseError( @@ -2721,6 +2750,7 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename: filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_MISSING_DERIVED_TYPE_END", ) if stripped.lower() in {"sequence", "private"}: return @@ -2730,6 +2760,7 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename: filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_OPENMP_DIRECTIVE", ) parsed = self._helper_parse_declaration_line( stripped, @@ -2755,6 +2786,7 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename: filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_DECLARATION", ) def _parse_derived_type_contains_line( @@ -2793,6 +2825,7 @@ def _parse_derived_type_contains_line( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_TYPE_BOUND_DECLARATION", ) def _helper_apply_local_interface_declarations( @@ -2991,7 +3024,11 @@ def _helper_push_declaration_to_scope( if role == "procedure_symbol": proc_state = scope.state if proc_state is None: # pragma: no cover - internal helper misuse. - raise FortranParseError("Procedure declaration scope is missing state.", filename=filename) + raise FortranParseError( + "Procedure declaration scope is missing state.", + filename=filename, + code="PARSE_INTERNAL_STATE", + ) if meta["base_type"] == "procedure" and meta["kind"] in proc_state.get("imports", set()): meta["kind"] = None for entity in split_csv(right): @@ -3019,7 +3056,11 @@ def _helper_push_declaration_to_scope( target = scope.model if target is None: # pragma: no cover - internal helper misuse. - raise FortranParseError("Declaration scope is missing a target model.", filename=filename) + raise FortranParseError( + "Declaration scope is missing a target model.", + filename=filename, + code="PARSE_INTERNAL_STATE", + ) for entity in split_csv(right): initializer = entity.split("=", 1)[1].strip() if "=" in entity else None @@ -3347,6 +3388,7 @@ def _handle_unknown_proc_declaration( filename=filename, line_number=lineno, source_line=source_line, + code="PARSE_UNSUPPORTED_DECLARATION", ) # ------------------------------------------------------------------ @@ -3385,6 +3427,7 @@ def _finalize_proc(self, state: dict) -> FortranProcedureSignature: raise FortranParseError( f"Failed to resolve declared argument '{arg.name}' in procedure '{sig.name}'.", filename=filename, + code="PARSE_UNRESOLVED_ARGUMENT_TYPE", ) local_resolver = _CompileTimeResolver(local_params) for arg in sig.arguments: @@ -3438,6 +3481,7 @@ def _finalize_proc(self, state: dict) -> FortranProcedureSignature: raise FortranParseError( f"Unknown datatype for function result '{sig.result.name}' in procedure '{sig.name}'.", filename=filename, + code="PARSE_UNKNOWN_FUNCTION_RESULT_TYPE", ) if sig.kind == "function": self._validate_function_result(sig, filename) @@ -3455,16 +3499,19 @@ def _validate_all_args_declared(sig: FortranProcedureSignature, filename: str | raise FortranParseError( f"Argument '{arg.name}' in procedure '{sig.name}' has no type declaration (implicit none is active).", filename=filename, + code="PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL", ) if sig.kind == "function" and sig.result and sig.result.base_type == "unknown": if explicit_result: raise FortranParseError( f"Unknown datatype for function result '{sig.result.name}' in procedure '{sig.name}'.", filename=filename, + code="PARSE_UNKNOWN_FUNCTION_RESULT_TYPE", ) raise FortranParseError( f"Function result '{sig.result.name}' in procedure '{sig.name}' has no type declaration (implicit none is active).", filename=filename, + code="PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL", ) @staticmethod @@ -3473,6 +3520,7 @@ def _validate_function_result(sig: FortranProcedureSignature, filename: str | No raise FortranParseError( f"Function '{sig.name}' has no result variable.", filename=filename, + code="PARSE_MISSING_FUNCTION_RESULT", ) result_name = sig.result.name.lower() func_name = sig.name.lower() @@ -3481,6 +3529,7 @@ def _validate_function_result(sig: FortranProcedureSignature, filename: str | No raise FortranParseError( f"Function result variable '{sig.result.name}' in function '{sig.name}' shadows an argument name.", filename=filename, + code="PARSE_RESULT_SHADOWS_ARGUMENT", ) @staticmethod @@ -3508,6 +3557,7 @@ def _validate_variable_declarations( raise FortranParseError( f"Duplicate variable '{var.name}' in {owner_kind} '{display_name}'.", filename=filename, + code="PARSE_DUPLICATE_VARIABLE", ) continue # pragma: no cover - exact duplicate declarations are invalid Fortran and tolerated defensively. seen[key] = var @@ -3548,6 +3598,7 @@ def _apply_module_visibility(module: FortranModule, filename: str | None) -> Non raise FortranParseError( f"Unknown type for variable '{var.name}' in module '{module.name}'.", filename=filename, + code="PARSE_UNKNOWN_VARIABLE_TYPE", ) @staticmethod @@ -3558,12 +3609,14 @@ def _validate_derived_type_fields(dtype: FortranDerivedType, filename: str | Non raise FortranParseError( f"Duplicate field '{f.name}' in derived type '{dtype.name}'.", filename=filename, + code="PARSE_DUPLICATE_FIELD", ) seen.add(f.name.lower()) if f.base_type == "unknown": # pragma: no cover - unknown type fields raise before finalization. raise FortranParseError( f"Unknown type for field '{f.name}' in derived type '{dtype.name}'.", filename=filename, + code="PARSE_UNKNOWN_FIELD_TYPE", ) @staticmethod @@ -3583,6 +3636,7 @@ def _validate_no_duplicate_arg_names( filename=filename, line_number=line_number, source_line=source_line, + code="PARSE_DUPLICATE_ARGUMENT", ) seen.add(key) diff --git a/tests/parser/c/fixtures/errors/invalid_type_specifiers.h.json b/tests/parser/c/fixtures/errors/invalid_type_specifiers.h.json index a28fe9b60..b3cf99b38 100644 --- a/tests/parser/c/fixtures/errors/invalid_type_specifiers.h.json +++ b/tests/parser/c/fixtures/errors/invalid_type_specifiers.h.json @@ -5,7 +5,7 @@ "Invalid type specifier sequence 'unsigned float'." ], "diagnostic_contains": [ - "error[CPARSE003]", + "error[CPARSE_INVALID_SPECIFIER_SEQUENCE]", "Invalid type specifier sequence 'unsigned float'.", "unsigned float value;" ] diff --git a/tests/parser/c/test_c_cli_skeleton.py b/tests/parser/c/test_c_cli_skeleton.py index 13890273b..a98005f1d 100644 --- a/tests/parser/c/test_c_cli_skeleton.py +++ b/tests/parser/c/test_c_cli_skeleton.py @@ -296,7 +296,7 @@ def test_cli_c_invalid_primitive_specifier_sequence_is_fatal(tmp_path: Path): res = subprocess.run(cmd, capture_output=True, text=True) assert res.returncode == 1 - assert "error[CPARSE003]: Invalid type specifier sequence 'unsigned float'." in res.stderr + assert "error[CPARSE_INVALID_SPECIFIER_SEQUENCE]: Invalid type specifier sequence 'unsigned float'." in res.stderr assert "\x1b[" not in res.stderr diff --git a/tests/parser/c/test_c_declarations_and_declarators.py b/tests/parser/c/test_c_declarations_and_declarators.py index f9b291233..711da5878 100644 --- a/tests/parser/c/test_c_declarations_and_declarators.py +++ b/tests/parser/c/test_c_declarations_and_declarators.py @@ -107,9 +107,9 @@ def test_invalid_primitive_specifier_sequences_raise_parse_errors(source, expect with pytest.raises(CParseError, match="Invalid type specifier sequence") as error: parse_c_file(source, filename="invalid_specifiers.h") - assert error.value.code == "CPARSE003" + assert error.value.code == "CPARSE_INVALID_SPECIFIER_SEQUENCE" assert ( - f"invalid_specifiers.h:1:{expected_column}: error[CPARSE003]" + f"invalid_specifiers.h:1:{expected_column}: error[CPARSE_INVALID_SPECIFIER_SEQUENCE]" in error.value.format_diagnostic(color=False) ) diff --git a/tests/parser/c/test_c_public_api_skeleton.py b/tests/parser/c/test_c_public_api_skeleton.py index b146871dd..6e6fc42b6 100644 --- a/tests/parser/c/test_c_public_api_skeleton.py +++ b/tests/parser/c/test_c_public_api_skeleton.py @@ -255,10 +255,10 @@ def test_c_parse_error_attributes_and_diagnostic_formatting(): assert err.line_number == 2 assert err.column == 5 assert err.base_message == "unexpected token" - assert err.code == "CPARSE001" + assert err.code == "CPARSE_ERROR" diagnostic = err.format_diagnostic(color=False, debug=True) - assert "bad.h:2:5: error[CPARSE001]: unexpected token" in diagnostic + assert "bad.h:2:5: error[CPARSE_ERROR]: unexpected token" in diagnostic assert "2 | int broken(;" in diagnostic assert "note: parser raised at" in diagnostic diff --git a/tests/parser/fortran/fixtures/errors/err_duplicate_argument_name.json b/tests/parser/fortran/fixtures/errors/err_duplicate_argument_name.json index f5a8a7ece..ec6dbdc14 100644 --- a/tests/parser/fortran/fixtures/errors/err_duplicate_argument_name.json +++ b/tests/parser/fortran/fixtures/errors/err_duplicate_argument_name.json @@ -5,7 +5,7 @@ "Duplicate argument name 'x' in procedure 'dup'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_DUPLICATE_ARGUMENT]", "Duplicate argument name 'x' in procedure 'dup'.", "subroutine dup(x, y, x)" ] diff --git a/tests/parser/fortran/fixtures/errors/err_duplicate_declaration_procedure.json b/tests/parser/fortran/fixtures/errors/err_duplicate_declaration_procedure.json index 2cec23e8d..73cb9eee5 100644 --- a/tests/parser/fortran/fixtures/errors/err_duplicate_declaration_procedure.json +++ b/tests/parser/fortran/fixtures/errors/err_duplicate_declaration_procedure.json @@ -5,7 +5,7 @@ "Duplicate declaration of symbol 'x' in procedure 'dup'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_DUPLICATE_DECLARATION]", "Duplicate declaration of symbol 'x' in procedure 'dup'.", "integer :: x" ] diff --git a/tests/parser/fortran/fixtures/errors/err_duplicate_field_derived_type.json b/tests/parser/fortran/fixtures/errors/err_duplicate_field_derived_type.json index a5256a829..d005c62a3 100644 --- a/tests/parser/fortran/fixtures/errors/err_duplicate_field_derived_type.json +++ b/tests/parser/fortran/fixtures/errors/err_duplicate_field_derived_type.json @@ -5,7 +5,7 @@ "Duplicate field 'x' in derived type 'point'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_DUPLICATE_FIELD]", "Duplicate field 'x' in derived type 'point'.", "" ] diff --git a/tests/parser/fortran/fixtures/errors/err_duplicate_parameter.json b/tests/parser/fortran/fixtures/errors/err_duplicate_parameter.json index 173ff80f6..3a49cbc84 100644 --- a/tests/parser/fortran/fixtures/errors/err_duplicate_parameter.json +++ b/tests/parser/fortran/fixtures/errors/err_duplicate_parameter.json @@ -5,7 +5,7 @@ "Duplicate PARAMETER declaration of symbol 'n' in procedure 'dup_param'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_DUPLICATE_PARAMETER]", "Duplicate PARAMETER declaration of symbol 'n' in procedure 'dup_param'.", "integer, parameter :: n = 10" ] diff --git a/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_global.json b/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_global.json index 69e06d29b..9f036d8b8 100644 --- a/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_global.json +++ b/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_global.json @@ -5,7 +5,7 @@ "Duplicate procedure name 'work' in global scope." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_DUPLICATE_PROCEDURE]", "Duplicate procedure name 'work' in global scope.", "subroutine work(n)" ] diff --git a/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_module.json b/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_module.json index e925f29b4..c2cc637c7 100644 --- a/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_module.json +++ b/tests/parser/fortran/fixtures/errors/err_duplicate_procedure_module.json @@ -5,7 +5,7 @@ "Duplicate procedure name 'work' in module 'm'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_DUPLICATE_PROCEDURE]", "Duplicate procedure name 'work' in module 'm'.", "subroutine work(n)" ] diff --git a/tests/parser/fortran/fixtures/errors/err_duplicate_variable_module.json b/tests/parser/fortran/fixtures/errors/err_duplicate_variable_module.json index e401d75fb..4096848cc 100644 --- a/tests/parser/fortran/fixtures/errors/err_duplicate_variable_module.json +++ b/tests/parser/fortran/fixtures/errors/err_duplicate_variable_module.json @@ -5,7 +5,7 @@ "Duplicate variable 'n' in module 'm'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_DUPLICATE_VARIABLE]", "Duplicate variable 'n' in module 'm'.", "" ] diff --git a/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_arg.json b/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_arg.json index 8441129c6..e6fac08b3 100644 --- a/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_arg.json +++ b/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_arg.json @@ -5,7 +5,7 @@ "Argument 'y' in procedure 'foo' has no type declaration (implicit none is active)." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL]", "Argument 'y' in procedure 'foo' has no type declaration (implicit none is active).", "" ] diff --git a/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_result.json b/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_result.json index 4b8f31d49..4a55be7c6 100644 --- a/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_result.json +++ b/tests/parser/fortran/fixtures/errors/err_implicit_none_undeclared_result.json @@ -5,7 +5,7 @@ "Function result 'f' in procedure 'f' has no type declaration (implicit none is active)." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL]", "Function result 'f' in procedure 'f' has no type declaration (implicit none is active).", "" ] diff --git a/tests/parser/fortran/fixtures/errors/err_parameter_without_type_implicit_none.json b/tests/parser/fortran/fixtures/errors/err_parameter_without_type_implicit_none.json index 5c31dc38b..341e701cc 100644 --- a/tests/parser/fortran/fixtures/errors/err_parameter_without_type_implicit_none.json +++ b/tests/parser/fortran/fixtures/errors/err_parameter_without_type_implicit_none.json @@ -5,7 +5,7 @@ "Unknown datatype for PARAMETER symbol 'zero' in procedure 'cst'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_UNKNOWN_PARAMETER_TYPE]", "Unknown datatype for PARAMETER symbol 'zero' in procedure 'cst'.", "parameter ( zero = 0.0e+0 )" ] diff --git a/tests/parser/fortran/fixtures/errors/err_result_shadows_argument.json b/tests/parser/fortran/fixtures/errors/err_result_shadows_argument.json index 80e96b063..c3fabbb38 100644 --- a/tests/parser/fortran/fixtures/errors/err_result_shadows_argument.json +++ b/tests/parser/fortran/fixtures/errors/err_result_shadows_argument.json @@ -5,7 +5,7 @@ "Function result variable 'res' in function 'f' shadows an argument name." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_RESULT_SHADOWS_ARGUMENT]", "Function result variable 'res' in function 'f' shadows an argument name.", "" ] diff --git a/tests/parser/fortran/fixtures/errors/err_unknown_function_result.json b/tests/parser/fortran/fixtures/errors/err_unknown_function_result.json index a2081b6ee..1d087b74c 100644 --- a/tests/parser/fortran/fixtures/errors/err_unknown_function_result.json +++ b/tests/parser/fortran/fixtures/errors/err_unknown_function_result.json @@ -5,7 +5,7 @@ "Unknown datatype for function result 'res' in procedure 'f'." ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_UNKNOWN_FUNCTION_RESULT_TYPE]", "Unknown datatype for function result 'res' in procedure 'f'.", "" ] diff --git a/tests/parser/fortran/fixtures/errors/err_unknown_type_derived_type.json b/tests/parser/fortran/fixtures/errors/err_unknown_type_derived_type.json index d6b483be3..b35d27028 100644 --- a/tests/parser/fortran/fixtures/errors/err_unknown_type_derived_type.json +++ b/tests/parser/fortran/fixtures/errors/err_unknown_type_derived_type.json @@ -5,7 +5,7 @@ "Unknown or unsupported datatype declaration in type 't': weirdtype :: x" ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_UNSUPPORTED_DECLARATION]", "Unknown or unsupported datatype declaration in type 't': weirdtype :: x", "weirdtype :: x" ] diff --git a/tests/parser/fortran/fixtures/errors/err_unknown_type_module.json b/tests/parser/fortran/fixtures/errors/err_unknown_type_module.json index 1a038c85f..ec47a6201 100644 --- a/tests/parser/fortran/fixtures/errors/err_unknown_type_module.json +++ b/tests/parser/fortran/fixtures/errors/err_unknown_type_module.json @@ -5,7 +5,7 @@ "Unknown or unsupported datatype declaration in module 'm': weirdtype :: x" ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_UNSUPPORTED_DECLARATION]", "Unknown or unsupported datatype declaration in module 'm': weirdtype :: x", "weirdtype :: x" ] diff --git a/tests/parser/fortran/fixtures/errors/err_unknown_type_procedure.json b/tests/parser/fortran/fixtures/errors/err_unknown_type_procedure.json index ca2b86795..6dccf0f9e 100644 --- a/tests/parser/fortran/fixtures/errors/err_unknown_type_procedure.json +++ b/tests/parser/fortran/fixtures/errors/err_unknown_type_procedure.json @@ -5,7 +5,7 @@ "Unknown or unsupported datatype declaration for procedure 'bad': weirdtype :: x" ], "diagnostic_contains": [ - "error[PARSE001]", + "error[PARSE_UNSUPPORTED_DECLARATION]", "Unknown or unsupported datatype declaration for procedure 'bad': weirdtype :: x", "weirdtype :: x" ] diff --git a/tests/parser/test_cli.py b/tests/parser/test_cli.py index f292eb796..363637896 100644 --- a/tests/parser/test_cli.py +++ b/tests/parser/test_cli.py @@ -189,7 +189,7 @@ def test_cli_formats_parse_errors_without_traceback(tmp_path: Path): assert res.returncode == 1 assert res.stdout == "" assert "Traceback" not in res.stderr - assert f"{f90}:2:1: error[PARSE001]:" in res.stderr + assert f"{f90}:2:1: error[PARSE_UNSUPPORTED_DECLARATION]:" in res.stderr assert "2 | weirdtype :: x" in res.stderr @@ -273,7 +273,7 @@ def test_cli_no_color_env_disables_default_ansi(tmp_path: Path): assert res.returncode == 1 assert "\033[" not in res.stderr - assert f"{f90}:2:1: error[PARSE001]:" in res.stderr + assert f"{f90}:2:1: error[PARSE_UNSUPPORTED_DECLARATION]:" in res.stderr @@ -563,7 +563,7 @@ def test_cli_fortran_rejects_embedded_c_declaration_outside_execution_body(tmp_p ) assert result.returncode == 1 - assert "PARSE001" in result.stderr + assert "PARSE_UNSUPPORTED_DECLARATION" in result.stderr assert "Unknown or unsupported datatype declaration" in result.stderr diff --git a/tests/parser/test_error_handling.py b/tests/parser/test_error_handling.py index 4c1ca5cba..f36e50c99 100644 --- a/tests/parser/test_error_handling.py +++ b/tests/parser/test_error_handling.py @@ -34,7 +34,7 @@ def test_parse_error_message_includes_filename_and_lineno(): parse_fortran_file(code, filename="myfile.f90") msg = str(exc_info.value) assert "myfile.f90:3:1" in msg - assert "error[PARSE001]" in msg + assert "error[PARSE_UNSUPPORTED_DECLARATION]" in msg def test_parse_error_message_includes_source_line(): @@ -73,7 +73,7 @@ def test_parse_error_formats_compiler_style_diagnostic(): parse_fortran_file(code, filename="myfile.f90") diagnostic = exc_info.value.format_diagnostic(color=False) - assert "myfile.f90:3:1: error[PARSE001]:" in diagnostic + assert "myfile.f90:3:1: error[PARSE_UNSUPPORTED_DECLARATION]:" in diagnostic assert "Unknown or unsupported datatype" in diagnostic assert "3 | weirdtype :: x" in diagnostic assert "| ^" in diagnostic @@ -779,7 +779,7 @@ def test_fortran_parser_rejects_invalid_non_fortran_syntax_outside_execution_bod ) as exc_info: parse_fortran_file(code, filename="mixed.f90") - assert exc_info.value.code in {"PARSE_INVALID_SYNTAX", "PARSE001"} + assert exc_info.value.code in {"PARSE_INVALID_SYNTAX", "PARSE_UNSUPPORTED_DECLARATION"} def test_fortran_parser_ignores_non_fortran_syntax_after_execution_boundary():