Skip to content

Commit fecffb2

Browse files
youben11gatesn
authored andcommitted
Flake8 plugin for linting (#656)
1 parent e6421d9 commit fecffb2

File tree

4 files changed

+161
-0
lines changed

4 files changed

+161
-0
lines changed

pyls/config/flake8_conf.py

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
('ignore', 'plugins.pycodestyle.ignore', list),
2020
('max-line-length', 'plugins.pycodestyle.maxLineLength', int),
2121
('select', 'plugins.pycodestyle.select', list),
22+
# flake8
23+
('exclude', 'plugins.flake8.exclude', list),
24+
('filename', 'plugins.flake8.filename', list),
25+
('hang-closing', 'plugins.flake8.hangClosing', bool),
26+
('ignore', 'plugins.flake8.ignore', list),
27+
('max-line-length', 'plugins.flake8.maxLineLength', int),
28+
('select', 'plugins.flake8.select', list),
2229
]
2330

2431

pyls/plugins/flake8_lint.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright 2019 Palantir Technologies, Inc.
2+
"""Linter pluging for flake8"""
3+
import logging
4+
from flake8.api import legacy as flake8
5+
from pyls import hookimpl, lsp
6+
7+
log = logging.getLogger(__name__)
8+
9+
10+
@hookimpl
11+
def pyls_settings():
12+
# Default flake8 to disabled
13+
return {'plugins': {'flake8': {'enabled': False}}}
14+
15+
16+
@hookimpl
17+
def pyls_lint(config, document):
18+
settings = config.plugin_settings('flake8')
19+
log.debug("Got flake8 settings: %s", settings)
20+
21+
opts = {
22+
'exclude': settings.get('exclude'),
23+
'filename': settings.get('filename'),
24+
'hang_closing': settings.get('hangClosing'),
25+
'ignore': settings.get('ignore'),
26+
'max_line_length': settings.get('maxLineLength'),
27+
'select': settings.get('select'),
28+
}
29+
30+
# Build the flake8 checker and use it to generate a report from the document
31+
kwargs = {k: v for k, v in opts.items() if v}
32+
style_guide = flake8.get_style_guide(quiet=4, verbose=0, **kwargs)
33+
report = style_guide.check_files([document.path])
34+
35+
return parse_report(document, report)
36+
37+
38+
def parse_report(document, report):
39+
"""
40+
Build a diagnostics from a report, it should extract every result and format
41+
it into a dict that looks like this:
42+
{
43+
'source': 'flake8',
44+
'code': code, # 'E501'
45+
'range': {
46+
'start': {
47+
'line': start_line,
48+
'character': start_column,
49+
},
50+
'end': {
51+
'line': end_line,
52+
'character': end_column,
53+
},
54+
},
55+
'message': msg,
56+
'severity': lsp.DiagnosticSeverity.*,
57+
}
58+
59+
Args:
60+
document: The document to be linted.
61+
report: A Report object returned by checking the document.
62+
Returns:
63+
A list of dictionaries.
64+
"""
65+
66+
file_checkers = report._application.file_checker_manager.checkers
67+
# No file have been checked
68+
if not file_checkers:
69+
return []
70+
# There should be only a filechecker since we are parsing using a path and not a pattern
71+
if len(file_checkers) > 1:
72+
log.error("Flake8 parsed more than a file for '%s'", document.path)
73+
74+
diagnostics = []
75+
file_checker = file_checkers[0]
76+
for error in file_checker.results:
77+
code, line, character, msg, physical_line = error
78+
diagnostics.append(
79+
{
80+
'source': 'flake8',
81+
'code': code,
82+
'range': {
83+
'start': {
84+
'line': line,
85+
'character': character
86+
},
87+
'end': {
88+
'line': line,
89+
# no way to determine the column
90+
'character': len(physical_line)
91+
}
92+
},
93+
'message': msg,
94+
# no way to determine the severity using the legacy api
95+
'severity': lsp.DiagnosticSeverity.Warning,
96+
}
97+
)
98+
99+
return diagnostics

setup.py

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
extras_require={
4949
'all': [
5050
'autopep8',
51+
'flake8',
5152
'mccabe',
5253
'pycodestyle',
5354
'pydocstyle>=2.0.0',
@@ -57,6 +58,7 @@
5758
'yapf',
5859
],
5960
'autopep8': ['autopep8'],
61+
'flake8': ['flake8'],
6062
'mccabe': ['mccabe'],
6163
'pycodestyle': ['pycodestyle'],
6264
'pydocstyle': ['pydocstyle>=2.0.0'],
@@ -76,6 +78,7 @@
7678
],
7779
'pyls': [
7880
'autopep8 = pyls.plugins.autopep8_format',
81+
'flake8 = pyls.plugins.flake8_lint',
7982
'jedi_completion = pyls.plugins.jedi_completion',
8083
'jedi_definition = pyls.plugins.definition',
8184
'jedi_hover = pyls.plugins.hover',

test/plugins/test_flake8_lint.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright 2019 Palantir Technologies, Inc.
2+
import tempfile
3+
import os
4+
from pyls import lsp, uris
5+
from pyls.plugins import flake8_lint
6+
from pyls.workspace import Document
7+
8+
DOC_URI = uris.from_fs_path(__file__)
9+
DOC = """import pyls
10+
11+
t = "TEST"
12+
13+
def using_const():
14+
\ta = 8 + 9
15+
\treturn t
16+
"""
17+
18+
19+
def temp_document(doc_text):
20+
temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False)
21+
name = temp_file.name
22+
temp_file.write(doc_text)
23+
temp_file.close()
24+
doc = Document(uris.from_fs_path(name))
25+
26+
return name, doc
27+
28+
29+
def test_flake8_no_checked_file(config):
30+
# A bad uri or a non-saved file may cause the flake8 linter to do nothing.
31+
# In this situtation, the linter will return an empty list.
32+
33+
doc = Document('', DOC)
34+
diags = flake8_lint.pyls_lint(config, doc)
35+
assert diags == []
36+
37+
38+
def test_flake8_lint(config):
39+
try:
40+
name, doc = temp_document(DOC)
41+
diags = flake8_lint.pyls_lint(config, doc)
42+
msg = 'local variable \'a\' is assigned to but never used'
43+
unused_var = [d for d in diags if d['message'] == msg][0]
44+
45+
assert unused_var['source'] == 'flake8'
46+
assert unused_var['code'] == 'F841'
47+
assert unused_var['range']['start'] == {'line': 6, 'character': 1}
48+
assert unused_var['range']['end'] == {'line': 6, 'character': 11}
49+
assert unused_var['severity'] == lsp.DiagnosticSeverity.Warning
50+
51+
finally:
52+
os.remove(name)

0 commit comments

Comments
 (0)