4
* Uses "PyLint" to detect various errors in Python code. To use this linter,
5
* you must install pylint and configure which codes you want to be reported as
6
* errors, warnings and advice.
8
* You should be able to install pylint with ##sudo easy_install pylint##. If
9
* your system is unusual, you can manually specify the location of pylint and
10
* its dependencies by configuring these keys in your .arcconfig:
13
* lint.pylint.logilab_astng.prefix
14
* lint.pylint.logilab_common.prefix
16
* You can specify additional command-line options to pass to PyLint by
17
* setting ##lint.pylint.options##. You may also specify a list of additional
18
* entries for PYTHONPATH with ##lint.pylint.pythonpath##. Those can be
19
* absolute or relative to the project root.
21
* If you have a PyLint rcfile, specify its path with
22
* ##lint.pylint.rcfile##. It can be absolute or relative to the project
23
* root. Be sure not to define ##output-format##, or if you do, set it to
26
* Specify which PyLint messages map to which Arcanist messages by defining
27
* the following regular expressions:
29
* lint.pylint.codes.error
30
* lint.pylint.codes.warning
31
* lint.pylint.codes.advice
33
* The regexps are run in that order; the first to match determines which
34
* Arcanist severity applies, if any. For example, to capture all PyLint
35
* "E...." errors as Arcanist errors, set ##lint.pylint.codes.error## to:
39
* You can also match more granularly:
43
* According to ##man pylint##, there are 5 kind of messages:
45
* (C) convention, for programming standard violation
46
* (R) refactor, for bad code smell
47
* (W) warning, for python specific problems
48
* (E) error, for probable bugs in the code
49
* (F) fatal, if an error occurred which prevented pylint from
50
* doing further processing.
52
final class ArcanistPyLintLinter extends ArcanistLinter {
54
private function getMessageCodeSeverity($code) {
55
$config = $this->getEngine()->getConfigurationManager();
57
$error_regexp = $config->getConfigFromAnySource(
58
'lint.pylint.codes.error');
59
$warning_regexp = $config->getConfigFromAnySource(
60
'lint.pylint.codes.warning');
61
$advice_regexp = $config->getConfigFromAnySource(
62
'lint.pylint.codes.advice');
64
if (!$error_regexp && !$warning_regexp && !$advice_regexp) {
65
throw new ArcanistUsageException(
66
"You are invoking the PyLint linter but have not configured any of ".
67
"'lint.pylint.codes.error', 'lint.pylint.codes.warning', or ".
68
"'lint.pylint.codes.advice'. Consult the documentation for ".
69
"ArcanistPyLintLinter.");
73
ArcanistLintSeverity::SEVERITY_ERROR => $error_regexp,
74
ArcanistLintSeverity::SEVERITY_WARNING => $warning_regexp,
75
ArcanistLintSeverity::SEVERITY_ADVICE => $advice_regexp,
78
foreach ($code_map as $sev => $codes) {
79
if ($codes === null) {
82
if (!is_array($codes)) {
83
$codes = array($codes);
85
foreach ($codes as $code_re) {
86
if (preg_match("/{$code_re}/", $code)) {
92
// If the message code doesn't match any of the provided regex's,
93
// then just disable it.
94
return ArcanistLintSeverity::SEVERITY_DISABLED;
97
private function getPyLintPath() {
98
$pylint_bin = 'pylint';
100
// Use the PyLint prefix specified in the config file
101
$config = $this->getEngine()->getConfigurationManager();
102
$prefix = $config->getConfigFromAnySource('lint.pylint.prefix');
103
if ($prefix !== null) {
104
$pylint_bin = $prefix.'/bin/'.$pylint_bin;
107
if (!Filesystem::pathExists($pylint_bin)) {
109
list($err) = exec_manual('which %s', $pylint_bin);
111
throw new ArcanistUsageException(
112
"PyLint does not appear to be installed on this system. Install it ".
113
"(e.g., with 'sudo easy_install pylint') or configure ".
114
"'lint.pylint.prefix' in your .arcconfig to point to the directory ".
115
"where it resides.");
122
private function getPyLintPythonPath() {
123
// Get non-default install locations for pylint and its dependencies
125
$config = $this->getEngine()->getConfigurationManager();
127
$config->getConfigFromAnySource('lint.pylint.prefix'),
128
$config->getConfigFromAnySource('lint.pylint.logilab_astng.prefix'),
129
$config->getConfigFromAnySource('lint.pylint.logilab_common.prefix'),
132
// Add the libraries to the python search path
133
$python_path = array();
134
foreach ($prefixes as $prefix) {
135
if ($prefix !== null) {
136
$python_path[] = $prefix.'/lib/python2.7/site-packages';
137
$python_path[] = $prefix.'/lib/python2.7/dist-packages';
138
$python_path[] = $prefix.'/lib/python2.6/site-packages';
139
$python_path[] = $prefix.'/lib/python2.6/dist-packages';
143
$working_copy = $this->getEngine()->getWorkingCopy();
144
$config_paths = $config->getConfigFromAnySource('lint.pylint.pythonpath');
145
if ($config_paths !== null) {
146
foreach ($config_paths as $config_path) {
147
if ($config_path !== null) {
148
$python_path[] = Filesystem::resolvePath(
150
$working_copy->getProjectRoot());
156
return implode(':', $python_path);
159
private function getPyLintOptions() {
160
// '-rn': don't print lint report/summary at end
161
$options = array('-rn');
163
// Version 0.x.x include the pylint message ids in the output
164
if (version_compare($this->getLinterVersion(), '1', 'lt')) {
165
array_push($options, '-iy', '--output-format=text');
167
// Version 1.x.x set the output specifically to the 0.x.x format
169
array_push($options, "--msg-template='{msg_id}:{line:3d}: {obj}: {msg}'");
172
$working_copy = $this->getEngine()->getWorkingCopy();
173
$config = $this->getEngine()->getConfigurationManager();
175
// Specify an --rcfile, either absolute or relative to the project root.
176
// Stupidly, the command line args above are overridden by rcfile, so be
178
$rcfile = $config->getConfigFromAnySource('lint.pylint.rcfile');
179
if ($rcfile !== null) {
180
$rcfile = Filesystem::resolvePath(
182
$working_copy->getProjectRoot());
183
$options[] = csprintf('--rcfile=%s', $rcfile);
186
// Add any options defined in the config file for PyLint
187
$config_options = $config->getConfigFromAnySource('lint.pylint.options');
188
if ($config_options !== null) {
189
$options = array_merge($options, $config_options);
192
return implode(' ', $options);
195
public function getLinterName() {
199
private function getLinterVersion() {
200
$pylint_bin = $this->getPyLintPath();
201
$options = '--version';
203
list($stdout) = execx('%s %s', $pylint_bin, $options);
205
$lines = phutil_split_lines($stdout, false);
208
// If the version command didn't return anything or the regex didn't match
209
// Assume a future version that at least is compatible with 1.x.x
210
if (count($lines) == 0 ||
211
!preg_match('/pylint\s((?:\d+\.?)+)/', $lines[0], $matches)) {
218
public function lintPath($path) {
219
$pylint_bin = $this->getPyLintPath();
220
$python_path = $this->getPyLintPythonPath();
221
$options = $this->getPyLintOptions();
222
$path_on_disk = $this->getEngine()->getFilePathOnDisk($path);
225
list($stdout, $_) = execx(
226
'/usr/bin/env PYTHONPATH=%s$PYTHONPATH %s %C %s',
231
} catch (CommandException $e) {
232
if ($e->getError() == 32) {
233
// According to ##man pylint## the exit status of 32 means there was a
234
// usage error. That's bad, so actually exit abnormally.
237
// The other non-zero exit codes mean there were messages issued,
238
// which is expected, so don't exit.
239
$stdout = $e->getStdout();
243
$lines = phutil_split_lines($stdout, false);
245
foreach ($lines as $line) {
247
$regex = '/([A-Z]\d+): *(\d+)(?:|,\d*): *(.*)$/';
248
if (!preg_match($regex, $line, $matches)) {
251
foreach ($matches as $key => $match) {
252
$matches[$key] = trim($match);
255
$message = new ArcanistLintMessage();
256
$message->setPath($path);
257
$message->setLine($matches[2]);
258
$message->setCode($matches[1]);
259
$message->setName($this->getLinterName().' '.$matches[1]);
260
$message->setDescription($matches[3]);
261
$message->setSeverity($this->getMessageCodeSeverity($matches[1]));
262
$this->addLintMessage($message);