diff --git a/CHANGELOG.md b/CHANGELOG.md index b075f3c6..bd3b6f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ Modifications by (in alphabetical order): * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +04/06/2026 PR #509 for #505. Fix truncated syntax error reporting when files + have line breaks before the module or program. + 21/04/2026 PR #502. Widen Proc_Decl (R1214) for Fortran 2008 to accept an initial-proc-target (R1217) on the right-hand side of ``=>``, not only null-init. See J3/10-007r1 ยง12.4.3.6. diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index bd536832..f8197a77 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -141,6 +141,7 @@ import re import sys import traceback +from collections import deque from typing import Optional, Tuple from io import StringIO @@ -594,7 +595,7 @@ def __init__( self.process_directives = process_directives self.filo_line = [] # used for un-consuming lines. - self.fifo_item = [] + self.fifo_item = deque() self.source_lines = [] # source lines cache self.f2py_comment_lines = [] # line numbers of f2py directives @@ -825,7 +826,7 @@ def put_item(self, item): # of the corresponding reader. self.reader.put_item(item) else: - self.fifo_item.insert(0, item) + self.fifo_item.appendleft(item) # Iterator methods: @@ -930,11 +931,10 @@ def _next(self, ignore_comments=None): """ if ignore_comments is None: ignore_comments = self._ignore_comments - fifo_item_pop = self.fifo_item.pop while 1: try: # first empty the FIFO item buffer: - item = fifo_item_pop(0) + item = self.fifo_item.popleft() except IndexError: # construct a new item from source item = self.get_source_item() @@ -986,8 +986,8 @@ def _next(self, ignore_comments=None): items.append(new_line) items.reverse() for newitem in items: - self.fifo_item.insert(0, newitem) - return fifo_item_pop(0) + self.fifo_item.appendleft(newitem) + return self.fifo_item.popleft() return item # Interface to returned items: @@ -1663,7 +1663,7 @@ def get_source_item(self): # blank. If it is a comment, it has been pushed onto the # fifo_item list. try: - return self.fifo_item.pop(0) + return self.fifo_item.popleft() except IndexError: # A blank line is represented as an empty comment return Comment("", (startlineno, endlineno), self) diff --git a/src/fparser/one/parsefortran.py b/src/fparser/one/parsefortran.py index afb6b8c4..3ee86d2d 100644 --- a/src/fparser/one/parsefortran.py +++ b/src/fparser/one/parsefortran.py @@ -118,7 +118,7 @@ def put_item(self, item): """ Pushes the given item to the reader. """ - self.reader.fifo_item.insert(0, item) + self.reader.fifo_item.appendleft(item) return def parse(self): diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 19ab1171..177b40e4 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -418,13 +418,8 @@ def match(reader): # Found a syntax error for this rule. Now look to match # (via Main_Program0) with a program containing no program # statement as this is optional in Fortran. - # result = BlockBase.match(Main_Program0, [], None, reader) - if not result and comments: - # This program only contains comments. - return (content,) - else: - return result + return result except StopIteration: # Reader has no more lines. pass diff --git a/src/fparser/two/tests/fortran2003/test_program_r201.py b/src/fparser/two/tests/fortran2003/test_program_r201.py index beac0079..7886bef3 100644 --- a/src/fparser/two/tests/fortran2003/test_program_r201.py +++ b/src/fparser/two/tests/fortran2003/test_program_r201.py @@ -42,14 +42,12 @@ from fparser.api import get_reader from fparser.two.Fortran2003 import Program -# Test no content or just white space. This is not officially a -# Fortran rule but fortran compilers tend to accept empty content so -# we follow their lead. - def test_empty_input(f2003_create): """Test that empty input or input only containing white space can be - parsed succesfully + parsed succesfully. This is not valid fortran but it is accepted by + compilers and some applications produce it when there is files with + preprocessor ifdefs and includes but all resolve to emtpy strings. """ for code in ["", " ", " \n \n\n"]: diff --git a/src/fparser/two/tests/test_comments_and_directives.py b/src/fparser/two/tests/test_comments_and_directives.py index b7914dec..ff0026b3 100644 --- a/src/fparser/two/tests/test_comments_and_directives.py +++ b/src/fparser/two/tests/test_comments_and_directives.py @@ -35,7 +35,7 @@ import pytest from fparser.two.Fortran2003 import Program, Comment, Directive, Subroutine_Subprogram -from fparser.two.utils import walk +from fparser.two.utils import walk, FortranSyntaxError from fparser.api import get_reader from fparser.two.parser import ParserFactory @@ -441,15 +441,11 @@ def test_directive_stmts(): old = reader.get_item() assert old is not None - out = walk(program, Comment) - comments = 0 - for comment in out: - if comment.items[0] != "": - comments = comments + 1 - assert comments == 3 - assert str(out[1]) == "!$dir inline" - assert str(out[3]) == "! A comment!" - assert str(out[4]) == "!!$ Another comment" + comments = walk(program, Comment) + assert len(comments) == 5 + assert str(comments[1]) == "!$dir inline" + assert str(comments[3]) == "! A comment!" + assert str(comments[4]) == "!!$ Another comment" # Check that passing something that isn't a comment into a Directive # __new__ call doesn't create a Directive. @@ -606,3 +602,47 @@ def test_inline_directive_is_comment(): program = Program(reader) out = walk(program, Directive) assert len(out) == 0 + + +def test_syntax_error_with_comments(): + """Test that when we keep comments we still correctly give syntax errors + when the first line of the file is a blank line.""" + source = """ + + +! This is module m + + +module m + integer :: x +contains + subroutine foo() + if (.true.) + x = 0 + end if + end subroutine +end module""" + reader = get_reader(source, ignore_comments=False) + with pytest.raises(FortranSyntaxError) as err: + program = Program(reader) + assert "at line 11\n" in str(err.value) + assert ">>> if (.true.)\n" in str(err.value) + + +def test_base_to_fortran_empty_comment(): + """Test that if we have an empty comment we get the correct + to_fortran from the base class implementation (i.e. no tab)""" + source = """ + !Comment + program test + end program + """ + reader = get_reader(source, ignore_comments=False) + program = Program(reader) + out = walk(program, Comment) + comment = out[1] + assert comment.tofortran(tab=" ") == " !Comment" + # Change the comment to be an empty comment. + comment.items = [""] + comment.item = "" + assert comment.tofortran(tab=" ") == ""