Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ae41c50
#390 Start to add support for linemarker.
hiker Apr 12, 2026
5e124e5
#390 Improve linermarker handling to support detection of invalid mar…
hiker Apr 13, 2026
7e482a2
#390 Simplified parameterization.
hiker Apr 16, 2026
88a74ee
#390 Fix incorrect linemarker specification in comment.
hiker Apr 16, 2026
8eb3954
#390 Remove need for using an additional regex to verify the linemark…
hiker Apr 16, 2026
fedc82c
#390 Updated manual.
hiker Apr 22, 2026
8480ccf
#390 Updated comments and added proper typing.
hiker Apr 22, 2026
b5e1f9f
#390 Added test that preprocessor directives are indeed properly pars…
hiker Apr 22, 2026
76441b6
#390 Moved location of preprocessor directive (which means it is now …
hiker Apr 22, 2026
099a818
#390 Fixed black.
hiker Apr 22, 2026
1634379
#390 Fix for github black, my black does not need that comma.
hiker Apr 22, 2026
27c7817
#390 Fix missing ',' which resulted in incorrectly concatenating stri…
hiker Apr 28, 2026
09099bf
Merge remote-tracking branch 'origin/master' into 390_ignore_linemarkers
hiker Apr 28, 2026
773ff8a
#390 Fixed previous fix - oops.
hiker Apr 28, 2026
c220665
Merge branch 'master' into 390_ignore_linemarkers
arporter Jun 4, 2026
f7172b3
#390 Covered missing line with test, fixed comments and coding style.
hiker Jun 9, 2026
50da372
#390 Undo my fix to limit line length to 80, since apparently black p…
hiker Jun 9, 2026
267eb90
#390 Undo yet another fix to limit line length to 80, since apparentl…
hiker Jun 9, 2026
ddb78ac
#390 update changelog
arporter Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Modifications by (in alphabetical order):
* P. Vitt, University of Siegen, Germany
* A. Voysey, UK Met Office

09/06/2026 PR #498 for #390 by adding support for linemarkers in parsed
code (e.g. '# 123 "test.f90"').

04/06/2026 PR #507 for #506. Remove setuptools_scm_git dependency. Version
information is now always obtained using importlib.metadata.version.

Expand Down
7 changes: 7 additions & 0 deletions doc/source/fparser2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,12 @@ backslash character `\\` at the end of the line.

__ http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf#page=157

Added is the support for compiler linemarkers, i.e. lines in the format
``# line-number "filename"``, which indicates for a compiler the line number
and filename that the next line came from. While technically not a preprocessor
directive, these statements follow a very similar syntax so their handling
is combined with the preprocessor handling.

The implementation of directives is in the C99Preprocessor.py `file`__
with support for the following::

Expand All @@ -568,6 +574,7 @@ with support for the following::
#error
#warning
#
# line-number "filename"

__ https://github.com/stfc/fparser/blob/master/src/fparser/two/C99Preprocessor.py

Expand Down
66 changes: 65 additions & 1 deletion src/fparser/two/C99Preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,19 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""C99 Preprocessor Syntax Rules."""
"""C99 Preprocessor Syntax Rules. It also supports linemarker statements
(which are technically not preprocessor directives, but are very close
Comment thread
arporter marked this conversation as resolved.
in their syntax, i.e. starting with `#`)

"""

# Author: Balthasar Reuter <balthasar.reuter@ecmwf.int>
# Based on previous work by Martin Schlipf (https://github.com/martin-schlipf)
# First version created: Jan 2020

import re
import sys
from typing import Optional, Union

from fparser.common.readfortran import FortranReaderBase, CppDirective
from fparser.two import pattern_tools as pattern
Expand All @@ -57,6 +62,7 @@
"Cpp_Macro_Stmt",
"Cpp_Undef_Stmt",
"Cpp_Line_Stmt",
"Cpp_Linemarker_Stmt",
"Cpp_Error_Stmt",
"Cpp_Warning_Stmt",
"Cpp_Null_Stmt",
Expand Down Expand Up @@ -649,6 +655,64 @@ def tostr(self):
return "{0} {1}".format(*self.items)


class Cpp_Linemarker_Stmt(WORDClsBase): # Linemarker
"""
This class represents a Linemarker. A linemarker indicates the
line number and file name the following line is coming from (e.g.
if a file has been inlined, this will allow the compiler to correctly
indicate the original source line). While linemarkers are technically
not preprocessor directives, their syntax is very similar, so they are
handled here.

linemarker-stmt is # digit-sequence "s-char-sequence" [digit ...]
"""

subclass_names = []
use_names = ["Cpp_Pp_Tokens"]

# The match method will check that it is a valid linemarker, i.e.
# it has a line number, and file name in double quotes. Setting value
# to None means that the pattern matching will return the matched
# string (i.e. `# linenumber "filename"`), any following flags will
# be stored as items of type Cpp_Pp_Tokens.
_pattern = pattern.Pattern("<linemarker>", r"^\s*#\s+\d+\s+\".*\".*$", value=None)

@staticmethod
def match(
string: Union[str, FortranReaderBase],
) -> Optional[tuple[str, "Cpp_Linemarker_Stmt"]]:
"""Implements the matching for a linemarker.
The optional flag (digits) allowed after the file name are not matched
any further but simply kept as a string.

:param string: the string to match with as a line statement.

:return: a tuple consisting of the string matched and an instance of
Cpp_Linemarker_Stmt or `None` if there is no match.

"""
if not string:
return None

return WORDClsBase.match(
Cpp_Linemarker_Stmt._pattern,
Cpp_Pp_Tokens,
string,
colons=False,
require_cls=False,
)

def tostr(self) -> str:
"""
Returns the line marker as string. Note that fparser accepts
spaces before the `#`, but it should remove the spaces, hence
we lstrip the result

:return: this linemarker as a string.
Comment thread
arporter marked this conversation as resolved.
"""
return self.items[0].lstrip()


class Cpp_Error_Stmt(WORDClsBase): # 6.10.5 Error directive
"""
C99 6.10.5 Error directive
Expand Down
70 changes: 68 additions & 2 deletions src/fparser/two/tests/test_c99preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,14 @@
Cpp_Macro_Identifier_List,
Cpp_Undef_Stmt,
Cpp_Line_Stmt,
Cpp_Linemarker_Stmt,
Cpp_Error_Stmt,
Cpp_Warning_Stmt,
Cpp_Null_Stmt,
Cpp_Pp_Tokens,
)
from fparser.two.utils import NoMatchError
from fparser.two.Fortran2003 import Program
from fparser.two.utils import NoMatchError, walk
from fparser.api import get_reader


Expand Down Expand Up @@ -366,7 +368,8 @@ def test_macro_stmt_with_whitespace(line, ref):
"#def",
"#defnie",
"#definex",
"#define 2a" "#define fail(...,test) test",
"#define 2a",
"#define fail(...,test) test",
"#define",
"#define fail(...,...)",
],
Expand Down Expand Up @@ -451,6 +454,30 @@ def test_incorrect_line_stmt(line):
assert "Cpp_Line_Stmt: '{0}'".format(line) in str(excinfo.value)


@pytest.mark.usefixtures("f2003_create")
@pytest.mark.parametrize(
"line, ref",
[
('# 123 "file"', '# 123 "file"'),
(' # 123 "file"', '# 123 "file"'),
('# 123 "file" 1 3', '# 123 "file" 1 3'),
],
)
def test_linemarker(line, ref):
"""Test that #line is recognized"""
result = Cpp_Linemarker_Stmt(line)
assert str(result) == ref


@pytest.mark.usefixtures("f2003_create")
@pytest.mark.parametrize("line", ["# abc", "", '# "bla"', "# 123 'wrong_quotes'"])
def test_incorrect_linemarker(line):
"""Test that incorrectly formed #line statements raise exception"""
with pytest.raises(NoMatchError) as excinfo:
_ = Cpp_Linemarker_Stmt(line)
assert "Cpp_Linemarker_Stmt: '{0}'".format(line) in str(excinfo.value)


@pytest.mark.usefixtures("f2003_create")
@pytest.mark.parametrize("line", ["#error MSG", " # error MSG "])
def test_error_statement_with_msg(line):
Expand Down Expand Up @@ -525,3 +552,42 @@ def test_incorrect_null_stmt(line):
with pytest.raises(NoMatchError) as excinfo:
_ = Cpp_Null_Stmt(line)
assert "Cpp_Null_Stmt: '{0}'".format(line) in str(excinfo.value)


@pytest.mark.usefixtures("f2003_create")
@pytest.mark.parametrize(
"cpp_class, cpp_directive",
[
(Cpp_If_Stmt, "#if CONST"),
(Cpp_Elif_Stmt, "#elif CONST"),
(Cpp_Endif_Stmt, "#endif"),
(Cpp_Include_Stmt, '#include "test.inc"'),
(Cpp_Macro_Stmt, "#define a b"),
(Cpp_Undef_Stmt, "#undef a"),
(Cpp_Line_Stmt, "#line 123"),
(Cpp_Linemarker_Stmt, '# 123 "test.f90"'),
(Cpp_Error_Stmt, "#error 123"),
(Cpp_Warning_Stmt, "#warning 123"),
(Cpp_Null_Stmt, "#"),
],
)
def test_cpp_in_fortran(cpp_class, cpp_directive):
"""
Verify that all cpp directives are correctly parsed as part of
a real program.
"""
code = f"""
program test
{cpp_directive}
integer a
a = 2
end program
"""
reader = get_reader(code)

obj = Program(reader)
all_cpp_nodes = walk(obj, cpp_class)

# There must be exactly one cpp node
assert len(all_cpp_nodes) == 1
assert str(all_cpp_nodes[0]) == cpp_directive
15 changes: 13 additions & 2 deletions src/fparser/two/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1837,7 +1837,7 @@ def match(keyword, cls, string, colons=False, require_cls=False):
2-tuple containing a string matching the 'WORD' and an \
instance of 'cls' (or None if an instance of cls is not \
required and not provided).
:rtype: Optional[Tupe[Str, Optional[Cls]]]
:rtype: Optional[Tuple[Str, Optional[Cls]]]

"""
if isinstance(keyword, (tuple, list)):
Expand All @@ -1863,7 +1863,18 @@ def match(keyword, cls, string, colons=False, require_cls=False):
if my_match is None:
return None
line = string[len(my_match.group()) :]
pattern_value = keyword.value
# Most patterns set a return value to be used, in order to remove
# white space (e.g. the pattern might be "^\s*(#\s*undef)\b",
# but the return value is `#undef`, meaning all optional white
# space will be removed. But in case of linemarkers, we need
# to match a non-constant expression (`# linenumber "filename"`).
# In this case, value is set to None, and we return the matched
# original string (i.e. the actual line number and filename
# specified)
if keyword.value:
pattern_value = keyword.value
else:
pattern_value = my_match.group()

if not line:
if require_cls:
Expand Down
Loading