1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# ----------------------------------------------------------------------------- |
3
|
|
|
# Copyright (c) 2016 Continuum Analytics, Inc. |
4
|
|
|
# |
5
|
|
|
# Licensed under the terms of the MIT License |
6
|
|
|
# (see LICENSE.txt for details) |
7
|
|
|
# ----------------------------------------------------------------------------- |
8
|
|
|
"""Generic and custom code linters.""" |
9
|
|
|
|
10
|
|
|
# Standard library imports |
11
|
|
|
import json |
12
|
|
|
import os |
13
|
|
|
import re |
14
|
|
|
|
15
|
|
|
# Local imports |
16
|
|
|
from ciocheck.tools import Tool |
17
|
|
|
from ciocheck.utils import run_command |
18
|
|
|
|
19
|
|
|
|
20
|
|
|
class Linter(Tool): |
21
|
|
|
"""Generic linter with json and regex output support.""" |
22
|
|
|
|
23
|
|
|
# Regex matching |
24
|
|
|
pattern = None |
25
|
|
|
|
26
|
|
|
# Json matching |
27
|
|
|
json_keys = [] # ((old_key, new_key), ...) |
28
|
|
|
output_on_stderr = False |
29
|
|
|
|
30
|
|
|
def __init__(self, cmd_root): |
31
|
|
|
"""Generic linter with json and regex output support.""" |
32
|
|
|
super(Linter, self).__init__(cmd_root) |
33
|
|
|
self.paths = None |
34
|
|
|
self.regex = None |
35
|
|
|
|
36
|
|
|
def _parse_regex(self, string): |
37
|
|
|
"""Parse output with grouped regex.""" |
38
|
|
|
results = [] |
39
|
|
|
self.regex = re.compile(self.pattern, re.VERBOSE) |
40
|
|
|
for matches in self.regex.finditer(string): |
41
|
|
|
results.append(matches.groupdict()) |
42
|
|
|
return results |
43
|
|
|
|
44
|
|
|
def _parse_json(self, string): |
45
|
|
|
"""Parse output with json keys.""" |
46
|
|
|
data = json.loads(string) |
47
|
|
|
results = [] |
48
|
|
|
for item in data: |
49
|
|
|
new_item = {} |
50
|
|
|
for (old_key, new_key) in self.json_keys: |
51
|
|
|
new_item[new_key] = item.pop(old_key) |
52
|
|
|
new_item.update(item) |
53
|
|
|
results.append(new_item) |
54
|
|
|
return results |
55
|
|
|
|
56
|
|
|
def _parse(self, string): |
57
|
|
|
"""Parse linter output.""" |
58
|
|
|
if self.json_keys: |
59
|
|
|
results = self._parse_json(string) |
60
|
|
|
elif self.pattern: |
61
|
|
|
results = self._parse_regex(string) |
62
|
|
|
else: |
63
|
|
|
raise Exception('Either a pattern or a json key mapping has to ' |
64
|
|
|
'be defined.') |
65
|
|
|
return results |
66
|
|
|
|
67
|
|
|
def extra_processing(self, results): |
68
|
|
|
"""Override in case extra processing on results is needed.""" |
69
|
|
|
return results |
70
|
|
|
|
71
|
|
|
def run(self, paths): |
72
|
|
|
"""Run linter and return a list of dicts.""" |
73
|
|
|
self.paths = list(paths.keys()) if isinstance(paths, dict) else paths |
74
|
|
|
if self.paths: |
75
|
|
|
args = list(self.command) |
76
|
|
|
args += self.paths |
77
|
|
|
out, err = run_command(args) |
78
|
|
|
if self.output_on_stderr: |
79
|
|
|
string = err |
80
|
|
|
else: |
81
|
|
|
string = out |
82
|
|
|
results = self._parse(string) |
83
|
|
|
results = self.extra_processing(results) |
84
|
|
|
else: |
85
|
|
|
results = [] |
86
|
|
|
|
87
|
|
|
return results |
88
|
|
|
|
89
|
|
|
|
90
|
|
|
class Flake8Linter(Linter): |
91
|
|
|
"""Flake8 python tool runner.""" |
92
|
|
|
|
93
|
|
|
language = 'python' |
94
|
|
|
name = 'flake8' |
95
|
|
|
extensions = ('py', ) |
96
|
|
|
command = ('flake8', ) |
97
|
|
|
config_file = '.flake8' |
98
|
|
|
config_sections = [('flake8', 'flake8')] |
99
|
|
|
|
100
|
|
|
# Match lines of the form: |
101
|
|
|
# path/to/file.py:328: undefined name '_thing' |
102
|
|
|
pattern = r''' |
103
|
|
|
(?P<path>.*?):(?P<line>\d{1,1000}): |
104
|
|
|
(?P<column>\d{1,1000}):\s |
105
|
|
|
(?P<type>[EWFCNTIBDSQ]\d{3})\s |
106
|
|
|
(?P<message>.*) |
107
|
|
|
''' |
108
|
|
|
|
109
|
|
|
|
110
|
|
|
class Pep8Linter(Linter): |
111
|
|
|
"""Pep8 python tool runner.""" |
112
|
|
|
|
113
|
|
|
language = 'python' |
114
|
|
|
name = 'pep8' |
115
|
|
|
extensions = ('py', ) |
116
|
|
|
command = ('pep8', ) |
117
|
|
|
config_file = '.pep8' |
118
|
|
|
config_sections = [('pep8', 'pep8')] |
119
|
|
|
|
120
|
|
|
# Match lines of the form: |
121
|
|
|
pattern = r''' |
122
|
|
|
(?P<path>.*?):(?P<line>\d{1,1000}): |
123
|
|
|
(?P<column>\d{1,1000}):\s |
124
|
|
|
(?P<type>[EWFCNTIBDSQ]\d{3})\s |
125
|
|
|
(?P<message>.*) |
126
|
|
|
''' |
127
|
|
|
|
128
|
|
|
|
129
|
|
|
class PydocstyleLinter(Linter): |
130
|
|
|
"""Pydocstyle python tool runner.""" |
131
|
|
|
|
132
|
|
|
language = 'python' |
133
|
|
|
name = 'pydocstyle' |
134
|
|
|
extensions = ('py', ) |
135
|
|
|
command = ('pydocstyle', ) |
136
|
|
|
config_file = '.pydocstyle' |
137
|
|
|
config_sections = [('pydocstyle', 'pydocstyle')] |
138
|
|
|
output_on_stderr = True |
139
|
|
|
|
140
|
|
|
# Match lines of the form: |
141
|
|
|
# ./bootstrap.py:1 at module level: |
142
|
|
|
# D400: First line should end with a period (not 't') |
143
|
|
|
pattern = r''' |
144
|
|
|
(?P<path>.*?): |
145
|
|
|
(?P<line>\d{1,1000000})\ # 1 million lines of code :-p ? |
146
|
|
|
(?P<symbol>.*):\n.*? |
147
|
|
|
(?P<type>D\d{3}):\s |
148
|
|
|
(?P<message>.*) |
149
|
|
|
''' |
150
|
|
|
|
151
|
|
|
|
152
|
|
|
class PylintLinter(Linter): |
153
|
|
|
"""Pylint python tool runner.""" |
154
|
|
|
|
155
|
|
|
language = 'python' |
156
|
|
|
name = 'pylint' |
157
|
|
|
extensions = ('py', ) |
158
|
|
|
command = ('pylint', '--output-format', 'json', '-j', '0') |
159
|
|
|
config_file = '.pydocstyle' |
160
|
|
|
config_sections = [('pydocstyle', 'pydocstyle')] |
161
|
|
|
json_keys = ( |
162
|
|
|
('message', 'message'), |
163
|
|
|
('line', 'line'), |
164
|
|
|
('column', 'column'), |
165
|
|
|
('type', 'type'), |
166
|
|
|
('path', 'path'), ) |
167
|
|
|
|
168
|
|
|
def extra_processing(self, results): |
169
|
|
|
"""Make path an absolute path.""" |
170
|
|
|
for item in results: |
171
|
|
|
item['path'] = os.path.join(self.cmd_root, item['path']) |
172
|
|
|
return results |
173
|
|
|
|
174
|
|
|
|
175
|
|
|
LINTERS = [ |
176
|
|
|
Pep8Linter, |
177
|
|
|
PydocstyleLinter, |
178
|
|
|
Flake8Linter, |
179
|
|
|
PylintLinter, |
180
|
|
|
] |
181
|
|
|
|
182
|
|
|
|
183
|
|
|
def test(): |
184
|
|
|
"""Main local test.""" |
185
|
|
|
here = os.path.dirname(os.path.realpath(__file__)) |
186
|
|
|
paths = [here] |
187
|
|
|
linter = PylintLinter(here) |
188
|
|
|
results = linter.run(paths) |
189
|
|
|
for result in results: |
190
|
|
|
print(result) |
191
|
|
|
|
192
|
|
|
|
193
|
|
|
if __name__ == '__main__': |
194
|
|
|
test() |
195
|
|
|
|