@@ -900,6 +900,7 @@ def _helper_prepare_source_units(
900900 self ._helper_validate_unit_headers (lines , filename )
901901 root_scope = _ParserScope (kind = "file" , name = None )
902902 units = self ._helper_slice_child_units (lines , parent_scope = root_scope , filename = filename )
903+ self ._helper_validate_file_scope_unparsed_lines (lines , filename )
903904 self ._helper_validate_sibling_units (units , parent_scope = root_scope , filename = filename )
904905 return lines , root_scope , units
905906
@@ -1136,6 +1137,86 @@ def _helper_validate_unit_headers(self, lines: _PreprocessedLines, filename: str
11361137 source_line = source_line ,
11371138 )
11381139
1140+ def _helper_validate_file_scope_unparsed_lines (self , lines : _PreprocessedLines , filename : str | None ) -> None :
1141+ """Reject any non-Fortran syntax outside recognized unit bodies.
1142+
1143+ This guard is intentionally language-agnostic: lines that are neither
1144+ valid file-scope Fortran constructs nor part of a recognized unit are
1145+ rejected via the generic invalid-syntax diagnostic path.
1146+ """
1147+ index = 0
1148+ pp_condition_stack : list [tuple [int , int ]] = []
1149+ pp_active_stack : list [bool ] = []
1150+ pp_group_counter = 0
1151+ while index < len (lines ):
1152+ line , lineno , source_line = lines [index ]
1153+ stripped = line .strip ()
1154+ if not stripped :
1155+ index += 1
1156+ continue
1157+ handled_pp , pp_group_counter = self ._handle_procedure_preprocessor_line (
1158+ stripped ,
1159+ macro_selection_enabled = False ,
1160+ macro_names = set (),
1161+ pp_condition_stack = pp_condition_stack ,
1162+ pp_active_stack = pp_active_stack ,
1163+ pp_group_counter = pp_group_counter ,
1164+ )
1165+ if handled_pp :
1166+ index += 1
1167+ continue
1168+ start = self ._helper_classify_unit_start (stripped )
1169+ if start is not None :
1170+ end_index = self ._helper_find_unit_end (lines , index , start [0 ], filename = filename )
1171+ if end_index is not None :
1172+ index = end_index + 1
1173+ continue
1174+ if self ._is_allowed_unparsed_file_scope_line (stripped ):
1175+ index += 1
1176+ continue
1177+ if self ._is_executable_statement_start (stripped ):
1178+ index += 1
1179+ continue
1180+ self ._raise_invalid_fortran_syntax_line (
1181+ stripped ,
1182+ context = "file scope" ,
1183+ filename = filename ,
1184+ lineno = lineno ,
1185+ source_line = source_line ,
1186+ )
1187+
1188+ @staticmethod
1189+ def _is_allowed_unparsed_file_scope_line (line : str ) -> bool :
1190+ stripped = line .strip ()
1191+ lowered = stripped .lower ()
1192+ return (
1193+ stripped .startswith ("#" )
1194+ or lowered == "contains"
1195+ or lowered .startswith ("end " )
1196+ or lowered .startswith (("endif" , "enddo" ))
1197+ or lowered == "else"
1198+ or lowered .startswith (("elseif" , "else if" ))
1199+ or FortranParser ._is_ignored_spec_statement (stripped )
1200+ or FortranParser ._is_openmp_directive (stripped )
1201+ )
1202+
1203+ @staticmethod
1204+ def _raise_invalid_fortran_syntax_line (
1205+ line : str ,
1206+ * ,
1207+ context : str ,
1208+ filename : str | None ,
1209+ lineno : int | None ,
1210+ source_line : str | None ,
1211+ ) -> None :
1212+ raise FortranParseError (
1213+ f"Invalid Fortran syntax in { context } : { line .strip ()} " ,
1214+ filename = filename ,
1215+ line_number = lineno ,
1216+ source_line = source_line ,
1217+ code = "PARSE_INVALID_SYNTAX" ,
1218+ )
1219+
11391220 def _helper_slice_child_units (
11401221 self ,
11411222 lines : _PreprocessedLines ,
@@ -1378,16 +1459,10 @@ def _helper_split_unit_parts(
13781459 index = child_end + 1
13791460 continue
13801461
1381- is_spec_statement = (
1382- self ._parse_use_statement (stripped ) is not None
1383- or self ._is_ignored_spec_statement (stripped )
1384- or self ._looks_like_declaration_or_spec (stripped )
1385- )
13861462 if (
13871463 region == "specification"
13881464 and grammar .has_execution_part
13891465 and self ._is_executable_statement_start (stripped )
1390- and not is_spec_statement
13911466 ):
13921467 region = "execution"
13931468
@@ -2047,6 +2122,8 @@ def _helper_visit_spec_part(
20472122 stripped = line .strip ()
20482123 if not stripped :
20492124 continue
2125+ if stripped .startswith ("#" ):
2126+ continue
20502127 if scope .kind == "procedure" :
20512128 self ._helper_visit_procedure_spec_line (
20522129 stripped ,
@@ -2167,9 +2244,15 @@ def _helper_visit_module_like_spec_line(
21672244 )
21682245 if parsed :
21692246 return
2170- if "::" not in stripped and not self ._looks_like_declaration_or_spec (stripped ):
2171- return
21722247 owner_kind , owner_name = self ._variable_scope_label (target )
2248+ if "::" not in stripped and not self ._looks_like_declaration_or_spec (stripped ):
2249+ self ._raise_invalid_fortran_syntax_line (
2250+ stripped ,
2251+ context = f"{ owner_kind } '{ owner_name or '<unnamed>' } ' specification part" ,
2252+ filename = filename ,
2253+ lineno = lineno ,
2254+ source_line = source_line ,
2255+ )
21732256 raise FortranParseError (
21742257 f"Unknown or unsupported datatype declaration in { owner_kind } '{ owner_name or '<unnamed>' } ': { stripped } " ,
21752258 filename = filename ,
@@ -2223,6 +2306,8 @@ def _helper_visit_procedure_spec_line(self, line: str, proc_state: dict, filenam
22232306 source_line = source_line ,
22242307 ):
22252308 return
2309+ if self ._is_statement_function_statement (stripped ):
2310+ return
22262311
22272312 parsed = self ._helper_parse_declaration_line (
22282313 stripped ,
@@ -2289,7 +2374,15 @@ def _helper_visit_type_spec_line(self, line: str, scope: _ParserScope, filename:
22892374 if parsed :
22902375 return
22912376 if "::" not in stripped and not self ._looks_like_declaration_or_spec (stripped ):
2292- return
2377+ if self ._is_executable_statement_start (stripped ):
2378+ return
2379+ self ._raise_invalid_fortran_syntax_line (
2380+ stripped ,
2381+ context = f"type '{ dtype .name } ' specification part" ,
2382+ filename = filename ,
2383+ lineno = lineno ,
2384+ source_line = source_line ,
2385+ )
22932386 raise FortranParseError (
22942387 f"Unknown or unsupported datatype declaration in type '{ dtype .name } ': { line .strip ()} " ,
22952388 filename = filename ,
@@ -2890,7 +2983,13 @@ def _handle_unknown_proc_declaration(
28902983 as an unsupported datatype declaration for the active procedure.
28912984 """
28922985 if not self ._looks_like_unknown_proc_declaration (line ):
2893- return
2986+ self ._raise_invalid_fortran_syntax_line (
2987+ line ,
2988+ context = f"procedure '{ proc_state ['signature' ].name } ' specification part" ,
2989+ filename = filename ,
2990+ lineno = lineno ,
2991+ source_line = source_line ,
2992+ )
28942993 if any (_REGEX [pattern_key ].search (line ) for pattern_key in _UNSUPPORTED_PATTERN_KEYS ):
28952994 return
28962995 raise FortranParseError (
@@ -3778,6 +3877,17 @@ def _looks_like_declaration_or_spec(line: str) -> bool:
37783877 return True
37793878 return bool (re .match (r"^[A-Za-z_]\w+\s+[A-Za-z_]\w*" , stripped ))
37803879
3880+ @staticmethod
3881+ def _is_statement_function_statement (line : str ) -> bool :
3882+ stripped = line .strip ()
3883+ return bool (
3884+ re .match (
3885+ r"^[A-Za-z_]\w*\s*\([^()]*\)\s*=" ,
3886+ stripped ,
3887+ flags = re .IGNORECASE ,
3888+ )
3889+ )
3890+
37813891 @staticmethod
37823892 def _is_ignored_spec_statement (line : str ) -> bool :
37833893 return bool (
@@ -3819,9 +3929,19 @@ def _is_executable_statement_start(line: str) -> bool:
38193929 stripped = line .strip ()
38203930 if not stripped : # pragma: no cover - callers skip blank lines before executable checks.
38213931 return False
3932+ labeled = re .match (r"^\d+\s+(?P<body>.*)$" , stripped )
3933+ if labeled :
3934+ stripped = labeled .group ("body" ).strip ()
3935+ if not stripped :
3936+ return False
38223937 lowered = stripped .lower ()
38233938 if FortranParser ._is_openmp_directive (stripped ):
38243939 return not FortranParser ._is_openmp_declarative_directive (stripped )
3940+ if (
3941+ FortranParser ._parse_use_statement (stripped ) is not None
3942+ or FortranParser ._is_ignored_spec_statement (stripped )
3943+ ):
3944+ return False
38253945 first_match = re .match (r"([a-z_][a-z0-9_]*)" , lowered )
38263946 first = first_match .group (1 ) if first_match else lowered .split (None , 1 )[0 ]
38273947 if first .isdigit ():
@@ -3836,6 +3956,8 @@ def _is_executable_statement_start(line: str) -> bool:
38363956 return True
38373957 if _REGEX ["legacy_parameter" ].match (stripped ):
38383958 return False
3959+ if FortranParser ._is_statement_function_statement (stripped ):
3960+ return False
38393961 if "=" in stripped and "::" not in stripped :
38403962 # Distinguish assignment/statements from declaration lines carrying
38413963 # type specs with named arguments, e.g.:
0 commit comments