Skip to content

Commit e668508

Browse files
committed
Adds glob support for all path inputs
1. Addresses multiple declarations for external functions #169 but more robustly. 2. Moves only_dirs method outside of class. Now --debug_parser will resolve the glob patterns in the settings file the same way as when it is run. 3. source_dirs and the exlcude_dirs have been converted into sets to reduce lookup times
1 parent 5f5572d commit e668508

File tree

3 files changed

+142
-106
lines changed

3 files changed

+142
-106
lines changed

fortls/__init__.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from ._version import __version__
1111
from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri
12-
from .langserver import LangServer
12+
from .langserver import LangServer, resolve_globs, only_dirs
1313
from .parse_fortran import fortran_file, process_file
1414

1515

@@ -710,17 +710,16 @@ def debug_server_parser(args):
710710
config_dict = json.load(fhandle)
711711
pp_suffixes = config_dict.get("pp_suffixes", None)
712712
pp_defs = config_dict.get("pp_defs", {})
713-
include_dirs = config_dict.get("include_dirs", [])
713+
include_dirs = []
714+
for path in config_dict.get("include_dirs", []):
715+
include_dirs.extend(
716+
only_dirs(resolve_globs(path, args.debug_rootpath))
717+
)
718+
714719
if isinstance(pp_defs, list):
715720
pp_defs = {key: "" for key in pp_defs}
716721
except:
717722
print("Error while parsing '.fortls' settings file")
718-
# Make relative include paths absolute
719-
for (i, include_dir) in enumerate(include_dirs):
720-
if not os.path.isabs(include_dir):
721-
include_dirs[i] = os.path.abspath(
722-
os.path.join(args.debug_rootpath, include_dir)
723-
)
724723
#
725724
print("\nTesting parser")
726725
print(' File = "{0}"'.format(args.debug_filepath))

fortls/langserver.py

Lines changed: 135 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
# TODO: make FORTRAN_EXT_REGEX be able to update from user input extensions
2+
# TODO: enable jsonc C-style comments
3+
4+
from __future__ import annotations
5+
6+
import json
17
import logging
28
import os
39
import re
410
import traceback
11+
from pathlib import Path
512

613
from fortls.intrinsics import (
714
get_intrinsic_keywords,
@@ -38,7 +45,7 @@
3845

3946
log = logging.getLogger(__name__)
4047
# Global regexes
41-
FORTRAN_EXT_REGEX = re.compile(r"^\.F(77|90|95|03|08|OR|PP)?$", re.I)
48+
FORTRAN_EXT_REGEX = re.compile(r"\.F(77|90|95|03|08|OR|PP)?$", re.I)
4249
INT_STMNT_REGEX = re.compile(r"^[ ]*[a-z]*$", re.I)
4350
TYPE_DEF_REGEX = re.compile(r"[ ]*(TYPE|CLASS)[ ]*\([a-z0-9_ ]*$", re.I)
4451
SCOPE_DEF_REGEX = re.compile(r"[ ]*(MODULE|PROGRAM|SUBROUTINE|FUNCTION)[ ]+", re.I)
@@ -66,7 +73,7 @@ def init_file(filepath, pp_defs, pp_suffixes, include_dirs):
6673

6774

6875
def get_line_prefix(pre_lines, curr_line, iChar):
69-
"""Get code line prefix from current line and preceeding continuation lines"""
76+
"""Get code line prefix from current line and preceding continuation lines"""
7077
if (curr_line is None) or (iChar > len(curr_line)) or (curr_line.startswith("#")):
7178
return None
7279
prepend_string = "".join(pre_lines)
@@ -87,6 +94,56 @@ def get_line_prefix(pre_lines, curr_line, iChar):
8794
return line_prefix
8895

8996

97+
def resolve_globs(glob_path: str, root_path: str = None) -> list[str]:
98+
"""Resolve glob patterns
99+
100+
Parameters
101+
----------
102+
glob_path : str
103+
Path containing the glob pattern follows
104+
`fnmatch` glob pattern, can include relative paths, etc.
105+
see fnmatch: https://docs.python.org/3/library/fnmatch.html#module-fnmatch
106+
107+
root_path : str, optional
108+
root path to start glob search. If left empty the root_path will be
109+
extracted from the glob_path, by default None
110+
111+
Returns
112+
-------
113+
list[str]
114+
Expanded glob patterns with absolute paths.
115+
Absolute paths are used to resolve any potential ambiguity
116+
"""
117+
# Path.glob returns a generator, we then cast the Path obj to a str
118+
# alternatively use p.as_posix()
119+
if root_path:
120+
return [str(p) for p in Path(root_path).resolve().glob(glob_path)]
121+
# Attempt to extract the root and glob pattern from the glob_path
122+
# This is substantially less robust that then above
123+
else:
124+
p = Path(glob_path).expanduser()
125+
parts = p.parts[p.is_absolute() :]
126+
return [str(i) for i in Path(p.root).resolve().glob(str(Path(*parts)))]
127+
128+
129+
def only_dirs(paths: list[str], err_msg: list = []) -> list[str]:
130+
dirs: list[str] = []
131+
for p in paths:
132+
if os.path.isdir(p):
133+
dirs.append(p)
134+
elif os.path.isfile(p):
135+
continue
136+
else:
137+
msg: str = (
138+
f"Directory '{p}' specified in '.fortls' settings file does not exist"
139+
)
140+
if err_msg:
141+
err_msg.append([2, msg])
142+
else:
143+
print(f"WARNING: {msg}")
144+
return dirs
145+
146+
90147
class LangServer:
91148
def __init__(self, conn, debug_log=False, settings={}):
92149
self.conn = conn
@@ -97,8 +154,8 @@ def __init__(self, conn, debug_log=False, settings={}):
97154
self.workspace = {}
98155
self.obj_tree = {}
99156
self.link_version = 0
100-
self.source_dirs = []
101-
self.excl_paths = []
157+
self.source_dirs = set()
158+
self.excl_paths = set()
102159
self.excl_suffixes = []
103160
self.post_messages = []
104161
self.pp_suffixes = None
@@ -220,50 +277,46 @@ def serve_initialize(self, request):
220277
self.root_path = path_from_uri(
221278
params.get("rootUri") or params.get("rootPath") or ""
222279
)
223-
self.source_dirs.append(self.root_path)
280+
self.source_dirs.add(self.root_path)
224281
# Check for config file
225282
config_path = os.path.join(self.root_path, ".fortls")
226283
config_exists = os.path.isfile(config_path)
227284
if config_exists:
228285
try:
229-
import json
286+
with open(config_path, "r") as jsonfile:
287+
# Allow for jsonc C-style commnets
288+
# jsondata = "".join(
289+
# line for line in jsonfile if not line.startswith("//")
290+
# )
291+
# config_dict = json.loads(jsondata)
292+
config_dict = json.load(jsonfile)
230293

231-
with open(config_path, "r") as fhandle:
232-
config_dict = json.load(fhandle)
233-
for excl_path in config_dict.get("excl_paths", []):
234-
self.excl_paths.append(os.path.join(self.root_path, excl_path))
235-
source_dirs = config_dict.get("source_dirs", [])
236-
ext_source_dirs = config_dict.get("ext_source_dirs", [])
237-
# Legacy definition
238-
if len(source_dirs) == 0:
239-
source_dirs = config_dict.get("mod_dirs", [])
240-
for source_dir in source_dirs:
241-
dir_path = os.path.join(self.root_path, source_dir)
242-
if os.path.isdir(dir_path):
243-
self.source_dirs.append(dir_path)
244-
else:
245-
self.post_messages.append(
246-
[
247-
2,
248-
r'Source directory "{0}" specified in '
249-
r'".fortls" settings file does not exist'.format(
250-
dir_path
251-
),
252-
]
253-
)
254-
for ext_source_dir in ext_source_dirs:
255-
if os.path.isdir(ext_source_dir):
256-
self.source_dirs.append(ext_source_dir)
257-
else:
258-
self.post_messages.append(
259-
[
260-
2,
261-
r'External source directory "{0}" specified in '
262-
r'".fortls" settings file does not exist'.format(
263-
ext_source_dir
264-
),
265-
]
294+
# Exclude paths (directories & files)
295+
# with glob resolution
296+
for path in config_dict.get("excl_paths", []):
297+
self.excl_paths.update(set(resolve_globs(path, self.root_path)))
298+
299+
# Source directory paths (directories)
300+
# with glob resolution
301+
# XXX: Drop support for ext_source_dirs since they end up in
302+
# source_dirs anyway
303+
source_dirs = config_dict.get("source_dirs", []) + config_dict.get(
304+
"ext_source_dirs", []
305+
)
306+
for path in source_dirs:
307+
self.source_dirs.update(
308+
set(
309+
only_dirs(
310+
resolve_globs(path, self.root_path),
311+
self.post_messages,
312+
)
266313
)
314+
)
315+
# Keep all directories present in source_dirs but not excl_paths
316+
self.source_dirs = {
317+
i for i in self.source_dirs if i not in self.excl_paths
318+
}
319+
267320
self.excl_suffixes = config_dict.get("excl_suffixes", [])
268321
self.lowercase_intrinsics = config_dict.get(
269322
"lowercase_intrinsics", self.lowercase_intrinsics
@@ -274,7 +327,12 @@ def serve_initialize(self, request):
274327
)
275328
self.pp_suffixes = config_dict.get("pp_suffixes", None)
276329
self.pp_defs = config_dict.get("pp_defs", {})
277-
self.include_dirs = config_dict.get("include_dirs", [])
330+
for path in config_dict.get("include_dirs", []):
331+
self.include_dirs.extend(
332+
only_dirs(
333+
resolve_globs(path, self.root_path), self.post_messages
334+
)
335+
)
278336
self.max_line_length = config_dict.get(
279337
"max_line_length", self.max_line_length
280338
)
@@ -285,14 +343,9 @@ def serve_initialize(self, request):
285343
self.pp_defs = {key: "" for key in self.pp_defs}
286344
except:
287345
self.post_messages.append(
288-
[1, 'Error while parsing ".fortls" settings file']
346+
[1, "Error while parsing '.fortls' settings file"]
289347
)
290-
# Make relative include paths absolute
291-
for (i, include_dir) in enumerate(self.include_dirs):
292-
if not os.path.isabs(include_dir):
293-
self.include_dirs[i] = os.path.abspath(
294-
os.path.join(self.root_path, include_dir)
295-
)
348+
296349
# Setup logging
297350
if self.debug_log and (self.root_path != ""):
298351
logging.basicConfig(
@@ -316,22 +369,15 @@ def serve_initialize(self, request):
316369
self.obj_tree[module.FQSN] = [module, None]
317370
# Set object settings
318371
set_keyword_ordering(self.sort_keywords)
319-
# Recursively add sub-directories
372+
# Recursively add sub-directories that only match Fortran extensions
320373
if len(self.source_dirs) == 1:
321-
self.source_dirs = []
322-
for dirName, subdirList, fileList in os.walk(self.root_path):
323-
if self.excl_paths.count(dirName) > 0:
324-
while len(subdirList) > 0:
325-
del subdirList[0]
374+
self.source_dirs = set()
375+
for root, dirs, files in os.walk(self.root_path):
376+
# Match not found
377+
if not list(filter(FORTRAN_EXT_REGEX.search, files)):
326378
continue
327-
contains_source = False
328-
for filename in fileList:
329-
_, ext = os.path.splitext(os.path.basename(filename))
330-
if FORTRAN_EXT_REGEX.match(ext):
331-
contains_source = True
332-
break
333-
if contains_source:
334-
self.source_dirs.append(dirName)
379+
if root not in self.source_dirs and root not in self.excl_paths:
380+
self.source_dirs.add(str(Path(root).resolve()))
335381
# Initialize workspace
336382
self.workspace_init()
337383
#
@@ -799,6 +845,7 @@ def get_definition(self, def_file, def_line, def_char):
799845
pre_lines, curr_line, _ = def_file.get_code_line(
800846
def_line, forward=False, strip_comment=True
801847
)
848+
# Returns none for string literals, when the query is in the middle
802849
line_prefix = get_line_prefix(pre_lines, curr_line, def_char)
803850
if line_prefix is None:
804851
return None
@@ -1024,7 +1071,7 @@ def serve_references(self, request):
10241071
def_obj = self.get_definition(file_obj, def_line, def_char)
10251072
if def_obj is None:
10261073
return None
1027-
# Determine global accesibility and type membership
1074+
# Determine global accessibility and type membership
10281075
restrict_file = None
10291076
type_mem = False
10301077
if def_obj.FQSN.count(":") > 2:
@@ -1309,10 +1356,8 @@ def serve_onChange(self, request):
13091356
path = path_from_uri(uri)
13101357
file_obj = self.workspace.get(path)
13111358
if file_obj is None:
1312-
self.post_message(
1313-
'Change request failed for unknown file "{0}"'.format(path)
1314-
)
1315-
log.error('Change request failed for unknown file "%s"', path)
1359+
self.post_message(f"Change request failed for unknown file '{path}'")
1360+
log.error("Change request failed for unknown file '%s'", path)
13161361
return
13171362
else:
13181363
# Update file contents with changes
@@ -1327,11 +1372,11 @@ def serve_onChange(self, request):
13271372
reparse_req = reparse_req or reparse_flag
13281373
except:
13291374
self.post_message(
1330-
'Change request failed for file "{0}": Could not apply change'
1331-
.format(path)
1375+
f"Change request failed for file '{path}': Could not apply"
1376+
" change"
13321377
)
13331378
log.error(
1334-
'Change request failed for file "%s": Could not apply change',
1379+
"Change request failed for file '%s': Could not apply change",
13351380
path,
13361381
exc_info=True,
13371382
)
@@ -1340,9 +1385,7 @@ def serve_onChange(self, request):
13401385
if reparse_req:
13411386
_, err_str = self.update_workspace_file(path, update_links=True)
13421387
if err_str is not None:
1343-
self.post_message(
1344-
'Change request failed for file "{0}": {1}'.format(path, err_str)
1345-
)
1388+
self.post_message(f"Change request failed for file '{path}': {err_str}")
13461389
return
13471390
# Update include statements linking to this file
13481391
for _, tmp_file in self.workspace.items():
@@ -1378,9 +1421,7 @@ def serve_onSave(self, request, did_open=False, did_close=False):
13781421
filepath, read_file=True, allow_empty=did_open
13791422
)
13801423
if err_str is not None:
1381-
self.post_message(
1382-
'Save request failed for file "{0}": {1}'.format(filepath, err_str)
1383-
)
1424+
self.post_message(f"Save request failed for file '{filepath}': {err_str}")
13841425
return
13851426
if did_change:
13861427
# Update include statements linking to this file
@@ -1446,20 +1487,22 @@ def update_workspace_file(
14461487
def workspace_init(self):
14471488
# Get filenames
14481489
file_list = []
1449-
for source_dir in self.source_dirs:
1450-
for filename in os.listdir(source_dir):
1451-
_, ext = os.path.splitext(os.path.basename(filename))
1452-
if FORTRAN_EXT_REGEX.match(ext):
1453-
filepath = os.path.normpath(os.path.join(source_dir, filename))
1454-
if self.excl_paths.count(filepath) > 0:
1455-
continue
1456-
inc_file = True
1457-
for excl_suffix in self.excl_suffixes:
1458-
if filepath.endswith(excl_suffix):
1459-
inc_file = False
1460-
break
1461-
if inc_file:
1462-
file_list.append(filepath)
1490+
for src_dir in self.source_dirs:
1491+
for f in os.listdir(src_dir):
1492+
p = os.path.join(src_dir, f)
1493+
# Process only files
1494+
if not os.path.isfile(p):
1495+
continue
1496+
# File extension must match supported extensions
1497+
if not FORTRAN_EXT_REGEX.search(f):
1498+
continue
1499+
# File cannot be in excluded paths/files
1500+
if p in self.excl_paths:
1501+
continue
1502+
# File cannot have an excluded extension
1503+
if any(f.endswith(ext) for ext in set(self.excl_suffixes)):
1504+
continue
1505+
file_list.append(p)
14631506
# Process files
14641507
from multiprocessing import Pool
14651508

@@ -1476,12 +1519,7 @@ def workspace_init(self):
14761519
result_obj = result.get()
14771520
if result_obj[0] is None:
14781521
self.post_messages.append(
1479-
[
1480-
1,
1481-
'Initialization failed for file "{0}": {1}'.format(
1482-
path, result_obj[1]
1483-
),
1484-
]
1522+
[1, f"Initialization failed for file '{path}': {result_obj[1]}"]
14851523
)
14861524
continue
14871525
self.workspace[path] = result_obj[0]

0 commit comments

Comments
 (0)