From ab843b54560ae649ef179148ee9d747f866d4f9e Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Fri, 29 May 2026 14:23:10 +0100 Subject: [PATCH 1/8] Fix for issue 505 by removing blank line comments being generated as real Comments --- src/fparser/two/Fortran2003.py | 6 +++ .../two/tests/test_comments_and_directives.py | 42 +++++++++++++------ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 19ab1171..038c9f07 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -238,6 +238,12 @@ def __new__(cls, string, parent_cls=None): from fparser.common import readfortran if isinstance(string, readfortran.Comment): + # We can reach this with false readfortran.Comment nodes + # that represent empty lines in the original input. These + # have no syntactic relevance, and so are not kept by + # fparser. + if string.comment == "": + return # We were after a comment and we got a comment. Construct # one manually to avoid recursively calling this __new__ # method again... diff --git a/src/fparser/two/tests/test_comments_and_directives.py b/src/fparser/two/tests/test_comments_and_directives.py index b7914dec..bedba960 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 @@ -92,7 +92,7 @@ def test_simple_prog(): " ! A full line comment\n" ' PRINT *, "Hello"\n' " ! This block gets executed\n" - "END PROGRAM a_prog\n" + "END PROGRAM a_prog" ) @@ -120,7 +120,7 @@ def test_ifthen(): ' PRINT *, "Hello"\n' " ! Another full line comment\n" " END IF\n" - "END PROGRAM a_prog\n" + "END PROGRAM a_prog" ) @@ -147,7 +147,7 @@ def test_inline_ifthen(): " ! An in-line comment here\n" " END IF\n" " ! A comment after a block\n" - "END PROGRAM a_prog\n" + "END PROGRAM a_prog" ) @@ -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) == 3 + assert str(comments[0]) == "!$dir inline" + assert str(comments[1]) == "! A comment!" + assert str(comments[2]) == "!!$ Another comment" # Check that passing something that isn't a comment into a Directive # __new__ call doesn't create a Directive. @@ -606,3 +602,23 @@ 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 = """ +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 6\n" in str(err.value) + assert ">>> if (.true.)\n" in str(err.value) From a303e1d581f478026f56edb5d7a740af89390030 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Fri, 29 May 2026 15:51:20 +0100 Subject: [PATCH 2/8] Fixes for reivew --- example/test_files/make_public_correct.f90 | 8 -------- src/fparser/two/tests/test_comments_and_directives.py | 7 ++++++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/example/test_files/make_public_correct.f90 b/example/test_files/make_public_correct.f90 index 44af5932..617285fb 100644 --- a/example/test_files/make_public_correct.f90 +++ b/example/test_files/make_public_correct.f90 @@ -1,31 +1,23 @@ MODULE a_mod - ! Access_Stmt private will be removed: - ! Attr_Spec with 0 and 1 additional attribute, the protected will be remobed REAL :: planet_radius = 123 REAL, PARAMETER :: planet_radius_constant = 123 - LOGICAL :: public_protected = .FALSE. LOGICAL :: only_protected = .FALSE. LOGICAL :: private_protected = .FALSE. - ! Access_stmt with public, this will be unmodified PUBLIC :: public_protected ! Protected_Stmt - the whole statement will be removed ! Access_stmt with private - the whole statement will be removed - TYPE :: my_type ! Private_Components_Stmt in a type will be removed INTEGER :: a, b CONTAINS ! This private will also be removed. - END TYPE my_type - ! Access_Spec - the `private` will be removed TYPE(my_type), PUBLIC :: my_var - CONTAINS SUBROUTINE sub_a END SUBROUTINE sub_a diff --git a/src/fparser/two/tests/test_comments_and_directives.py b/src/fparser/two/tests/test_comments_and_directives.py index bedba960..050fac4b 100644 --- a/src/fparser/two/tests/test_comments_and_directives.py +++ b/src/fparser/two/tests/test_comments_and_directives.py @@ -608,6 +608,11 @@ 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 @@ -620,5 +625,5 @@ def test_syntax_error_with_comments(): reader = get_reader(source, ignore_comments=False) with pytest.raises(FortranSyntaxError) as err: program = Program(reader) - assert "at line 6\n" in str(err.value) + assert "at line 11\n" in str(err.value) assert ">>> if (.true.)\n" in str(err.value) From 3fde3acd09dd9b0260917ff5012fbafd9c41fc70 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Mon, 1 Jun 2026 10:47:46 +0100 Subject: [PATCH 3/8] Add missing coverage --- .../two/tests/test_comments_and_directives.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/fparser/two/tests/test_comments_and_directives.py b/src/fparser/two/tests/test_comments_and_directives.py index 050fac4b..d15b255c 100644 --- a/src/fparser/two/tests/test_comments_and_directives.py +++ b/src/fparser/two/tests/test_comments_and_directives.py @@ -627,3 +627,22 @@ def test_syntax_error_with_comments(): 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[0] + assert comment.tofortran(tab=" ") == " !Comment" + # Change the comment to be an empty comment. + comment.items = [""] + comment.item = "" + assert comment.tofortran(tab=" ") == "" From e8d60b3a911e1e685ee2d820e40ed93da707dd98 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Wed, 3 Jun 2026 13:51:30 +0100 Subject: [PATCH 4/8] Swap the fifo_item list to a deque --- src/fparser/common/readfortran.py | 15 ++++++++------- src/fparser/one/parsefortran.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index bd536832..056feeff 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,11 @@ def _next(self, ignore_comments=None): """ if ignore_comments is None: ignore_comments = self._ignore_comments - fifo_item_pop = self.fifo_item.pop + fifo_item_pop = self.fifo_item.popleft while 1: try: # first empty the FIFO item buffer: - item = fifo_item_pop(0) + item = fifo_item_pop() except IndexError: # construct a new item from source item = self.get_source_item() @@ -986,8 +987,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 fifo_item_pop() return item # Interface to returned items: @@ -1663,7 +1664,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): From ec9ff56dbc8f8e46846b3969f0e8d496ce984ca9 Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 4 Jun 2026 14:07:15 +0100 Subject: [PATCH 5/8] Reintroduce empty lines, but fix the syntax bug in another way --- example/test_files/make_public_correct.f90 | 8 ++++++++ src/fparser/two/Fortran2003.py | 10 +++------- .../two/tests/test_comments_and_directives.py | 16 ++++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/example/test_files/make_public_correct.f90 b/example/test_files/make_public_correct.f90 index 617285fb..44af5932 100644 --- a/example/test_files/make_public_correct.f90 +++ b/example/test_files/make_public_correct.f90 @@ -1,23 +1,31 @@ MODULE a_mod + ! Access_Stmt private will be removed: + ! Attr_Spec with 0 and 1 additional attribute, the protected will be remobed REAL :: planet_radius = 123 REAL, PARAMETER :: planet_radius_constant = 123 + LOGICAL :: public_protected = .FALSE. LOGICAL :: only_protected = .FALSE. LOGICAL :: private_protected = .FALSE. + ! Access_stmt with public, this will be unmodified PUBLIC :: public_protected ! Protected_Stmt - the whole statement will be removed ! Access_stmt with private - the whole statement will be removed + TYPE :: my_type ! Private_Components_Stmt in a type will be removed INTEGER :: a, b CONTAINS ! This private will also be removed. + END TYPE my_type + ! Access_Spec - the `private` will be removed TYPE(my_type), PUBLIC :: my_var + CONTAINS SUBROUTINE sub_a END SUBROUTINE sub_a diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 038c9f07..1d3b2963 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -242,8 +242,8 @@ def __new__(cls, string, parent_cls=None): # that represent empty lines in the original input. These # have no syntactic relevance, and so are not kept by # fparser. - if string.comment == "": - return + #if string.comment == "": + # return # We were after a comment and we got a comment. Construct # one manually to avoid recursively calling this __new__ # method again... @@ -426,11 +426,7 @@ def match(reader): # 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/test_comments_and_directives.py b/src/fparser/two/tests/test_comments_and_directives.py index d15b255c..ff0026b3 100644 --- a/src/fparser/two/tests/test_comments_and_directives.py +++ b/src/fparser/two/tests/test_comments_and_directives.py @@ -92,7 +92,7 @@ def test_simple_prog(): " ! A full line comment\n" ' PRINT *, "Hello"\n' " ! This block gets executed\n" - "END PROGRAM a_prog" + "END PROGRAM a_prog\n" ) @@ -120,7 +120,7 @@ def test_ifthen(): ' PRINT *, "Hello"\n' " ! Another full line comment\n" " END IF\n" - "END PROGRAM a_prog" + "END PROGRAM a_prog\n" ) @@ -147,7 +147,7 @@ def test_inline_ifthen(): " ! An in-line comment here\n" " END IF\n" " ! A comment after a block\n" - "END PROGRAM a_prog" + "END PROGRAM a_prog\n" ) @@ -442,10 +442,10 @@ def test_directive_stmts(): assert old is not None comments = walk(program, Comment) - assert len(comments) == 3 - assert str(comments[0]) == "!$dir inline" - assert str(comments[1]) == "! A comment!" - assert str(comments[2]) == "!!$ Another 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. @@ -640,7 +640,7 @@ def test_base_to_fortran_empty_comment(): reader = get_reader(source, ignore_comments=False) program = Program(reader) out = walk(program, Comment) - comment = out[0] + comment = out[1] assert comment.tofortran(tab=" ") == " !Comment" # Change the comment to be an empty comment. comment.items = [""] From c402bdb3d309334cb300bab6462de20dea6a729a Mon Sep 17 00:00:00 2001 From: LonelyCat124 <3043914+LonelyCat124@users.noreply.github.com.> Date: Thu, 4 Jun 2026 14:19:55 +0100 Subject: [PATCH 6/8] Remove old comments --- src/fparser/two/Fortran2003.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py index 1d3b2963..177b40e4 100644 --- a/src/fparser/two/Fortran2003.py +++ b/src/fparser/two/Fortran2003.py @@ -238,12 +238,6 @@ def __new__(cls, string, parent_cls=None): from fparser.common import readfortran if isinstance(string, readfortran.Comment): - # We can reach this with false readfortran.Comment nodes - # that represent empty lines in the original input. These - # have no syntactic relevance, and so are not kept by - # fparser. - #if string.comment == "": - # return # We were after a comment and we got a comment. Construct # one manually to avoid recursively calling this __new__ # method again... @@ -424,7 +418,6 @@ 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) return result except StopIteration: From 73dd3a9989c1cd0a80c3365c0fef2f1bf9f17531 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 4 Jun 2026 14:57:32 +0100 Subject: [PATCH 7/8] Update docstring and syntax --- src/fparser/common/readfortran.py | 5 ++--- src/fparser/two/tests/fortran2003/test_program_r201.py | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/fparser/common/readfortran.py b/src/fparser/common/readfortran.py index 056feeff..f8197a77 100644 --- a/src/fparser/common/readfortran.py +++ b/src/fparser/common/readfortran.py @@ -931,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.popleft while 1: try: # first empty the FIFO item buffer: - item = fifo_item_pop() + item = self.fifo_item.popleft() except IndexError: # construct a new item from source item = self.get_source_item() @@ -988,7 +987,7 @@ def _next(self, ignore_comments=None): items.reverse() for newitem in items: self.fifo_item.appendleft(newitem) - return fifo_item_pop() + return self.fifo_item.popleft() return item # Interface to returned items: 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"]: From a4fbd657809e3fb5863e4314a135ed5b92399335 Mon Sep 17 00:00:00 2001 From: Sergi Siso Date: Thu, 4 Jun 2026 15:20:43 +0100 Subject: [PATCH 8/8] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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.