Skip to content

Commit d3d69df

Browse files
authored
Merge pull request #62 from AntonVucinic/feature/error-end-location
Add error end functionality
2 parents 6fd72c3 + 8fee724 commit d3d69df

File tree

4 files changed

+72
-73
lines changed

4 files changed

+72
-73
lines changed

pylsp_mypy/plugin.py

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

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

3540
log = logging.getLogger(__name__)
3641

@@ -77,41 +82,38 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
7782
The dict with the lint data.
7883
7984
"""
80-
result = re.match(line_pattern, line)
81-
if result:
82-
file_path, linenoStr, offsetStr, 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-
errno = 2
94-
if severity == "error":
95-
errno = 1
96-
diag: Dict[str, Any] = {
97-
"source": "mypy",
98-
"range": {
99-
"start": {"line": lineno, "character": offset},
100-
# There may be a better solution, but mypy does not provide end
101-
"end": {"line": lineno, "character": offset + 1},
102-
},
103-
"message": msg,
104-
"severity": errno,
105-
}
106-
if document:
107-
# although mypy does not provide the end of the affected range, we
108-
# can make a good guess by highlighting the word that Mypy flagged
109-
word = document.word_at_position(diag["range"]["start"])
110-
if word:
111-
diag["range"]["end"]["character"] = diag["range"]["start"]["character"] + len(word)
112-
113-
return diag
114-
return None
85+
result = line_pattern.match(line)
86+
if not result:
87+
return None
88+
89+
file_path = result["file"]
90+
if file_path != "<string>": # live mode
91+
# results from other files can be included, but we cannot return
92+
# them.
93+
if document and document.path and not document.path.endswith(file_path):
94+
log.warning("discarding result for %s against %s", file_path, document.path)
95+
return None
96+
97+
lineno = int(result["start_line"]) - 1 # 0-based line number
98+
offset = int(result["start_col"]) - 1 # 0-based offset
99+
end_lineno = int(result["end_line"]) - 1
100+
end_offset = int(result["end_col"]) # end is exclusive
101+
102+
severity = result["severity"]
103+
if severity not in ("error", "note"):
104+
log.warning(f"invalid error severity '{severity}'")
105+
errno = 1 if severity == "error" else 3
106+
107+
return {
108+
"source": "mypy",
109+
"range": {
110+
"start": {"line": lineno, "character": offset},
111+
"end": {"line": end_lineno, "character": end_offset},
112+
},
113+
"message": result["message"],
114+
"severity": errno,
115+
"code": result["code"],
116+
}
115117

116118

117119
def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]:
@@ -229,7 +231,7 @@ def get_diagnostics(
229231
if dmypy:
230232
dmypy_status_file = settings.get("dmypy_status_file", ".dmypy.json")
231233

232-
args = ["--show-column-numbers"]
234+
args = ["--show-error-end", "--no-error-summary"]
233235

234236
global tmpFile
235237
if live_mode and not is_saved:

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
python-lsp-server
2-
mypy
2+
mypy >= 0.981
33
tomli >= 1.1.0 ; python_version < "3.11"
44
black
55
pre-commit

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ python_requires = >= 3.7
2121
packages = find:
2222
install_requires =
2323
python-lsp-server >=1.7.0
24-
mypy
24+
mypy >= 0.981
2525
tomli >= 1.1.0 ; python_version < "3.11"
2626

2727
[flake8]

test/test_plugin.py

Lines changed: 31 additions & 34 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,11 +17,12 @@
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: error: "Request" has no attribute "id"'
22-
TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"'
23-
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]'
23+
TEST_LINE_NOTE = (
24+
'test_plugin.py:124:1:129:77: note: Use "-> None" if function does not return a value'
25+
)
2426

2527
windows_flag: Dict[str, int] = (
2628
{"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore
@@ -65,40 +67,31 @@ def test_plugin(workspace, last_diagnostics_monkeypatch):
6567
diag = diags[0]
6668
assert diag["message"] == TYPE_ERR_MSG
6769
assert diag["range"]["start"] == {"line": 0, "character": 0}
68-
assert diag["range"]["end"] == {"line": 0, "character": 1}
70+
# Running mypy in 3.7 produces wrong error ends this can be removed when 3.7 reaches EOL
71+
if sys.version_info < (3, 8):
72+
assert diag["range"]["end"] == {"line": 0, "character": 1}
73+
else:
74+
assert diag["range"]["end"] == {"line": 0, "character": 9}
75+
assert diag["severity"] == 1
76+
assert diag["code"] == "attr-defined"
6977

7078

7179
def test_parse_full_line(workspace):
7280
diag = plugin.parse_line(TEST_LINE) # TODO parse a document here
7381
assert diag["message"] == '"Request" has no attribute "id"'
7482
assert diag["range"]["start"] == {"line": 278, "character": 7}
75-
assert diag["range"]["end"] == {"line": 278, "character": 8}
76-
83+
assert diag["range"]["end"] == {"line": 278, "character": 16}
84+
assert diag["severity"] == 1
85+
assert diag["code"] == "attr-defined"
7786

78-
def test_parse_line_without_col(workspace):
79-
doc = Document(DOC_URI, workspace)
80-
diag = plugin.parse_line(TEST_LINE_WITHOUT_COL, doc)
81-
assert diag["message"] == '"Request" has no attribute "id"'
82-
assert diag["range"]["start"] == {"line": 278, "character": 0}
83-
assert diag["range"]["end"] == {"line": 278, "character": 1}
8487

85-
86-
def test_parse_line_without_line(workspace):
87-
doc = Document(DOC_URI, workspace)
88-
diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc)
89-
assert diag["message"] == '"Request" has no attribute "id"'
90-
assert diag["range"]["start"] == {"line": 0, "character": 0}
91-
assert diag["range"]["end"] == {"line": 0, "character": 6}
92-
93-
94-
@pytest.mark.parametrize("word,bounds", [("", (7, 8)), ("my_var", (7, 13))])
95-
def test_parse_line_with_context(monkeypatch, word, bounds, workspace):
96-
doc = Document(DOC_URI, workspace)
97-
monkeypatch.setattr(Document, "word_at_position", lambda *args: word)
98-
diag = plugin.parse_line(TEST_LINE, doc)
99-
assert diag["message"] == '"Request" has no attribute "id"'
100-
assert diag["range"]["start"] == {"line": 278, "character": bounds[0]}
101-
assert diag["range"]["end"] == {"line": 278, "character": bounds[1]}
88+
def test_parse_note_line(workspace):
89+
diag = plugin.parse_line(TEST_LINE_NOTE)
90+
assert diag["message"] == 'Use "-> None" if function does not return a value'
91+
assert diag["range"]["start"] == {"line": 123, "character": 0}
92+
assert diag["range"]["end"] == {"line": 128, "character": 77}
93+
assert diag["severity"] == 3
94+
assert diag["code"] is None
10295

10396

10497
def test_multiple_workspaces(tmpdir, last_diagnostics_monkeypatch):
@@ -107,7 +100,7 @@ def foo():
107100
return
108101
unreachable = 1
109102
"""
110-
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
103+
DOC_ERR_MSG = "Statement is unreachable"
111104

112105
# Initialize two workspace folders.
113106
folder1 = tmpdir.mkdir("folder1")
@@ -132,6 +125,7 @@ def foo():
132125
assert len(diags) == 1
133126
diag = diags[0]
134127
assert diag["message"] == DOC_ERR_MSG
128+
assert diag["code"] == "unreachable"
135129

136130
# Test document in workspace 2 (without mypy.ini configuration)
137131
doc2 = Document(DOC_URI, ws2, DOC_SOURCE)
@@ -226,7 +220,8 @@ def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace):
226220
"--",
227221
"--python-executable",
228222
"/tmp/fake",
229-
"--show-column-numbers",
223+
"--show-error-end",
224+
"--no-error-summary",
230225
document.path,
231226
]
232227
m.assert_called_with(expected, capture_output=True, **windows_flag, encoding="utf-8")
@@ -270,7 +265,7 @@ def foo():
270265
return
271266
unreachable = 1
272267
"""
273-
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
268+
DOC_ERR_MSG = "Statement is unreachable"
274269

275270
config_sub_paths = [".config"]
276271

@@ -296,6 +291,7 @@ def foo():
296291
assert len(diags) == 1
297292
diag = diags[0]
298293
assert diag["message"] == DOC_ERR_MSG
294+
assert diag["code"] == "unreachable"
299295

300296

301297
def test_config_sub_paths_config_changed(tmpdir, last_diagnostics_monkeypatch):
@@ -304,7 +300,7 @@ def foo():
304300
return
305301
unreachable = 1
306302
"""
307-
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
303+
DOC_ERR_MSG = "Statement is unreachable"
308304

309305
# Create configuration file for workspace.
310306
config_dir = tmpdir.mkdir(".config")
@@ -327,3 +323,4 @@ def foo():
327323
assert len(diags) == 1
328324
diag = diags[0]
329325
assert diag["message"] == DOC_ERR_MSG
326+
assert diag["code"] == "unreachable"

0 commit comments

Comments
 (0)