Skip to content

Commit d62e83b

Browse files
committed
Improves diagnostic errors
Improves code coverage of diagnostics
1 parent b6266e4 commit d62e83b

File tree

8 files changed

+347
-224
lines changed

8 files changed

+347
-224
lines changed

fortls/objects.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from fortls.ftypes import INCLUDE_info, USE_info
3030
from fortls.helper_functions import get_keywords, get_paren_substring, get_var_stack
3131
from fortls.jsonrpc import path_to_uri
32-
from fortls.json_templates import range_json, diagnostic_json
32+
from fortls.json_templates import range_json, diagnostic_json, location_json
3333

3434

3535
def get_use_tree(
@@ -325,10 +325,9 @@ def build(self, file_obj):
325325
if self.has_related:
326326
diag["relatedInformation"] = [
327327
{
328-
"location": {
329-
"uri": path_to_uri(self.related_path),
330-
**range_json(self.related_line, 0, self.related_line, 0),
331-
},
328+
**location_json(
329+
path_to_uri(self.related_path), self.related_line, 0
330+
),
332331
"message": self.related_message,
333332
}
334333
]
@@ -1975,6 +1974,26 @@ def add_doc(self, doc_string: str, forward: bool = False):
19751974
if self.last_obj is not None:
19761975
self.last_obj.add_doc(doc_string)
19771976

1977+
def add_error(self, msg: str, sev: int, ln: int, sch: int, ech: int = None):
1978+
"""Add a Diagnostic error, encountered during parsing, for a range
1979+
in the document.
1980+
1981+
Parameters
1982+
----------
1983+
msg : str
1984+
Error message
1985+
sev : int
1986+
Severity, Error, Warning, Notification
1987+
ln : int
1988+
Line number
1989+
sch : int
1990+
Start character
1991+
ech : int
1992+
End character
1993+
"""
1994+
# Convert from Editor line numbers 1-base index to LSP index which is 0-based
1995+
self.parse_errors.append(diagnostic_json(ln - 1, sch, ln - 1, ech, msg, sev))
1996+
19781997
def start_ppif(self, line_number: int):
19791998
self.pp_if.append([line_number - 1, -1])
19801999

fortls/parse_fortran.py

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
SELECT_TYPE_ID,
2323
SUBMODULE_TYPE_ID,
2424
FRegex,
25+
Severity,
2526
log,
2627
)
2728
from fortls.ftypes import (
@@ -69,7 +70,6 @@
6970
fortran_var,
7071
fortran_where,
7172
)
72-
from fortls.json_templates import diagnostic_json
7373

7474

7575
def get_line_context(line: str) -> tuple[str, None] | tuple[str, str]:
@@ -1156,8 +1156,8 @@ def preprocess(
11561156
def check_file(self, obj_tree, max_line_length=-1, max_comment_line_length=-1):
11571157
diagnostics = []
11581158
if (max_line_length > 0) or (max_comment_line_length > 0):
1159-
line_message = f'Line length exceeds "max_line_length" ({max_line_length})'
1160-
comment_message = (
1159+
msg_line = f'Line length exceeds "max_line_length" ({max_line_length})'
1160+
msg_comment = (
11611161
'Comment line length exceeds "max_comment_line_length"'
11621162
f" ({max_comment_line_length})"
11631163
)
@@ -1169,22 +1169,17 @@ def check_file(self, obj_tree, max_line_length=-1, max_comment_line_length=-1):
11691169
for (i, line) in enumerate(self.contents_split):
11701170
if COMMENT_LINE_MATCH.match(line) is None:
11711171
if 0 < max_line_length < len(line):
1172-
diagnostics.append(
1173-
diagnostic_json(
1174-
i, max_line_length, i, len(line), line_message, 2
1175-
)
1172+
self.ast.add_error(
1173+
msg_line, Severity.warn, i + 1, max_line_length, len(line)
11761174
)
11771175
else:
11781176
if 0 < max_comment_line_length < len(line):
1179-
diagnostics.append(
1180-
diagnostic_json(
1181-
i,
1182-
max_comment_line_length,
1183-
i,
1184-
len(line),
1185-
comment_message,
1186-
2,
1187-
)
1177+
self.ast.add_error(
1178+
msg_comment,
1179+
Severity.warn,
1180+
i + 1,
1181+
max_comment_line_length,
1182+
len(line),
11881183
)
11891184
errors, diags_ast = self.ast.check_file(obj_tree)
11901185
diagnostics += diags_ast
@@ -1646,16 +1641,8 @@ def parse(
16461641

16471642
elif obj_type == "vis":
16481643
if file_ast.current_scope is None:
1649-
file_ast.parse_errors.append(
1650-
diagnostic_json(
1651-
line_number,
1652-
0,
1653-
line_number,
1654-
0,
1655-
"Visibility statement without enclosing scope",
1656-
1,
1657-
)
1658-
)
1644+
msg = "Visibility statement without enclosing scope"
1645+
file_ast.add_error(msg, Severity.error, line_number, 0)
16591646
else:
16601647
if (len(obj_info.obj_names) == 0) and (obj_info.type == 1):
16611648
file_ast.current_scope.set_default_vis(-1)
@@ -1738,37 +1725,32 @@ def _parse_implicit(self, line: str, ln: int, file_ast: fortran_ast):
17381725
match = FRegex.IMPLICIT.match(line)
17391726
if match is None:
17401727
return False
1741-
err_message = None
17421728
if file_ast.current_scope is None:
1743-
err_message = "IMPLICIT statement without enclosing scope"
1729+
msg = "IMPLICIT statement without enclosing scope"
1730+
file_ast.add_error(msg, Severity.error, ln, match.start(1), match.end(1))
17441731
else:
17451732
if match.group(1).lower() == "none":
17461733
file_ast.current_scope.set_implicit(False, ln)
17471734
else:
17481735
file_ast.current_scope.set_implicit(True, ln)
1749-
if err_message:
1750-
file_ast.parse_errors.append(
1751-
diagnostic_json(ln, match.start(1), ln, match.end(1), err_message, 1)
1752-
)
1736+
17531737
self.parser_debug("IMPLICIT", self.line, ln)
17541738
return True
17551739

17561740
def _parse_contains(self, line: str, ln: int, file_ast: fortran_ast):
17571741
match = FRegex.CONTAINS.match(line)
17581742
if match is None:
17591743
return False
1760-
err_message: str = None
1744+
msg: str = None
17611745
try:
17621746
if file_ast.current_scope is None:
1763-
err_message = "CONTAINS statement without enclosing scope"
1747+
msg = "CONTAINS statement without enclosing scope"
17641748
else:
17651749
file_ast.current_scope.mark_contains(ln)
17661750
except ValueError:
1767-
err_message = "Multiple CONTAINS statements in scope"
1768-
if err_message:
1769-
file_ast.parse_errors.append(
1770-
diagnostic_json(ln, match.start(1), ln, match.end(1), err_message, 1)
1771-
)
1751+
msg = "Multiple CONTAINS statements in scope"
1752+
if msg:
1753+
file_ast.add_error(msg, Severity.error, ln, match.start(1), match.end(1))
17721754
self.parser_debug("CONTAINS", self.line, ln)
17731755
return True
17741756

test/test_server.py

Lines changed: 0 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -733,182 +733,3 @@ def hover_request(file_path, line, char):
733733
check_return(results[8], ())
734734
check_return(results[9], ())
735735
check_return(results[10], ((3, " !! Doc 9"), (4, " !! Doc 10")))
736-
737-
738-
def test_diagnostics():
739-
"""
740-
Tests some aspects of diagnostics
741-
"""
742-
743-
def check_return(results, ref_results):
744-
for i, r in enumerate(results):
745-
print(r["diagnostics"], ref_results[i])
746-
assert r["diagnostics"] == ref_results[i]
747-
748-
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
749-
# Test subroutines and functions with interfaces as arguments
750-
file_path = str(test_dir / "test_diagnostic_int.f90")
751-
string += write_rpc_notification(
752-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
753-
)
754-
# Test that use, non_intrinsic does not raise a diagnostic error
755-
file_path = str(test_dir / "test_nonintrinsic.f90")
756-
string += write_rpc_notification(
757-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
758-
)
759-
# Test that submodules with spacings in their parent's names are parsed
760-
file_path = str(test_dir / "test_submodule.f90")
761-
string += write_rpc_notification(
762-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
763-
)
764-
# Tests that variables named end do not close the scope prematurely
765-
file_path = str(test_dir / "diag" / "test_scope_end_name_var.f90")
766-
string += write_rpc_notification(
767-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
768-
)
769-
# Test that externals can be split between multiple lines
770-
# and that diagnostics for multiple definitions of externals can account
771-
# for that
772-
file_path = str(test_dir / "diag" / "test_external.f90")
773-
string += write_rpc_notification(
774-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
775-
)
776-
# Checks that forall with end forall inside a case select does not cause
777-
# unexpected end of scope.
778-
file_path = str(test_dir / "diag" / "test_forall.f90")
779-
string += write_rpc_notification(
780-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
781-
)
782-
# Test USE directive ordering errors
783-
file_path = str(test_dir / "diag" / "test_use_ordering.f90")
784-
string += write_rpc_notification(
785-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
786-
)
787-
# Test where blocks
788-
file_path = str(test_dir / "diag" / "test_where.f90")
789-
string += write_rpc_notification(
790-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
791-
)
792-
# Test where semicolon (multi-line)
793-
file_path = str(test_dir / "diag" / "test_semicolon.f90")
794-
string += write_rpc_notification(
795-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
796-
)
797-
# Test ENUM block
798-
file_path = str(test_dir / "diag" / "test_enum.f90")
799-
string += write_rpc_notification(
800-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
801-
)
802-
# Test module procedure in submodules importing scopes
803-
file_path = str(test_dir / "subdir" / "test_submod.F90")
804-
string += write_rpc_notification(
805-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
806-
)
807-
errcode, results = run_request(string)
808-
assert errcode == 0
809-
810-
# Load a different config file
811-
# Test long lines
812-
root = str(test_dir / "diag")
813-
string = write_rpc_request(1, "initialize", {"rootPath": root})
814-
file_path = str(test_dir / "diag" / "test_lines.f90")
815-
string += write_rpc_notification(
816-
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
817-
)
818-
file_path = str(test_dir / "diag" / "conf_long_lines.json")
819-
errcode, res = run_request(string, [f"--config {file_path}"])
820-
assert errcode == 0
821-
results.extend(res[1:])
822-
823-
root = path_to_uri(str((test_dir / "diag" / "test_external.f90").resolve()))
824-
ref_results = [
825-
[],
826-
[],
827-
[],
828-
[],
829-
[
830-
{
831-
"range": {
832-
"start": {"line": 7, "character": 17},
833-
"end": {"line": 7, "character": 22},
834-
},
835-
"message": 'Variable "VAR_B" declared twice in scope',
836-
"severity": 1,
837-
"relatedInformation": [
838-
{
839-
"location": {
840-
"uri": str(root),
841-
"range": {
842-
"start": {"line": 5, "character": 0},
843-
"end": {"line": 5, "character": 0},
844-
},
845-
},
846-
"message": "First declaration",
847-
}
848-
],
849-
},
850-
{
851-
"range": {
852-
"start": {"line": 8, "character": 17},
853-
"end": {"line": 8, "character": 22},
854-
},
855-
"message": 'Variable "VAR_A" declared twice in scope',
856-
"severity": 1,
857-
"relatedInformation": [
858-
{
859-
"location": {
860-
"uri": str(root),
861-
"range": {
862-
"start": {"line": 3, "character": 0},
863-
"end": {"line": 3, "character": 0},
864-
},
865-
},
866-
"message": "First declaration",
867-
}
868-
],
869-
},
870-
],
871-
[],
872-
[],
873-
[],
874-
[],
875-
[],
876-
[],
877-
[
878-
{
879-
"range": {
880-
"start": {"line": 2, "character": 100},
881-
"end": {"line": 2, "character": 155},
882-
},
883-
"message": 'Line length exceeds "max_line_length" (100)',
884-
"severity": 2,
885-
},
886-
{
887-
"range": {
888-
"start": {"line": 3, "character": 100},
889-
"end": {"line": 3, "character": 127},
890-
},
891-
"message": (
892-
'Comment line length exceeds "max_comment_line_length" (100)'
893-
),
894-
"severity": 2,
895-
},
896-
],
897-
]
898-
check_return(results[1:], ref_results)
899-
900-
901-
if __name__ == "__main__":
902-
test_init()
903-
test_logger()
904-
test_open()
905-
test_change()
906-
test_symbols()
907-
test_workspace_symbols()
908-
test_comp()
909-
test_sig()
910-
test_def()
911-
test_refs()
912-
test_hover()
913-
test_docs()
914-
test_diagnostics()

0 commit comments

Comments
 (0)