Skip to content

Add error end functionality #62

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

Merged
Merged
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
76 changes: 39 additions & 37 deletions pylsp_mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
from pylsp.config.config import Config
from pylsp.workspace import Document, Workspace

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

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -77,41 +82,38 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[
The dict with the lint data.

"""
result = re.match(line_pattern, line)
if result:
file_path, linenoStr, offsetStr, severity, msg = result.groups()

if file_path != "<string>": # live mode
# results from other files can be included, but we cannot return
# them.
if document and document.path and not document.path.endswith(file_path):
log.warning("discarding result for %s against %s", file_path, document.path)
return None

lineno = int(linenoStr or 1) - 1 # 0-based line number
offset = int(offsetStr or 1) - 1 # 0-based offset
errno = 2
if severity == "error":
errno = 1
diag: Dict[str, Any] = {
"source": "mypy",
"range": {
"start": {"line": lineno, "character": offset},
# There may be a better solution, but mypy does not provide end
"end": {"line": lineno, "character": offset + 1},
},
"message": msg,
"severity": errno,
}
if document:
# although mypy does not provide the end of the affected range, we
# can make a good guess by highlighting the word that Mypy flagged
word = document.word_at_position(diag["range"]["start"])
if word:
diag["range"]["end"]["character"] = diag["range"]["start"]["character"] + len(word)

return diag
return None
result = line_pattern.match(line)
if not result:
return None

file_path = result["file"]
if file_path != "<string>": # live mode
# results from other files can be included, but we cannot return
# them.
if document and document.path and not document.path.endswith(file_path):
log.warning("discarding result for %s against %s", file_path, document.path)
return None

lineno = int(result["start_line"]) - 1 # 0-based line number
offset = int(result["start_col"]) - 1 # 0-based offset
end_lineno = int(result["end_line"]) - 1
end_offset = int(result["end_col"]) # end is exclusive

severity = result["severity"]
if severity not in ("error", "note"):
log.warning(f"invalid error severity '{severity}'")
errno = 1 if severity == "error" else 3

return {
"source": "mypy",
"range": {
"start": {"line": lineno, "character": offset},
"end": {"line": end_lineno, "character": end_offset},
},
"message": result["message"],
"severity": errno,
"code": result["code"],
}


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

args = ["--show-column-numbers"]
args = ["--show-error-end", "--no-error-summary"]

global tmpFile
if live_mode and not is_saved:
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
python-lsp-server
mypy
mypy >= 0.981
tomli >= 1.1.0 ; python_version < "3.11"
black
pre-commit
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ python_requires = >= 3.7
packages = find:
install_requires =
python-lsp-server >=1.7.0
mypy
mypy >= 0.981
tomli >= 1.1.0 ; python_version < "3.11"

[flake8]
Expand Down
65 changes: 31 additions & 34 deletions test/test_plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import collections
import os
import subprocess
import sys
from pathlib import Path
from typing import Dict
from unittest.mock import Mock
Expand All @@ -16,11 +17,12 @@
DOC_URI = f"file:/{Path(__file__)}"
DOC_TYPE_ERR = """{}.append(3)
"""
TYPE_ERR_MSG = '"Dict[<nothing>, <nothing>]" has no attribute "append" [attr-defined]'
TYPE_ERR_MSG = '"Dict[<nothing>, <nothing>]" has no attribute "append"'

TEST_LINE = 'test_plugin.py:279:8: error: "Request" has no attribute "id"'
TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"'
TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"'
TEST_LINE = 'test_plugin.py:279:8:279:16: error: "Request" has no attribute "id" [attr-defined]'
TEST_LINE_NOTE = (
'test_plugin.py:124:1:129:77: note: Use "-> None" if function does not return a value'
)

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


def test_parse_full_line(workspace):
diag = plugin.parse_line(TEST_LINE) # TODO parse a document here
assert diag["message"] == '"Request" has no attribute "id"'
assert diag["range"]["start"] == {"line": 278, "character": 7}
assert diag["range"]["end"] == {"line": 278, "character": 8}

assert diag["range"]["end"] == {"line": 278, "character": 16}
assert diag["severity"] == 1
assert diag["code"] == "attr-defined"

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


def test_parse_line_without_line(workspace):
doc = Document(DOC_URI, workspace)
diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc)
assert diag["message"] == '"Request" has no attribute "id"'
assert diag["range"]["start"] == {"line": 0, "character": 0}
assert diag["range"]["end"] == {"line": 0, "character": 6}


@pytest.mark.parametrize("word,bounds", [("", (7, 8)), ("my_var", (7, 13))])
def test_parse_line_with_context(monkeypatch, word, bounds, workspace):
doc = Document(DOC_URI, workspace)
monkeypatch.setattr(Document, "word_at_position", lambda *args: word)
diag = plugin.parse_line(TEST_LINE, doc)
assert diag["message"] == '"Request" has no attribute "id"'
assert diag["range"]["start"] == {"line": 278, "character": bounds[0]}
assert diag["range"]["end"] == {"line": 278, "character": bounds[1]}
def test_parse_note_line(workspace):
diag = plugin.parse_line(TEST_LINE_NOTE)
assert diag["message"] == 'Use "-> None" if function does not return a value'
assert diag["range"]["start"] == {"line": 123, "character": 0}
assert diag["range"]["end"] == {"line": 128, "character": 77}
assert diag["severity"] == 3
assert diag["code"] is None


def test_multiple_workspaces(tmpdir, last_diagnostics_monkeypatch):
Expand All @@ -107,7 +100,7 @@ def foo():
return
unreachable = 1
"""
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
DOC_ERR_MSG = "Statement is unreachable"

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

# Test document in workspace 2 (without mypy.ini configuration)
doc2 = Document(DOC_URI, ws2, DOC_SOURCE)
Expand Down Expand Up @@ -226,7 +220,8 @@ def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace):
"--",
"--python-executable",
"/tmp/fake",
"--show-column-numbers",
"--show-error-end",
"--no-error-summary",
document.path,
]
m.assert_called_with(expected, capture_output=True, **windows_flag, encoding="utf-8")
Expand Down Expand Up @@ -270,7 +265,7 @@ def foo():
return
unreachable = 1
"""
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
DOC_ERR_MSG = "Statement is unreachable"

config_sub_paths = [".config"]

Expand All @@ -296,6 +291,7 @@ def foo():
assert len(diags) == 1
diag = diags[0]
assert diag["message"] == DOC_ERR_MSG
assert diag["code"] == "unreachable"


def test_config_sub_paths_config_changed(tmpdir, last_diagnostics_monkeypatch):
Expand All @@ -304,7 +300,7 @@ def foo():
return
unreachable = 1
"""
DOC_ERR_MSG = "Statement is unreachable [unreachable]"
DOC_ERR_MSG = "Statement is unreachable"

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