Source code

Revision control

Other Tools

1
# This Source Code Form is subject to the terms of the Mozilla Public
2
# License, v. 2.0. If a copy of the MPL was not distributed with this
3
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5
import json
6
import os
7
import platform
8
import subprocess
9
import sys
10
11
import mozfile
12
13
from mozlint import result
14
from mozlint.pathutils import expand_exclusions
15
from mozlint.util import pip
16
17
here = os.path.abspath(os.path.dirname(__file__))
18
FLAKE8_REQUIREMENTS_PATH = os.path.join(here, 'flake8_requirements.txt')
19
20
FLAKE8_NOT_FOUND = """
21
Could not find flake8! Install flake8 and try again.
22
23
$ pip install -U --require-hashes -r {}
24
""".strip().format(FLAKE8_REQUIREMENTS_PATH)
25
26
27
FLAKE8_INSTALL_ERROR = """
28
Unable to install correct version of flake8
29
Try to install it manually with:
30
$ pip install -U --require-hashes -r {}
31
""".strip().format(FLAKE8_REQUIREMENTS_PATH)
32
33
LINE_OFFSETS = {
34
# continuation line under-indented for hanging indent
35
'E121': (-1, 2),
36
# continuation line missing indentation or outdented
37
'E122': (-1, 2),
38
# continuation line over-indented for hanging indent
39
'E126': (-1, 2),
40
# continuation line over-indented for visual indent
41
'E127': (-1, 2),
42
# continuation line under-indented for visual indent
43
'E128': (-1, 2),
44
# continuation line unaligned for hanging indend
45
'E131': (-1, 2),
46
# expected 1 blank line, found 0
47
'E301': (-1, 2),
48
# expected 2 blank lines, found 1
49
'E302': (-2, 3),
50
}
51
"""Maps a flake8 error to a lineoffset tuple.
52
53
The offset is of the form (lineno_offset, num_lines) and is passed
54
to the lineoffset property of an `Issue`.
55
"""
56
57
# We use sys.prefix to find executables as that gets modified with
58
# virtualenv's activate_this.py, whereas sys.executable doesn't.
59
if platform.system() == 'Windows':
60
bindir = os.path.join(sys.prefix, 'Scripts')
61
else:
62
bindir = os.path.join(sys.prefix, 'bin')
63
64
65
class NothingToLint(Exception):
66
"""Exception used to bail out of flake8's internals if all the specified
67
files were excluded.
68
"""
69
70
71
def setup(root, **lintargs):
72
if not pip.reinstall_program(FLAKE8_REQUIREMENTS_PATH):
73
print(FLAKE8_INSTALL_ERROR)
74
return 1
75
76
77
def lint(paths, config, **lintargs):
78
from flake8.main.application import Application
79
80
log = lintargs['log']
81
root = lintargs['root']
82
config_path = os.path.join(root, '.flake8')
83
84
if lintargs.get('fix'):
85
fix_cmd = [
86
os.path.join(bindir, 'autopep8'),
87
'--global-config', config_path,
88
'--in-place', '--recursive',
89
]
90
91
if config.get('exclude'):
92
fix_cmd.extend(['--exclude', ','.join(config['exclude'])])
93
94
subprocess.call(fix_cmd + paths)
95
96
# Run flake8.
97
app = Application()
98
99
output_file = mozfile.NamedTemporaryFile(mode='r')
100
flake8_cmd = [
101
'--config', config_path,
102
'--output-file', output_file.name,
103
'--format', '{"path":"%(path)s","lineno":%(row)s,'
104
'"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}',
105
'--filename', ','.join(['*.{}'.format(e) for e in config['extensions']]),
106
]
107
log.debug("Command: {}".format(' '.join(flake8_cmd)))
108
109
orig_make_file_checker_manager = app.make_file_checker_manager
110
111
def wrap_make_file_checker_manager(self):
112
"""Flake8 is very inefficient when it comes to applying exclusion
113
rules, using `expand_exclusions` to turn directories into a list of
114
relevant python files is an order of magnitude faster.
115
116
Hooking into flake8 here also gives us a convenient place to merge the
117
`exclude` rules specified in the root .flake8 with the ones added by
118
tools/lint/mach_commands.py.
119
"""
120
config.setdefault('exclude', []).extend(self.options.exclude)
121
self.options.exclude = None
122
self.args = self.args + list(expand_exclusions(paths, config, root))
123
124
if not self.args:
125
raise NothingToLint
126
return orig_make_file_checker_manager()
127
128
app.make_file_checker_manager = wrap_make_file_checker_manager.__get__(app, Application)
129
130
# Make sure to run from repository root so exclusions are joined to the
131
# repository root and not the current working directory.
132
oldcwd = os.getcwd()
133
os.chdir(root)
134
try:
135
app.run(flake8_cmd)
136
except NothingToLint:
137
pass
138
finally:
139
os.chdir(oldcwd)
140
141
results = []
142
143
def process_line(line):
144
# Escape slashes otherwise JSON conversion will not work
145
line = line.replace('\\', '\\\\')
146
try:
147
res = json.loads(line)
148
except ValueError:
149
print('Non JSON output from linter, will not be processed: {}'.format(line))
150
return
151
152
if res.get('code') in LINE_OFFSETS:
153
res['lineoffset'] = LINE_OFFSETS[res['code']]
154
155
results.append(result.from_config(config, **res))
156
157
list(map(process_line, output_file.readlines()))
158
return results