diff --git a/CHANGELOG.md b/CHANGELOG.md index c234d0a4..c596e902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/fortls/parsers/internal/parser.py b/fortls/parsers/internal/parser.py index 1dc93760..9f95264b 100644 --- a/fortls/parsers/internal/parser.py +++ b/fortls/parsers/internal/parser.py @@ -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 diff --git a/test/test_preproc_parser.py b/test/test_preproc_parser.py index c897669a..f5adff67 100644 --- a/test/test_preproc_parser.py +++ b/test/test_preproc_parser.py @@ -36,3 +36,48 @@ def test_pp_macro_expansion(): ] output, _, _, _ = preprocess_file(lines) assert output == ref + + +def test_pp_zero_argument_function_macro(): + r"""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". + """ + 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))"]