Skip to content

Commit 65ea44f

Browse files
authored
Merge pull request #498 from stfc/390_ignore_linemarkers
390 ignore linemarkers
2 parents 73e31b5 + ddb78ac commit 65ea44f

5 files changed

Lines changed: 156 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Modifications by (in alphabetical order):
2222
* P. Vitt, University of Siegen, Germany
2323
* A. Voysey, UK Met Office
2424

25+
09/06/2026 PR #498 for #390 by adding support for linemarkers in parsed
26+
code (e.g. '# 123 "test.f90"').
27+
2528
04/06/2026 PR #507 for #506. Remove setuptools_scm_git dependency. Version
2629
information is now always obtained using importlib.metadata.version.
2730

doc/source/fparser2.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,12 @@ backslash character `\\` at the end of the line.
552552

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

555+
Added is the support for compiler linemarkers, i.e. lines in the format
556+
``# line-number "filename"``, which indicates for a compiler the line number
557+
and filename that the next line came from. While technically not a preprocessor
558+
directive, these statements follow a very similar syntax so their handling
559+
is combined with the preprocessor handling.
560+
555561
The implementation of directives is in the C99Preprocessor.py `file`__
556562
with support for the following::
557563

@@ -568,6 +574,7 @@ with support for the following::
568574
#error
569575
#warning
570576
#
577+
# line-number "filename"
571578

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

src/fparser/two/C99Preprocessor.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,19 @@
3232
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
3333
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3434

35-
"""C99 Preprocessor Syntax Rules."""
35+
"""C99 Preprocessor Syntax Rules. It also supports linemarker statements
36+
(which are technically not preprocessor directives, but are very close
37+
in their syntax, i.e. starting with `#`)
38+
39+
"""
3640

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

4145
import re
4246
import sys
47+
from typing import Optional, Union
4348

4449
from fparser.common.readfortran import FortranReaderBase, CppDirective
4550
from fparser.two import pattern_tools as pattern
@@ -57,6 +62,7 @@
5762
"Cpp_Macro_Stmt",
5863
"Cpp_Undef_Stmt",
5964
"Cpp_Line_Stmt",
65+
"Cpp_Linemarker_Stmt",
6066
"Cpp_Error_Stmt",
6167
"Cpp_Warning_Stmt",
6268
"Cpp_Null_Stmt",
@@ -649,6 +655,64 @@ def tostr(self):
649655
return "{0} {1}".format(*self.items)
650656

651657

658+
class Cpp_Linemarker_Stmt(WORDClsBase): # Linemarker
659+
"""
660+
This class represents a Linemarker. A linemarker indicates the
661+
line number and file name the following line is coming from (e.g.
662+
if a file has been inlined, this will allow the compiler to correctly
663+
indicate the original source line). While linemarkers are technically
664+
not preprocessor directives, their syntax is very similar, so they are
665+
handled here.
666+
667+
linemarker-stmt is # digit-sequence "s-char-sequence" [digit ...]
668+
"""
669+
670+
subclass_names = []
671+
use_names = ["Cpp_Pp_Tokens"]
672+
673+
# The match method will check that it is a valid linemarker, i.e.
674+
# it has a line number, and file name in double quotes. Setting value
675+
# to None means that the pattern matching will return the matched
676+
# string (i.e. `# linenumber "filename"`), any following flags will
677+
# be stored as items of type Cpp_Pp_Tokens.
678+
_pattern = pattern.Pattern("<linemarker>", r"^\s*#\s+\d+\s+\".*\".*$", value=None)
679+
680+
@staticmethod
681+
def match(
682+
string: Union[str, FortranReaderBase],
683+
) -> Optional[tuple[str, "Cpp_Linemarker_Stmt"]]:
684+
"""Implements the matching for a linemarker.
685+
The optional flag (digits) allowed after the file name are not matched
686+
any further but simply kept as a string.
687+
688+
:param string: the string to match with as a line statement.
689+
690+
:return: a tuple consisting of the string matched and an instance of
691+
Cpp_Linemarker_Stmt or `None` if there is no match.
692+
693+
"""
694+
if not string:
695+
return None
696+
697+
return WORDClsBase.match(
698+
Cpp_Linemarker_Stmt._pattern,
699+
Cpp_Pp_Tokens,
700+
string,
701+
colons=False,
702+
require_cls=False,
703+
)
704+
705+
def tostr(self) -> str:
706+
"""
707+
Returns the line marker as string. Note that fparser accepts
708+
spaces before the `#`, but it should remove the spaces, hence
709+
we lstrip the result
710+
711+
:return: this linemarker as a string.
712+
"""
713+
return self.items[0].lstrip()
714+
715+
652716
class Cpp_Error_Stmt(WORDClsBase): # 6.10.5 Error directive
653717
"""
654718
C99 6.10.5 Error directive

src/fparser/two/tests/test_c99preprocessor.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,14 @@
5757
Cpp_Macro_Identifier_List,
5858
Cpp_Undef_Stmt,
5959
Cpp_Line_Stmt,
60+
Cpp_Linemarker_Stmt,
6061
Cpp_Error_Stmt,
6162
Cpp_Warning_Stmt,
6263
Cpp_Null_Stmt,
6364
Cpp_Pp_Tokens,
6465
)
65-
from fparser.two.utils import NoMatchError
66+
from fparser.two.Fortran2003 import Program
67+
from fparser.two.utils import NoMatchError, walk
6668
from fparser.api import get_reader
6769

6870

@@ -366,7 +368,8 @@ def test_macro_stmt_with_whitespace(line, ref):
366368
"#def",
367369
"#defnie",
368370
"#definex",
369-
"#define 2a" "#define fail(...,test) test",
371+
"#define 2a",
372+
"#define fail(...,test) test",
370373
"#define",
371374
"#define fail(...,...)",
372375
],
@@ -451,6 +454,30 @@ def test_incorrect_line_stmt(line):
451454
assert "Cpp_Line_Stmt: '{0}'".format(line) in str(excinfo.value)
452455

453456

457+
@pytest.mark.usefixtures("f2003_create")
458+
@pytest.mark.parametrize(
459+
"line, ref",
460+
[
461+
('# 123 "file"', '# 123 "file"'),
462+
(' # 123 "file"', '# 123 "file"'),
463+
('# 123 "file" 1 3', '# 123 "file" 1 3'),
464+
],
465+
)
466+
def test_linemarker(line, ref):
467+
"""Test that #line is recognized"""
468+
result = Cpp_Linemarker_Stmt(line)
469+
assert str(result) == ref
470+
471+
472+
@pytest.mark.usefixtures("f2003_create")
473+
@pytest.mark.parametrize("line", ["# abc", "", '# "bla"', "# 123 'wrong_quotes'"])
474+
def test_incorrect_linemarker(line):
475+
"""Test that incorrectly formed #line statements raise exception"""
476+
with pytest.raises(NoMatchError) as excinfo:
477+
_ = Cpp_Linemarker_Stmt(line)
478+
assert "Cpp_Linemarker_Stmt: '{0}'".format(line) in str(excinfo.value)
479+
480+
454481
@pytest.mark.usefixtures("f2003_create")
455482
@pytest.mark.parametrize("line", ["#error MSG", " # error MSG "])
456483
def test_error_statement_with_msg(line):
@@ -525,3 +552,42 @@ def test_incorrect_null_stmt(line):
525552
with pytest.raises(NoMatchError) as excinfo:
526553
_ = Cpp_Null_Stmt(line)
527554
assert "Cpp_Null_Stmt: '{0}'".format(line) in str(excinfo.value)
555+
556+
557+
@pytest.mark.usefixtures("f2003_create")
558+
@pytest.mark.parametrize(
559+
"cpp_class, cpp_directive",
560+
[
561+
(Cpp_If_Stmt, "#if CONST"),
562+
(Cpp_Elif_Stmt, "#elif CONST"),
563+
(Cpp_Endif_Stmt, "#endif"),
564+
(Cpp_Include_Stmt, '#include "test.inc"'),
565+
(Cpp_Macro_Stmt, "#define a b"),
566+
(Cpp_Undef_Stmt, "#undef a"),
567+
(Cpp_Line_Stmt, "#line 123"),
568+
(Cpp_Linemarker_Stmt, '# 123 "test.f90"'),
569+
(Cpp_Error_Stmt, "#error 123"),
570+
(Cpp_Warning_Stmt, "#warning 123"),
571+
(Cpp_Null_Stmt, "#"),
572+
],
573+
)
574+
def test_cpp_in_fortran(cpp_class, cpp_directive):
575+
"""
576+
Verify that all cpp directives are correctly parsed as part of
577+
a real program.
578+
"""
579+
code = f"""
580+
program test
581+
{cpp_directive}
582+
integer a
583+
a = 2
584+
end program
585+
"""
586+
reader = get_reader(code)
587+
588+
obj = Program(reader)
589+
all_cpp_nodes = walk(obj, cpp_class)
590+
591+
# There must be exactly one cpp node
592+
assert len(all_cpp_nodes) == 1
593+
assert str(all_cpp_nodes[0]) == cpp_directive

src/fparser/two/utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,7 +1837,7 @@ def match(keyword, cls, string, colons=False, require_cls=False):
18371837
2-tuple containing a string matching the 'WORD' and an \
18381838
instance of 'cls' (or None if an instance of cls is not \
18391839
required and not provided).
1840-
:rtype: Optional[Tupe[Str, Optional[Cls]]]
1840+
:rtype: Optional[Tuple[Str, Optional[Cls]]]
18411841
18421842
"""
18431843
if isinstance(keyword, (tuple, list)):
@@ -1863,7 +1863,18 @@ def match(keyword, cls, string, colons=False, require_cls=False):
18631863
if my_match is None:
18641864
return None
18651865
line = string[len(my_match.group()) :]
1866-
pattern_value = keyword.value
1866+
# Most patterns set a return value to be used, in order to remove
1867+
# white space (e.g. the pattern might be "^\s*(#\s*undef)\b",
1868+
# but the return value is `#undef`, meaning all optional white
1869+
# space will be removed. But in case of linemarkers, we need
1870+
# to match a non-constant expression (`# linenumber "filename"`).
1871+
# In this case, value is set to None, and we return the matched
1872+
# original string (i.e. the actual line number and filename
1873+
# specified)
1874+
if keyword.value:
1875+
pattern_value = keyword.value
1876+
else:
1877+
pattern_value = my_match.group()
18671878

18681879
if not line:
18691880
if require_cls:

0 commit comments

Comments
 (0)