Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 @@ -4,6 +4,9 @@

### Fixed

- Fixed a crash (`re.error: invalid group reference`) when a source file defines
a function-like macro with no arguments, e.g. `#define ok() ...`
([#486](https://github.com/fortran-lang/fortls/issues/486))
- Fixed missing registered capability for `textDocument/documentHighlight`
([#421](https://github.com/fortran-lang/fortls/issues/421s))

Expand Down
10 changes: 8 additions & 2 deletions fortls/parsers/internal/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2086,11 +2086,17 @@ def replace_vars(line: str):

def expand_func_macro(def_name: str, def_value: tuple[str, str]):
def_args, sub = def_value
def_args = def_args.split(",")
# "".split(",") is [""], not [], so a zero-argument macro such as
# `#define ok() ...` used to be treated as having one unnamed argument.
# The substitution below then ran `\b()\b`, which matches at every word
# boundary, and peppered the replacement text with group references --
# `ie/=0` became `...\10`, i.e. a reference to group 10, and re raised
# "invalid group reference". Drop empty names so the count is honest.
def_args = [arg for arg in (a.strip() for a in def_args.split(",")) if arg]
regex = re.compile(rf"\b{def_name}\s*\({','.join(['(.*)']*len(def_args))}\)")

for i, arg in enumerate(def_args, start=1):
sub = re.sub(rf"\b({arg.strip()})\b", rf"\\{i}", sub)
sub = re.sub(rf"\b({re.escape(arg)})\b", rf"\\{i}", sub)

return regex, sub

Expand Down
45 changes: 45 additions & 0 deletions test/test_preproc_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,48 @@ def test_pp_macro_expansion():
]
output, _, _, _ = preprocess_file(lines)
assert output == ref


def test_pp_zero_argument_function_macro():
"""A function-like macro with no arguments expands without crashing.

Regression test for #486. `"".split(",")` is `[""]`, not `[]`, so a macro
such as `ok()` was treated as having one unnamed argument. The argument
substitution then ran `\b()\b`, which matches at every word boundary, and
injected group references throughout the replacement text -- `ie/=0` became
`...\10`, and `re` raised "invalid group reference 10".
Comment on lines +44 to +48

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the docstring was not raw, so it held a literal backspace instead of the text. Fixed in 17950af.

"""
lines = [
"#define ok() if(ie/=0) then; return; end if;",
"subroutine b",
"integer :: ie",
"ie = 1",
"ok()",
"end subroutine",
]
ref = [
"#define ok() if(ie/=0) then; return; end if;",
"subroutine b",
"integer :: ie",
"ie = 1",
"if(ie/=0) then; return; end if;",
"end subroutine",
]
output, _, _, _ = preprocess_file(lines)
assert output == ref


def test_pp_function_macro_arities():
"""Zero, one and two argument macros all expand correctly."""
lines = [
"#define NOARG() 42",
"#define SQUARE(x) ((x)*(x))",
"#define ADD(a, b) ((a) + (b))",
"i = NOARG()",
"j = SQUARE(3)",
"k = ADD(1, 2)",
]
output, _, _, _ = preprocess_file(lines)
# The leading space in "( 2)" is pre-existing behaviour: argument capture
# does not strip whitespace. Asserted as-is so this test stays about arity.
assert output[3:] == ["i = 42", "j = ((3)*(3))", "k = ((1) + ( 2))"]
Loading