diff --git a/CHANGELOG.md b/CHANGELOG.md index dfcf1965..8a478156 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Fixed +- Fixed preprocessor bug with `if` and `elif` conditionals + ([#322](https://github.com/fortran-lang/fortls/issues/322)) - Fixed bug where type fields or methods were not detected if spaces were used around `%` ([#286](https://github.com/fortran-lang/fortls/issues/286)) diff --git a/fortls/parse_fortran.py b/fortls/parse_fortran.py index 235b5c08..b707aea9 100644 --- a/fortls/parse_fortran.py +++ b/fortls/parse_fortran.py @@ -2090,6 +2090,7 @@ def replace_vars(line: str): pp_skips = [] pp_defines = [] pp_stack = [] + pp_stack_group = [] defs_tmp = pp_defs.copy() def_regexes = {} output_file = [] @@ -2135,25 +2136,49 @@ def replace_vars(line: str): # Closing/middle conditional statements inc_start = False exc_start = False + exc_continue = False if match.group(1) == "elif": - if pp_stack[-1][0] < 0: - pp_stack[-1][0] = i + 1 - exc_start = True + if (not pp_stack_group) or (pp_stack_group[-1][0] != len(pp_stack)): + # First elif statement for this elif group + if pp_stack[-1][0] < 0: + pp_stack_group.append([len(pp_stack), True]) + else: + pp_stack_group.append([len(pp_stack), False]) + if pp_stack_group[-1][1]: + # An earlier if or elif in this group has been true + exc_continue = True + if pp_stack[-1][0] < 0: + pp_stack[-1][0] = i + 1 + elif eval_pp_if(line[match.end(1) :], defs_tmp): + pp_stack[-1][1] = i + 1 + pp_skips.append(pp_stack.pop()) + pp_stack_group[-1][1] = True + pp_stack.append([-1, -1]) + inc_start = True else: - if eval_pp_if(line[match.end(1) :], defs_tmp): - pp_stack[-1][1] = i - 1 - pp_stack.append([-1, -1]) - inc_start = True + exc_start = True elif match.group(1) == "else": if pp_stack[-1][0] < 0: pp_stack[-1][0] = i + 1 exc_start = True + elif ( + pp_stack_group + and (pp_stack_group[-1][0] == len(pp_stack)) + and (pp_stack_group[-1][1]) + ): + # An earlier if or elif in this group has been true + exc_continue = True else: pp_stack[-1][1] = i + 1 + pp_skips.append(pp_stack.pop()) + pp_stack.append([-1, -1]) inc_start = True elif match.group(1) == "endif": + if pp_stack_group and (pp_stack_group[-1][0] == len(pp_stack)): + pp_stack_group.pop() if pp_stack[-1][0] < 0: pp_stack.pop() + log.debug(f"{line.strip()} !!! Conditional TRUE/END({i + 1})") continue if pp_stack[-1][1] < 0: pp_stack[-1][1] = i + 1 @@ -2164,6 +2189,8 @@ def replace_vars(line: str): log.debug(f"{line.strip()} !!! Conditional TRUE({i + 1})") elif exc_start: log.debug(f"{line.strip()} !!! Conditional FALSE({i + 1})") + elif exc_continue: + log.debug(f"{line.strip()} !!! Conditional EXCLUDED({i + 1})") continue # Handle variable/macro definitions files match = FRegex.PP_DEF.match(line) diff --git a/test/test_preproc.py b/test/test_preproc.py index e1bdd49d..50f50607 100644 --- a/test/test_preproc.py +++ b/test/test_preproc.py @@ -30,6 +30,18 @@ def check_return(result_array, checks): string += hover_req(file_path, 10, 15) # defined without () file_path = root_dir / "preproc_keywords.F90" string += hover_req(file_path, 6, 2) # ignores PP across Fortran line continuations + file_path = root_dir / "preproc_else.F90" + string += hover_req(file_path, 8, 12) + string += hover_req(file_path, 18, 12) + file_path = root_dir / "preproc_elif.F90" + string += hover_req(file_path, 22, 15) + string += hover_req(file_path, 24, 10) + file_path = root_dir / "preproc_elif_elif_skip.F90" + string += hover_req(file_path, 30, 23) + file_path = root_dir / "preproc_if_elif_else.F90" + string += hover_req(file_path, 30, 23) + file_path = root_dir / "preproc_if_elif_skip.F90" + string += hover_req(file_path, 30, 23) config = str(root_dir / ".pp_conf.json") errcode, results = run_request(string, ["--config", config]) assert errcode == 0 @@ -49,6 +61,13 @@ def check_return(result_array, checks): ), "```fortran90\n#define SUCCESS .true.\n```", "```fortran90\nREAL, CONTIGUOUS, POINTER, DIMENSION(:) :: var1\n```", + "```fortran90\nINTEGER :: var0\n```", + "```fortran90\nREAL :: var1\n```", + "```fortran90\nINTEGER :: var2\n```", + "```fortran90\nINTEGER, INTENT(INOUT) :: var\n```", + "```fortran90\nINTEGER, PARAMETER :: res = 0+1+0+0\n```", + "```fortran90\nINTEGER, PARAMETER :: res = 0+0+0+1\n```", + "```fortran90\nINTEGER, PARAMETER :: res = 1+0+0+0\n```", ) assert len(ref_results) == len(results) - 1 check_return(results[1:], ref_results) diff --git a/test/test_source/pp/preproc_elif.F90 b/test/test_source/pp/preproc_elif.F90 new file mode 100644 index 00000000..87138247 --- /dev/null +++ b/test/test_source/pp/preproc_elif.F90 @@ -0,0 +1,27 @@ +subroutine preprocessor_elif(var, var3, var4, var5, var6) + +! This file, as used in test_preproc, checks that +! 1. the steps after the preprocessor parsing has fully finished, are only +! using content from the parts within the preprocessor if-elif-else that +! should be used. To do this, it has some regular fortran code within the +! #if and #elif. +! 2. the #endif correctly concludes the if-elif, so any new #define statements +! that come after the #endif, are picked up during the preprocessor parsing. + +#if 0 +integer, intent(in) :: var +#elif 1 +integer, intent(inout) :: var +var = 3 +#else +integer, intent(out) :: var +var = 5 +#endif + +#define OTHERTYPE integer + +OTHERTYPE :: var2 + +PRINT*, var + +endsubroutine preprocessor_elif diff --git a/test/test_source/pp/preproc_elif_elif_skip.F90 b/test/test_source/pp/preproc_elif_elif_skip.F90 new file mode 100644 index 00000000..a41250b2 --- /dev/null +++ b/test/test_source/pp/preproc_elif_elif_skip.F90 @@ -0,0 +1,33 @@ +subroutine preprocessor_elif_elif_skip() + +! This file, as used in test_preproc, and together with the two similar files, +! tests that when there is an if-elif-elif-else, only the first branch that +! evaluates to true is used, and the others ignored. Also when multiple +! conditions evaluate to true. + +#if 0 +#define PART1 0 +#elif 1 +#define PART2 1 +#elif 1 +#define PART3 0 +#else +#define PART4 0 +#endif + +#ifndef PART1 +#define PART1 0 +#endif +#ifndef PART2 +#define PART2 0 +#endif +#ifndef PART3 +#define PART3 0 +#endif +#ifndef PART4 +#define PART4 0 +#endif + +integer, parameter :: res = PART1+PART2+PART3+PART4 + +end subroutine preprocessor_elif_elif_skip diff --git a/test/test_source/pp/preproc_else.F90 b/test/test_source/pp/preproc_else.F90 new file mode 100644 index 00000000..edd19f63 --- /dev/null +++ b/test/test_source/pp/preproc_else.F90 @@ -0,0 +1,21 @@ +subroutine preprocessor_else(var) + +#if 0 +#define MYTYPE logical +#else +#define MYTYPE integer +#endif + +MYTYPE :: var0 + +#undef MYTYPE + +#if 1 +#define MYTYPE real +#else +#define MYTYPE character +#endif + +MYTYPE :: var1 + +endsubroutine preprocessor_else diff --git a/test/test_source/pp/preproc_if_elif_else.F90 b/test/test_source/pp/preproc_if_elif_else.F90 new file mode 100644 index 00000000..8457ab60 --- /dev/null +++ b/test/test_source/pp/preproc_if_elif_else.F90 @@ -0,0 +1,33 @@ +subroutine preprocessor_if_elif_else() + +! This file, as used in test_preproc, and together with the two similar files, +! tests that when there is an if-elif-elif-else, only the first branch that +! evaluates to true is used, and the others ignored. Also when multiple +! conditions evaluate to true. + +#if 0 +#define PART1 0 +#elif 0 +#define PART2 0 +#elif 0 +#define PART3 0 +#else +#define PART4 1 +#endif + +#ifndef PART1 +#define PART1 0 +#endif +#ifndef PART2 +#define PART2 0 +#endif +#ifndef PART3 +#define PART3 0 +#endif +#ifndef PART4 +#define PART4 0 +#endif + +integer, parameter :: res = PART1+PART2+PART3+PART4 + +endsubroutine preprocessor_if_elif_else diff --git a/test/test_source/pp/preproc_if_elif_skip.F90 b/test/test_source/pp/preproc_if_elif_skip.F90 new file mode 100644 index 00000000..d478fd0e --- /dev/null +++ b/test/test_source/pp/preproc_if_elif_skip.F90 @@ -0,0 +1,33 @@ +subroutine preprocessor_if_elif_skip() + +! This file, as used in test_preproc, and together with the two similar files, +! tests that when there is an if-elif-elif-else, only the first branch that +! evaluates to true is used, and the others ignored. Also when multiple +! conditions evaluate to true. + +#if 1 +#define PART1 1 +#elif 0 +#define PART2 0 +#elif 1 +#define PART3 0 +#else +#define PART4 0 +#endif + +#ifndef PART1 +#define PART1 0 +#endif +#ifndef PART2 +#define PART2 0 +#endif +#ifndef PART3 +#define PART3 0 +#endif +#ifndef PART4 +#define PART4 0 +#endif + +integer, parameter :: res = PART1+PART2+PART3+PART4 + +end subroutine preprocessor_if_elif_skip