-
Notifications
You must be signed in to change notification settings - Fork 285
/
Copy pathpylint_lint.py
175 lines (151 loc) · 5.91 KB
/
pylint_lint.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# Copyright 2018 Google LLC.
"""Linter plugin for pylint."""
import collections
import json
import logging
import sys
from pylint.epylint import py_run
from pyls import hookimpl, lsp
log = logging.getLogger(__name__)
ARGS = { # 'argument_name': 'name_under_plugin_conf'
'disable': 'disable',
'ignore': 'ignore',
'max-line-length': 'maxLineLength',
}
class PylintLinter(object):
last_diags = collections.defaultdict(list)
@classmethod
def lint(cls, document, is_saved, flags=''):
"""Plugin interface to pyls linter.
Args:
document: The document to be linted.
is_saved: Whether or not the file has been saved to disk.
flags: Additional flags to pass to pylint. Not exposed to
pyls_lint, but used for testing.
Returns:
A list of dicts with the following format:
{
'source': 'pylint',
'range': {
'start': {
'line': start_line,
'character': start_column,
},
'end': {
'line': end_line,
'character': end_column,
},
}
'message': msg,
'severity': lsp.DiagnosticSeverity.*,
}
"""
if not is_saved:
# Pylint can only be run on files that have been saved to disk.
# Rather than return nothing, return the previous list of
# diagnostics. If we return an empty list, any diagnostics we'd
# previously shown will be cleared until the next save. Instead,
# continue showing (possibly stale) diagnostics until the next
# save.
return cls.last_diags[document.path]
# py_run will call shlex.split on its arguments, and shlex.split does
# not handle Windows paths (it will try to perform escaping). Turn
# backslashes into forward slashes first to avoid this issue.
path = document.path
if sys.platform.startswith('win'):
path = path.replace('\\', '/')
pylint_call = '{} -f json {}'.format(path, flags)
log.debug("Calling pylint with '%s'", pylint_call)
json_out, err = py_run(pylint_call, return_std=True)
# Get strings
json_out = json_out.getvalue()
err = err.getvalue()
if err != '':
log.error("Error calling pylint: '%s'", err)
# pylint prints nothing rather than [] when there are no diagnostics.
# json.loads will not parse an empty string, so just return.
if not json_out.strip():
cls.last_diags[document.path] = []
return []
# Pylint's JSON output is a list of objects with the following format.
#
# {
# "obj": "main",
# "path": "foo.py",
# "message": "Missing function docstring",
# "message-id": "C0111",
# "symbol": "missing-docstring",
# "column": 0,
# "type": "convention",
# "line": 5,
# "module": "foo"
# }
#
# The type can be any of:
#
# * convention
# * error
# * fatal
# * refactor
# * warning
diagnostics = []
for diag in json.loads(json_out):
# pylint lines index from 1, pyls lines index from 0
line = diag['line'] - 1
err_range = {
'start': {
'line': line,
# Index columns start from 0
'character': diag['column'],
},
'end': {
'line': line,
# It's possible that we're linting an empty file. Even an empty
# file might fail linting if it isn't named properly.
'character': len(document.lines[line]) if document.lines else 0,
},
}
if diag['type'] == 'convention':
severity = lsp.DiagnosticSeverity.Information
elif diag['type'] == 'error':
severity = lsp.DiagnosticSeverity.Error
elif diag['type'] == 'fatal':
severity = lsp.DiagnosticSeverity.Error
elif diag['type'] == 'refactor':
severity = lsp.DiagnosticSeverity.Hint
elif diag['type'] == 'warning':
severity = lsp.DiagnosticSeverity.Warning
diagnostics.append({
'source': 'pylint',
'range': err_range,
'message': '[{}] {}'.format(diag['symbol'], diag['message']),
'severity': severity,
'code': diag['message-id']
})
cls.last_diags[document.path] = diagnostics
return diagnostics
def _build_pylint_flags(settings):
"""Build arguments for calling pylint.
If args is found then it's the arguments used, otherwise,
we build arguments from the plugin config.
"""
pylint_args = settings.get('args')
if pylint_args is None:
# Build args from plugin config
pylint_args = list()
for arg_name in ARGS:
arg_val = settings.get(ARGS[arg_name])
arg = None
if isinstance(arg_val, list):
arg = '--{}={}'.format(arg_name, ','.join(arg_val))
elif isinstance(arg_val, int):
arg = '--{}={}'.format(arg_name, arg_val)
if arg:
pylint_args.append(arg)
return ' '.join(pylint_args)
@hookimpl
def pyls_lint(config, document, is_saved):
settings = config.plugin_settings('pylint')
log.debug("Got pylint settings: %s", settings)
flags = _build_pylint_flags(settings)
return PylintLinter.lint(document, is_saved, flags=flags)