Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: adds custom exceptions #380

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.vscode
*.egg-info
venv/
.venv/
dist/
build/
docs/_build/
Expand Down
18 changes: 7 additions & 11 deletions fortls/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,13 @@

import json5

from .exceptions import DebugError, ParameterError, ParserError
from .helper_functions import only_dirs, resolve_globs
from .jsonrpc import JSONRPC2Connection, ReadWriter, path_from_uri
from .langserver import LangServer
from .parsers.internal.parser import FortranFile, preprocess_file


class DebugError(Exception):
"""Base class for debug CLI."""


class ParameterError(DebugError):
"""Exception raised for errors in the parameters."""


def is_debug_mode(args):
debug_flags = [
"debug_diagnostics",
Expand Down Expand Up @@ -425,9 +418,12 @@ def debug_parser(args):

print(f' File = "{args.debug_filepath}"')
file_obj = FortranFile(args.debug_filepath, pp_suffixes)
err_str, _ = file_obj.load_from_disk()
if err_str:
raise DebugError(f"Reading file failed: {err_str}")
try:
file_obj.load_from_disk()
except ParserError as exc:
msg = f"Reading file {args.debug_filepath} failed: {str(exc)}"
raise DebugError(msg) from exc
print(f' File = "{args.debug_filepath}"')
print(f" Detected format: {'fixed' if file_obj.fixed else 'free'}")
print("\n" + "=" * 80 + "\nParser Output\n" + "=" * 80 + "\n")
file_ast = file_obj.parse(debug=True, pp_defs=pp_defs, include_dirs=include_dirs)
Expand Down
17 changes: 17 additions & 0 deletions fortls/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations


class DebugError(Exception):
"""Base class for debug CLI."""


class ParameterError(DebugError):
"""Exception raised for errors in the parameters."""


class ParserError(Exception):
"""Parser base class exception"""


class FortranFileNotFoundError(ParserError, FileNotFoundError):
"""File not found"""
50 changes: 32 additions & 18 deletions fortls/langserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@
load_intrinsics,
set_lowercase_intrinsics,
)
from fortls.parsers.internal.parser import FortranFile, get_line_context
from fortls.parsers.internal.parser import (
FortranFile,
FortranFileNotFoundError,
get_line_context,
)
from fortls.parsers.internal.scope import Scope
from fortls.parsers.internal.use import Use
from fortls.parsers.internal.utilities import (
Expand Down Expand Up @@ -1313,9 +1317,10 @@
return
# Parse newly updated file
if reparse_req:
_, err_str = self.update_workspace_file(path, update_links=True)
if err_str is not None:
self.post_message(f"Change request failed for file '{path}': {err_str}")
try:
self.update_workspace_file(path, update_links=True)
except LSPError as e:
self.post_message(f"Change request failed for file '{path}': {str(e)}")

Check warning on line 1323 in fortls/langserver.py

View check run for this annotation

Codecov / codecov/patch

fortls/langserver.py#L1322-L1323

Added lines #L1322 - L1323 were not covered by tests
return
# Update include statements linking to this file
for _, tmp_file in self.workspace.items():
Expand Down Expand Up @@ -1350,11 +1355,12 @@
for key in ast_old.global_dict:
self.obj_tree.pop(key, None)
return
did_change, err_str = self.update_workspace_file(
filepath, read_file=True, allow_empty=did_open
)
if err_str is not None:
self.post_message(f"Save request failed for file '{filepath}': {err_str}")
try:
did_change = self.update_workspace_file(
filepath, read_file=True, allow_empty=did_open
)
except LSPError as e:
self.post_message(f"Save request failed for file '{filepath}': {str(e)}")

Check warning on line 1363 in fortls/langserver.py

View check run for this annotation

Codecov / codecov/patch

fortls/langserver.py#L1362-L1363

Added lines #L1362 - L1363 were not covered by tests
return
if did_change:
# Update include statements linking to this file
Expand Down Expand Up @@ -1390,12 +1396,14 @@
return False, None
else:
return False, "File does not exist" # Error during load
err_string, file_changed = file_obj.load_from_disk()
if err_string:
log.error("%s : %s", err_string, filepath)
return False, err_string # Error during file read
if not file_changed:
return False, None
try:
file_changed = file_obj.load_from_disk()
if not file_changed:
return False, None
except FortranFileNotFoundError as exc:
log.error("%s : %s", str(exc), filepath)
raise LSPError from exc

Check warning on line 1405 in fortls/langserver.py

View check run for this annotation

Codecov / codecov/patch

fortls/langserver.py#L1403-L1405

Added lines #L1403 - L1405 were not covered by tests

ast_new = file_obj.parse(
pp_defs=self.pp_defs, include_dirs=self.include_dirs
)
Expand Down Expand Up @@ -1452,9 +1460,11 @@
A Fortran file object or a string containing the error message
"""
file_obj = FortranFile(filepath, pp_suffixes)
err_str, _ = file_obj.load_from_disk()
if err_str:
return err_str
# TODO: allow to bubble up the error message
try:
file_obj.load_from_disk()
except FortranFileNotFoundError as e:
return str(e)

Check warning on line 1467 in fortls/langserver.py

View check run for this annotation

Codecov / codecov/patch

fortls/langserver.py#L1466-L1467

Added lines #L1466 - L1467 were not covered by tests
try:
# On Windows multiprocess does not propagate global variables in a shell.
# Windows uses 'spawn' while Unix uses 'fork' which propagates globals.
Expand Down Expand Up @@ -1844,6 +1854,10 @@
sys.setrecursionlimit(limit)


class LSPError(Exception):
"""Base class for Language Server Protocol errors"""


class JSONRPC2Error(Exception):
def __init__(self, code, message, data=None):
self.code = code
Expand Down
123 changes: 57 additions & 66 deletions fortls/parsers/internal/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

# Python < 3.8 does not have typing.Literals
try:
from typing import Literal
from typing import Iterable, Literal
except ImportError:
from typing_extensions import Literal
from typing_extensions import Iterable, Literal

from re import Match, Pattern

Expand All @@ -24,6 +24,7 @@
Severity,
log,
)
from fortls.exceptions import FortranFileNotFoundError
from fortls.ftypes import (
ClassInfo,
FunSig,
Expand Down Expand Up @@ -870,41 +871,45 @@ def copy(self) -> FortranFile:
copy_obj.set_contents(self.contents_split)
return copy_obj

def load_from_disk(self) -> tuple[str | None, bool | None]:
def load_from_disk(self) -> bool:
"""Read file from disk or update file contents only if they have changed
A MD5 hash is used to determine that

Returns
-------
tuple[str|None, bool|None]
``str`` : string containing IO error message else None
``bool``: boolean indicating if the file has changed
bool
boolean indicating if the file has changed

Raises
------
FortranFileNotFoundError
If the file could not be found
"""
contents: str
try:
# errors="replace" prevents UnicodeDecodeError being raised
with open(self.path, encoding="utf-8", errors="replace") as f:
contents = re.sub(r"\t", r" ", f.read())
except OSError:
return "Could not read/decode file", None
else:
# Check if files are the same
try:
hash = hashlib.md5(
contents.encode("utf-8"), usedforsecurity=False
).hexdigest()
# Python <=3.8 does not have the `usedforsecurity` option
except TypeError:
hash = hashlib.md5(contents.encode("utf-8")).hexdigest()

if hash == self.hash:
return None, False

self.hash = hash
self.contents_split = contents.splitlines()
self.fixed = detect_fixed_format(self.contents_split)
self.contents_pp = self.contents_split
self.nLines = len(self.contents_split)
return None, True
except FileNotFoundError as exc:
raise FortranFileNotFoundError(exc) from exc
# Check if files are the same
try:
hash = hashlib.md5(
contents.encode("utf-8"), usedforsecurity=False
).hexdigest()
# Python <=3.8 does not have the `usedforsecurity` option
except TypeError:
hash = hashlib.md5(contents.encode("utf-8")).hexdigest()

if hash == self.hash:
return False

self.hash = hash
self.contents_split = contents.splitlines()
self.fixed = detect_fixed_format(self.contents_split)
self.contents_pp = self.contents_split
self.nLines = len(self.contents_split)
return True

def apply_change(self, change: dict) -> bool:
"""Apply a change to the file."""
Expand Down Expand Up @@ -2070,14 +2075,11 @@ def replace_vars(line: str):

if defs is None:
defs = {}
out_line = replace_defined(text)
out_line = replace_vars(out_line)
try:
line_res = eval(replace_ops(out_line))
except:
return eval(replace_ops(replace_vars(replace_defined(text))))
# This needs to catch all possible exceptions thrown by eval()
except Exception:
return False
else:
return line_res

def expand_func_macro(def_name: str, def_value: tuple[str, str]):
def_args, sub = def_value
Expand All @@ -2096,6 +2098,14 @@ def append_multiline_macro(def_value: str | tuple, line: str):
return (def_args, def_value)
return def_value + line

def find_file_in_directories(directories: Iterable[str], filename: str) -> str:
for include_dir in directories:
file = os.path.join(include_dir, filename)
if os.path.isfile(file):
return file
msg = f"Could not locate include file: {filename} in {directories}"
raise FortranFileNotFoundError(msg)

if pp_defs is None:
pp_defs = {}
if include_dirs is None:
Expand Down Expand Up @@ -2249,40 +2259,21 @@ def append_multiline_macro(def_value: str | tuple, line: str):
if (match is not None) and ((len(pp_stack) == 0) or (pp_stack[-1][0] < 0)):
log.debug("%s !!! Include statement(%d)", line.strip(), i + 1)
include_filename = match.group(1).replace('"', "")
include_path = None
# Intentionally keep this as a list and not a set. There are cases
# where projects play tricks with the include order of their headers
# to get their codes to compile. Using a set would not permit that.
for include_dir in include_dirs:
include_path_tmp = os.path.join(include_dir, include_filename)
if os.path.isfile(include_path_tmp):
include_path = os.path.abspath(include_path_tmp)
break
if include_path is not None:
try:
include_file = FortranFile(include_path)
err_string, _ = include_file.load_from_disk()
if err_string is None:
log.debug("\n!!! Parsing include file '%s'", include_path)
_, _, _, defs_tmp = preprocess_file(
include_file.contents_split,
file_path=include_path,
pp_defs=defs_tmp,
include_dirs=include_dirs,
debug=debug,
)
log.debug("!!! Completed parsing include file\n")

else:
log.debug("!!! Failed to parse include file: %s", err_string)

except:
log.debug("!!! Failed to parse include file: exception")

else:
log.debug(
"%s !!! Could not locate include file (%d)", line.strip(), i + 1
try:
include_path = find_file_in_directories(include_dirs, include_filename)
include_file = FortranFile(include_path)
include_file.load_from_disk()
log.debug("\n!!! Parsing include file '%s'", include_path)
_, _, _, defs_tmp = preprocess_file(
include_file.contents_split,
file_path=include_path,
pp_defs=defs_tmp,
include_dirs=include_dirs,
debug=debug,
)
log.debug("!!! Completed parsing include file")
except FortranFileNotFoundError as e:
log.debug("%s !!! %s - Ln:%d", line.strip(), str(e), i + 1)

# Substitute (if any) read in preprocessor macros
for def_tmp, value in defs_tmp.items():
Expand Down
Loading