Skip to content

Commit 562cbba

Browse files
committed
Add error end and diagnostic code functionality
- Change pattern so that it only accepts diagnostics with full details - Rewrite `parse_line` - Remove redundant tests
1 parent c9063c4 commit 562cbba

File tree

2 files changed

+56
-80
lines changed

2 files changed

+56
-80
lines changed

pylsp_mypy/plugin.py

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
from pylsp.config.config import Config
3131
from pylsp.workspace import Document, Workspace
3232

33-
line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)?(?:(\d+):)?(?:(\d+):)? (\w+): (.*)"
33+
line_pattern = re.compile(
34+
r"^(?P<file>.+):(?P<start_line>\d+):(?P<start_col>\d*):(?P<end_line>\d*):(?P<end_col>\d*): (?P<severity>\w+): (?P<message>.+?)(?: +\[(?P<code>.+)\])?$"
35+
)
3436

3537
log = logging.getLogger(__name__)
3638

@@ -77,40 +79,38 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
7779
The dict with the lint data.
7880
7981
"""
80-
result = re.match(line_pattern, line)
81-
if result:
82-
file_path, linenoStr, offsetStr, endlinenoStr, endoffsetStr, severity, msg = result.groups()
83-
84-
if file_path != "<string>": # live mode
85-
# results from other files can be included, but we cannot return
86-
# them.
87-
if document and document.path and not document.path.endswith(file_path):
88-
log.warning("discarding result for %s against %s", file_path, document.path)
89-
return None
90-
91-
lineno = int(linenoStr or 1) - 1 # 0-based line number
92-
offset = int(offsetStr or 1) - 1 # 0-based offset
93-
end_lineno = (int(endlinenoStr) - 1) if endlinenoStr else None
94-
end_offset = (int(endoffsetStr)) if endoffsetStr else None
95-
errno = 2
96-
if severity == "error":
97-
errno = 1
98-
diag: Dict[str, Any] = {
99-
"source": "mypy",
100-
"range": {
101-
"start": {"line": lineno, "character": offset},
102-
"end": {"line": end_lineno or lineno, "character": end_offset},
103-
},
104-
"message": msg,
105-
"severity": errno,
106-
}
107-
if diag["range"]["end"]["character"] is None:
108-
if document:
109-
word = document.word_at_position(diag["range"]["start"])
110-
diag["range"]["end"]["character"] = offset + len(word) if word else offset + 1
111-
112-
return diag
113-
return None
82+
result = line_pattern.match(line)
83+
if not result:
84+
return None
85+
86+
file_path = result["file"]
87+
if file_path != "<string>": # live mode
88+
# results from other files can be included, but we cannot return
89+
# them.
90+
if document and document.path and not document.path.endswith(file_path):
91+
log.warning("discarding result for %s against %s", file_path, document.path)
92+
return None
93+
94+
lineno = int(result["start_line"]) - 1 # 0-based line number
95+
offset = int(result["start_col"]) - 1 # 0-based offset
96+
end_lineno = int(result["end_line"]) - 1
97+
end_offset = int(result["end_col"]) # end is exclusive
98+
99+
severity = result["severity"]
100+
if severity not in ("error", "note"):
101+
log.warning(f"invalid error severity '{severity}'")
102+
errno = 1 if severity == "error" else 3
103+
104+
return {
105+
"source": "mypy",
106+
"range": {
107+
"start": {"line": lineno, "character": offset},
108+
"end": {"line": end_lineno, "character": end_offset},
109+
},
110+
"message": result["message"],
111+
"severity": errno,
112+
"code": result["code"],
113+
}
114114

115115

116116
def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]:
@@ -228,7 +228,7 @@ def get_diagnostics(
228228
if dmypy:
229229
dmypy_status_file = settings.get("dmypy_status_file", ".dmypy.json")
230230

231-
args = ["--show-error-end"]
231+
args = ["--show-error-end", "--no-error-summary"]
232232

233233
global tmpFile
234234
if live_mode and not is_saved:

test/test_plugin.py

Lines changed: 20 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import collections
22
import os
33
import subprocess
4+
import sys
45
from pathlib import Path
56
from typing import Dict
67
from unittest.mock import Mock
@@ -16,12 +17,9 @@
1617
DOC_URI = f"file:/{Path(__file__)}"
1718
DOC_TYPE_ERR = """{}.append(3)
1819
"""
19-
TYPE_ERR_MSG = '"Dict[<nothing>, <nothing>]" has no attribute "append" [attr-defined]'
20+
TYPE_ERR_MSG = '"Dict[<nothing>, <nothing>]" has no attribute "append"'
2021

21-
TEST_LINE = 'test_plugin.py:279:8:279:19: error: "Request" has no attribute "id"'
22-
TEST_LINE_WITHOUT_END = 'test_plugin.py:279:8: error: "Request" has no attribute "id"'
23-
TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"'
24-
TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"'
22+
TEST_LINE = 'test_plugin.py:279:8:279:16: error: "Request" has no attribute "id" [attr-defined]'
2523

2624
windows_flag: Dict[str, int] = (
2725
{"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore
@@ -66,48 +64,22 @@ def test_plugin(workspace, last_diagnostics_monkeypatch):
6664
diag = diags[0]
6765
assert diag["message"] == TYPE_ERR_MSG
6866
assert diag["range"]["start"] == {"line": 0, "character": 0}
69-
assert diag["range"]["end"] == {"line": 0, "character": 9}
67+
# Running mypy in 3.7 produces wrong error ends this can be removed when 3.7 reaches EOL
68+
if sys.version_info < (3, 8):
69+
assert diag["range"]["end"] == {"line": 0, "character": 1}
70+
else:
71+
assert diag["range"]["end"] == {"line": 0, "character": 9}
72+
assert diag["severity"] == 1
73+
assert diag["code"] == "attr-defined"
7074

7175

7276
def test_parse_full_line(workspace):
7377
diag = plugin.parse_line(TEST_LINE) # TODO parse a document here
7478
assert diag["message"] == '"Request" has no attribute "id"'
7579
assert diag["range"]["start"] == {"line": 278, "character": 7}
76-
assert diag["range"]["end"] == {"line": 278, "character": 19}
77-
78-
79-
def test_parse_line_without_end(workspace):
80-
doc = Document(DOC_URI, workspace)
81-
diag = plugin.parse_line(TEST_LINE_WITHOUT_END, doc)
82-
assert diag["message"] == '"Request" has no attribute "id"'
83-
assert diag["range"]["start"] == {"line": 278, "character": 7}
84-
assert diag["range"]["end"] == {"line": 278, "character": 13}
85-
86-
87-
def test_parse_line_without_col(workspace):
88-
doc = Document(DOC_URI, workspace)
89-
diag = plugin.parse_line(TEST_LINE_WITHOUT_COL, doc)
90-
assert diag["message"] == '"Request" has no attribute "id"'
91-
assert diag["range"]["start"] == {"line": 278, "character": 0}
92-
assert diag["range"]["end"] == {"line": 278, "character": 1}
93-
94-
95-
def test_parse_line_without_line(workspace):
96-
doc = Document(DOC_URI, workspace)
97-
diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc)
98-
assert diag["message"] == '"Request" has no attribute "id"'
99-
assert diag["range"]["start"] == {"line": 0, "character": 0}
100-
assert diag["range"]["end"] == {"line": 0, "character": 6}
101-
102-
103-
@pytest.mark.parametrize("word,bounds", [("", (7, 8)), ("my_var", (7, 13))])
104-
def test_parse_line_with_context(monkeypatch, word, bounds, workspace):
105-
doc = Document(DOC_URI, workspace)
106-
monkeypatch.setattr(Document, "word_at_position", lambda *args: word)
107-
diag = plugin.parse_line(TEST_LINE_WITHOUT_END, doc)
108-
assert diag["message"] == '"Request" has no attribute "id"'
109-
assert diag["range"]["start"] == {"line": 278, "character": bounds[0]}
110-
assert diag["range"]["end"] == {"line": 278, "character": bounds[1]}
80+
assert diag["range"]["end"] == {"line": 278, "character": 16}
81+
assert diag["severity"] == 1
82+
assert diag["code"] == "attr-defined"
11183

11284

11385
def test_multiple_workspaces(tmpdir, last_diagnostics_monkeypatch):
@@ -116,7 +88,7 @@ def foo():
11688
return
11789
unreachable = 1
11890
"""
119-
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
91+
DOC_ERR_MSG = "Statement is unreachable"
12092

12193
# Initialize two workspace folders.
12294
folder1 = tmpdir.mkdir("folder1")
@@ -141,6 +113,7 @@ def foo():
141113
assert len(diags) == 1
142114
diag = diags[0]
143115
assert diag["message"] == DOC_ERR_MSG
116+
assert diag["code"] == "unreachable"
144117

145118
# Test document in workspace 2 (without mypy.ini configuration)
146119
doc2 = Document(DOC_URI, ws2, DOC_SOURCE)
@@ -236,6 +209,7 @@ def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace):
236209
"--python-executable",
237210
"/tmp/fake",
238211
"--show-error-end",
212+
"--no-error-summary",
239213
document.path,
240214
]
241215
m.assert_called_with(expected, capture_output=True, **windows_flag, encoding="utf-8")
@@ -279,7 +253,7 @@ def foo():
279253
return
280254
unreachable = 1
281255
"""
282-
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
256+
DOC_ERR_MSG = "Statement is unreachable"
283257

284258
config_sub_paths = [".config"]
285259

@@ -305,6 +279,7 @@ def foo():
305279
assert len(diags) == 1
306280
diag = diags[0]
307281
assert diag["message"] == DOC_ERR_MSG
282+
assert diag["code"] == "unreachable"
308283

309284

310285
def test_config_sub_paths_config_changed(tmpdir, last_diagnostics_monkeypatch):
@@ -313,7 +288,7 @@ def foo():
313288
return
314289
unreachable = 1
315290
"""
316-
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
291+
DOC_ERR_MSG = "Statement is unreachable"
317292

318293
# Create configuration file for workspace.
319294
config_dir = tmpdir.mkdir(".config")
@@ -336,3 +311,4 @@ def foo():
336311
assert len(diags) == 1
337312
diag = diags[0]
338313
assert diag["message"] == DOC_ERR_MSG
314+
assert diag["code"] == "unreachable"

0 commit comments

Comments
 (0)