Skip to content

Commit 8396e79

Browse files
emanspeaksgnikit
authored andcommitted
init commit of cherry-picked changes from #341
1 parent 922e984 commit 8396e79

File tree

9 files changed

+196
-36
lines changed

9 files changed

+196
-36
lines changed

fortls/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,11 @@ def locate_config(root: str) -> str | None:
501501
error_exit(f"Reading file failed: {err_str}")
502502
print(f" Detected format: {'fixed' if file_obj.fixed else 'free'}")
503503
print("\n=========\nParser Output\n=========\n")
504-
file_ast = file_obj.parse(debug=True, pp_defs=pp_defs, include_dirs=include_dirs)
504+
file_ast = file_obj.parse(
505+
debug=True,
506+
pp_defs=pp_defs,
507+
include_dirs=include_dirs,
508+
)
505509
print("\n=========\nObject Tree\n=========\n")
506510
for obj in file_ast.get_scopes():
507511
print("{}: {}".format(obj.get_type(), obj.FQSN))

fortls/langserver.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -751,11 +751,18 @@ def get_definition(
751751
return None
752752
# Search in Preprocessor defined variables
753753
if def_name in def_file.pp_defs:
754+
def_value = def_file.pp_defs.get(def_name)
755+
def_arg_str = ""
756+
if isinstance(def_value, tuple):
757+
def_arg_str, def_value = def_value
758+
def_arg_str = ", ".join([x.strip() for x in def_arg_str.split(",")])
759+
def_arg_str = f"({def_arg_str})"
760+
754761
var = Variable(
755762
def_file.ast,
756763
def_line + 1,
757764
def_name,
758-
f"#define {def_name} {def_file.pp_defs.get(def_name)}",
765+
f"#define {def_name}{def_arg_str} {def_value}",
759766
[],
760767
)
761768
return var
@@ -1316,7 +1323,9 @@ def serve_onChange(self, request: dict):
13161323
# Update inheritance (currently file only)
13171324
# tmp_file.ast.resolve_links(self.obj_tree, self.link_version)
13181325
elif file_obj.preproc:
1319-
file_obj.preprocess(pp_defs=self.pp_defs)
1326+
file_obj.preprocess(
1327+
pp_defs=self.pp_defs,
1328+
)
13201329
self.pp_defs = {**self.pp_defs, **file_obj.pp_defs}
13211330

13221331
def serve_onOpen(self, request: dict):
@@ -1389,7 +1398,8 @@ def update_workspace_file(
13891398
if not file_changed:
13901399
return False, None
13911400
ast_new = file_obj.parse(
1392-
pp_defs=self.pp_defs, include_dirs=self.include_dirs
1401+
pp_defs=self.pp_defs,
1402+
include_dirs=self.include_dirs,
13931403
)
13941404
# Add the included read in pp_defs from to the ones specified in the
13951405
# configuration file
@@ -1453,7 +1463,10 @@ def file_init(
14531463
# This is a bypass.
14541464
# For more see on SO: shorturl.at/hwAG1
14551465
set_keyword_ordering(sort)
1456-
file_ast = file_obj.parse(pp_defs=pp_defs, include_dirs=include_dirs)
1466+
file_ast = file_obj.parse(
1467+
pp_defs=pp_defs,
1468+
include_dirs=include_dirs,
1469+
)
14571470
except:
14581471
log.error("Error while parsing file %s", filepath, exc_info=True)
14591472
return "Error during parsing"

fortls/parsers/internal/parser.py

+82-22
Original file line numberDiff line numberDiff line change
@@ -1176,7 +1176,10 @@ def find_word_in_code_line(
11761176
return line_no, word_range
11771177

11781178
def preprocess(
1179-
self, pp_defs: dict = None, include_dirs: set = None, debug: bool = False
1179+
self,
1180+
pp_defs: dict = None,
1181+
include_dirs: set = None,
1182+
debug: bool = False,
11801183
) -> tuple[list, list]:
11811184
if pp_defs is None:
11821185
pp_defs = {}
@@ -1265,7 +1268,9 @@ def parse(
12651268
if self.preproc:
12661269
log.debug("=== PreProc Pass ===\n")
12671270
pp_skips, pp_defines = self.preprocess(
1268-
pp_defs=pp_defs, include_dirs=include_dirs, debug=debug
1271+
pp_defs=pp_defs,
1272+
include_dirs=include_dirs,
1273+
debug=debug,
12691274
)
12701275
for pp_reg in pp_skips:
12711276
file_ast.start_ppif(pp_reg[0])
@@ -2038,6 +2043,7 @@ def replace_ops(expr: str):
20382043
expr = expr.replace("!=", " <> ")
20392044
expr = expr.replace("!", " not ")
20402045
expr = expr.replace(" <> ", " != ")
2046+
20412047
return expr
20422048

20432049
def replace_defined(line: str):
@@ -2070,7 +2076,9 @@ def replace_vars(line: str):
20702076

20712077
if defs is None:
20722078
defs = {}
2073-
out_line = replace_defined(text)
2079+
2080+
out_line = text
2081+
out_line = replace_defined(out_line)
20742082
out_line = replace_vars(out_line)
20752083
try:
20762084
line_res = eval(replace_ops(out_line))
@@ -2098,26 +2106,27 @@ def replace_vars(line: str):
20982106
if def_cont_name is not None:
20992107
output_file.append("")
21002108
if line.rstrip()[-1] != "\\":
2101-
defs_tmp[def_cont_name] += line.strip()
2109+
append_multiline_macro(defs_tmp, def_cont_name, line.strip())
21022110
def_cont_name = None
21032111
else:
2104-
defs_tmp[def_cont_name] += line[0:-1].strip()
2112+
append_multiline_macro(defs_tmp, def_cont_name, line[0:-1].strip())
2113+
21052114
continue
21062115
# Handle conditional statements
21072116
match = FRegex.PP_REGEX.match(line)
2108-
if match:
2117+
if match and check_pp_prefix(match.group(1)):
21092118
output_file.append(line)
21102119
def_name = None
21112120
if_start = False
21122121
# Opening conditional statements
2113-
if match.group(1) == "if ":
2114-
is_path = eval_pp_if(line[match.end(1) :], defs_tmp)
2122+
if match.group(2).lower() == "if ":
2123+
is_path = eval_pp_if(line[match.end(2) :], defs_tmp)
21152124
if_start = True
2116-
elif match.group(1) == "ifdef":
2125+
elif match.group(2).lower() == "ifdef":
21172126
if_start = True
21182127
def_name = line[match.end(0) :].strip()
21192128
is_path = def_name in defs_tmp
2120-
elif match.group(1) == "ifndef":
2129+
elif match.group(2).lower() == "ifndef":
21212130
if_start = True
21222131
def_name = line[match.end(0) :].strip()
21232132
is_path = not (def_name in defs_tmp)
@@ -2135,7 +2144,7 @@ def replace_vars(line: str):
21352144
inc_start = False
21362145
exc_start = False
21372146
exc_continue = False
2138-
if match.group(1) == "elif":
2147+
if match.group(2).lower() == "elif":
21392148
if (not pp_stack_group) or (pp_stack_group[-1][0] != len(pp_stack)):
21402149
# First elif statement for this elif group
21412150
if pp_stack[-1][0] < 0:
@@ -2147,15 +2156,15 @@ def replace_vars(line: str):
21472156
exc_continue = True
21482157
if pp_stack[-1][0] < 0:
21492158
pp_stack[-1][0] = i + 1
2150-
elif eval_pp_if(line[match.end(1) :], defs_tmp):
2159+
elif eval_pp_if(line[match.end(2) :], defs_tmp):
21512160
pp_stack[-1][1] = i + 1
21522161
pp_skips.append(pp_stack.pop())
21532162
pp_stack_group[-1][1] = True
21542163
pp_stack.append([-1, -1])
21552164
inc_start = True
21562165
else:
21572166
exc_start = True
2158-
elif match.group(1) == "else":
2167+
elif match.group(2).lower() == "else":
21592168
if pp_stack[-1][0] < 0:
21602169
pp_stack[-1][0] = i + 1
21612170
exc_start = True
@@ -2171,7 +2180,7 @@ def replace_vars(line: str):
21712180
pp_skips.append(pp_stack.pop())
21722181
pp_stack.append([-1, -1])
21732182
inc_start = True
2174-
elif match.group(1) == "endif":
2183+
elif match.group(2).lower() == "endif":
21752184
if pp_stack_group and (pp_stack_group[-1][0] == len(pp_stack)):
21762185
pp_stack_group.pop()
21772186
if pp_stack[-1][0] < 0:
@@ -2192,10 +2201,12 @@ def replace_vars(line: str):
21922201
continue
21932202
# Handle variable/macro definitions files
21942203
match = FRegex.PP_DEF.match(line)
2195-
if (match is not None) and ((len(pp_stack) == 0) or (pp_stack[-1][0] < 0)):
2204+
if (match is not None and check_pp_prefix(match.group(1))) and (
2205+
(len(pp_stack) == 0) or (pp_stack[-1][0] < 0)
2206+
):
21962207
output_file.append(line)
21972208
pp_defines.append(i + 1)
2198-
def_name = match.group(2)
2209+
def_name = match.group(3)
21992210
# If this is an argument list of a function add them to the name
22002211
# get_definition will only return the function name upon hover
22012212
# hence if the argument list is appended in the def_name then
@@ -2204,18 +2215,29 @@ def replace_vars(line: str):
22042215
# This also does not allow for multiline argument list definitions.
22052216
# if match.group(3):
22062217
# def_name += match.group(3)
2207-
if (match.group(1) == "define") and (def_name not in defs_tmp):
2218+
if (match.group(2) == "define") and (def_name not in defs_tmp):
22082219
eq_ind = line[match.end(0) :].find(" ")
2220+
if eq_ind < 0:
2221+
eq_ind = line[match.end(0) :].find("\t")
2222+
22092223
if eq_ind >= 0:
22102224
# Handle multiline macros
22112225
if line.rstrip()[-1] == "\\":
2212-
defs_tmp[def_name] = line[match.end(0) + eq_ind : -1].strip()
2226+
def_value = line[match.end(0) + eq_ind : -1].strip()
22132227
def_cont_name = def_name
22142228
else:
2215-
defs_tmp[def_name] = line[match.end(0) + eq_ind :].strip()
2229+
def_value = line[match.end(0) + eq_ind :].strip()
22162230
else:
2217-
defs_tmp[def_name] = "True"
2218-
elif (match.group(1) == "undef") and (def_name in defs_tmp):
2231+
def_value = "True"
2232+
2233+
# are there arguments to parse?
2234+
if match.group(4):
2235+
def_value = (match.group(5), def_value)
2236+
2237+
defs_tmp[def_name] = def_value
2238+
elif (
2239+
match.group(2) == "undef"
2240+
) and (def_name in defs_tmp):
22192241
defs_tmp.pop(def_name, None)
22202242
log.debug(f"{line.strip()} !!! Define statement({i + 1})")
22212243
continue
@@ -2265,8 +2287,16 @@ def replace_vars(line: str):
22652287
continue
22662288
def_regex = def_regexes.get(def_tmp)
22672289
if def_regex is None:
2268-
def_regex = re.compile(rf"\b{def_tmp}\b")
2290+
if isinstance(value, tuple):
2291+
def_regex = expand_def_func_macro(def_tmp, value)
2292+
else:
2293+
def_regex = re.compile(rf"\b{def_tmp}\b")
2294+
22692295
def_regexes[def_tmp] = def_regex
2296+
2297+
if isinstance(def_regex, tuple):
2298+
def_regex, value = def_regex
2299+
22702300
line_new, nsubs = def_regex.subn(value, line)
22712301
if nsubs > 0:
22722302
log.debug(
@@ -2275,3 +2305,33 @@ def replace_vars(line: str):
22752305
line = line_new
22762306
output_file.append(line)
22772307
return output_file, pp_skips, pp_defines, defs_tmp
2308+
2309+
2310+
def expand_def_func_macro(def_name: str, def_value: tuple[str, str]):
2311+
def_args, sub = def_value
2312+
def_args = def_args.split(",")
2313+
regex = re.compile(rf"\b{def_name}\s*\({','.join(['(.*)']*len(def_args))}\)")
2314+
2315+
for i, arg in enumerate(def_args):
2316+
arg = arg.strip()
2317+
sub = re.sub(rf"\b({arg})\b", rf"\\{i + 1}", sub)
2318+
2319+
return regex, sub
2320+
2321+
2322+
def append_multiline_macro(pp_defs: dict, def_name: str, line: str):
2323+
def_value = pp_defs[def_name]
2324+
def_args = None
2325+
if isinstance(def_value, tuple):
2326+
def_args, def_value = def_value
2327+
2328+
def_value += line
2329+
2330+
if def_args is not None:
2331+
def_value = (def_args, def_value)
2332+
2333+
pp_defs[def_name] = def_value
2334+
2335+
2336+
def check_pp_prefix(prefix: str):
2337+
return prefix == "#"

fortls/regex_patterns.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,18 @@ class FortranRegularExpressions:
124124
FREE_FORMAT_TEST: Pattern = compile(r"[ ]{1,4}[a-z]", I)
125125
# Preprocessor matching rules
126126
DEFINED: Pattern = compile(r"defined[ ]*\(?[ ]*([a-z_]\w*)[ ]*\)?", I)
127-
PP_REGEX: Pattern = compile(r"#(if |ifdef|ifndef|else|elif|endif)")
128-
PP_DEF: Pattern = compile(r"#(define|undef)[ ]*([\w]+)(\((\w+(,[ ]*)?)+\))?", I)
127+
PP_REGEX: Pattern = compile(
128+
r"[ ]*(#)[ ]*(if |ifdef|ifndef|else|elif|endif)",
129+
I,
130+
)
131+
PP_DEF: Pattern = compile(
132+
r"[ ]*(#)[ ]*(define|undef|undefined)"
133+
r"[ ]+(\w+)(\([ ]*([ \w,]*?)[ ]*\))?",
134+
I,
135+
)
129136
PP_DEF_TEST: Pattern = compile(r"(![ ]*)?defined[ ]*\([ ]*(\w*)[ ]*\)$", I)
130-
PP_INCLUDE: Pattern = compile(r"#include[ ]*([\"\w\.]*)", I)
131-
PP_ANY: Pattern = compile(r"(^#:?\w+)")
137+
PP_INCLUDE: Pattern = compile(r"[ ]*#[ ]*include[ ]+([\"\w\.]*)", I)
138+
PP_ANY: Pattern = compile(r"(^[ ]*(?:#)[ ]*\w*:?\w+)")
132139
# Context matching rules
133140
CALL: Pattern = compile(r"[ ]*CALL[ ]+[\w%]*$", I)
134141
INT_STMNT: Pattern = compile(r"^[ ]*[a-z]*$", I)

test/test_preproc.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def check_return(result_array, checks):
4242
string += hover_req(file_path, 30, 23)
4343
file_path = root_dir / "preproc_if_elif_skip.F90"
4444
string += hover_req(file_path, 30, 23)
45+
file_path = root_dir / "preproc_spacing_arg_defs.F90"
46+
string += hover_req(file_path, 11, 20)
47+
string += hover_req(file_path, 20, 17)
48+
string += hover_req(file_path, 22, 13)
4549
config = str(root_dir / ".pp_conf.json")
4650
errcode, results = run_request(string, ["--config", config])
4751
assert errcode == 0
@@ -52,12 +56,12 @@ def check_return(result_array, checks):
5256
"```fortran90\n#define PETSC_ERR_INT_OVERFLOW 84\n```",
5357
"```fortran90\n#define varVar 55\n```",
5458
(
55-
"```fortran90\n#define ewrite if (priority <= 3) write((priority),"
56-
" format)\n```"
59+
"```fortran90\n#define ewrite(priority, format)"
60+
" if (priority <= 3) write((priority), format)\n```"
5761
),
5862
(
59-
"```fortran90\n#define ewrite2 if (priority <= 3) write((priority),"
60-
" format)\n```"
63+
"```fortran90\n#define ewrite2(priority, format)"
64+
" if (priority <= 3) write((priority), format)\n```"
6165
),
6266
"```fortran90\n#define SUCCESS .true.\n```",
6367
"```fortran90\nREAL, CONTIGUOUS, POINTER, DIMENSION(:) :: var1\n```",
@@ -68,6 +72,9 @@ def check_return(result_array, checks):
6872
"```fortran90\nINTEGER, PARAMETER :: res = 0+1+0+0\n```",
6973
"```fortran90\nINTEGER, PARAMETER :: res = 0+0+0+1\n```",
7074
"```fortran90\nINTEGER, PARAMETER :: res = 1+0+0+0\n```",
75+
"```fortran90\n#define MAYBEWRAP(PROCEDURE) PROCEDURE\n```",
76+
"```fortran90\nSUBROUTINE test_type_set_test()\n```",
77+
"```fortran90\n#define MACROARGS(x, y) x + y\n```",
7178
)
7279
assert len(ref_results) == len(results) - 1
7380
check_return(results[1:], ref_results)

test/test_server.py

+5
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def test_workspace_symbols():
175175
def check_return(result_array):
176176
# Expected objects
177177
objs = (
178+
["argtest", 13, 16],
178179
["test", 6, 7],
179180
["test_abstract", 2, 0],
180181
["test_associate_block", 2, 0],
@@ -196,7 +197,11 @@ def check_return(result_array):
196197
["test_str1", 13, 5],
197198
["test_str2", 13, 5],
198199
["test_sub", 6, 8],
200+
["test_type", 5, 5],
201+
["test_type_set_test", 6, 25],
199202
["test_vis_mod", 2, 0],
203+
["the_test", 13, 15],
204+
["wrap_test_type_set_test", 6, 33],
200205
)
201206
assert len(result_array) == len(objs)
202207
for i, obj in enumerate(objs):

test/test_source/pp/.pp_conf.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"pp_suffixes": [".h", ".F90"],
88
"incl_suffixes": [".h"],
99
"include_dirs": ["include"],
10-
"pp_defs": { "HAVE_CONTIGUOUS": "" }
10+
"pp_defs": { "HAVE_CONTIGUOUS": "" },
1111
}

test/test_source/pp/include/indent.h

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
!! sample code adapted from json-fortran/json_macros.inc
2+
3+
# define SPACING_TEST
4+
# define FILE_ENCODING ,encoding='UTF-8'
5+
6+
# ifdef __GFORTRAN__
7+
! gfortran uses cpp in old-school compatibility mode so
8+
! the # stringify and ## concatenate operators don't work
9+
! but we can use C/C++ style comment to ensure PROCEDURE is
10+
! correctly tokenized and prepended with 'wrap_' when the
11+
! macro is expanded
12+
# define MAYBEWRAP(PROCEDURE) PROCEDURE , wrap_/**/PROCEDURE
13+
# else
14+
! Intel's fpp does support the more contemporary ## concatenation
15+
! operator, but doesn't treat the C/C++ comments the same way.
16+
! If you use the gfortran approach and pass the -noB switch to
17+
! fpp, the macro will expand, but with a space between wrap_ and
18+
! whatever PROCEDURE expands to
19+
# define MAYBEWRAP(PROCEDURE) PROCEDURE
20+
# endif
21+
22+
# define MACROARGS( x , y ) x + y

0 commit comments

Comments
 (0)