~ubuntu-branches/ubuntu/wily/phabricator/wily

« back to all changes in this revision

Viewing changes to src/lint/linter/ArcanistPyLintLinter.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-11-01 23:20:06 UTC
  • mto: This revision was merged to the branch mainline in revision 4.
  • Revision ID: package-import@ubuntu.com-20141101232006-mvlnp0cil67tsboe
Tags: upstream-0~git20141101/arcanist
Import upstream version 0~git20141101, component arcanist

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
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.
 
7
 *
 
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:
 
11
 *
 
12
 *   lint.pylint.prefix
 
13
 *   lint.pylint.logilab_astng.prefix
 
14
 *   lint.pylint.logilab_common.prefix
 
15
 *
 
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.
 
20
 *
 
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
 
24
 * ##text##.
 
25
 *
 
26
 * Specify which PyLint messages map to which Arcanist messages by defining
 
27
 * the following regular expressions:
 
28
 *
 
29
 *   lint.pylint.codes.error
 
30
 *   lint.pylint.codes.warning
 
31
 *   lint.pylint.codes.advice
 
32
 *
 
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:
 
36
 *
 
37
 *    ^E.*
 
38
 *
 
39
 * You can also match more granularly:
 
40
 *
 
41
 *    ^E(0001|0002)$
 
42
 *
 
43
 * According to ##man pylint##, there are 5 kind of messages:
 
44
 *
 
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.
 
51
 */
 
52
final class ArcanistPyLintLinter extends ArcanistLinter {
 
53
 
 
54
  private function getMessageCodeSeverity($code) {
 
55
    $config = $this->getEngine()->getConfigurationManager();
 
56
 
 
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');
 
63
 
 
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.");
 
70
    }
 
71
 
 
72
    $code_map = array(
 
73
      ArcanistLintSeverity::SEVERITY_ERROR    => $error_regexp,
 
74
      ArcanistLintSeverity::SEVERITY_WARNING  => $warning_regexp,
 
75
      ArcanistLintSeverity::SEVERITY_ADVICE   => $advice_regexp,
 
76
    );
 
77
 
 
78
    foreach ($code_map as $sev => $codes) {
 
79
      if ($codes === null) {
 
80
        continue;
 
81
      }
 
82
      if (!is_array($codes)) {
 
83
        $codes = array($codes);
 
84
      }
 
85
      foreach ($codes as $code_re) {
 
86
        if (preg_match("/{$code_re}/", $code)) {
 
87
          return $sev;
 
88
        }
 
89
      }
 
90
    }
 
91
 
 
92
    // If the message code doesn't match any of the provided regex's,
 
93
    // then just disable it.
 
94
    return ArcanistLintSeverity::SEVERITY_DISABLED;
 
95
  }
 
96
 
 
97
  private function getPyLintPath() {
 
98
    $pylint_bin = 'pylint';
 
99
 
 
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;
 
105
    }
 
106
 
 
107
    if (!Filesystem::pathExists($pylint_bin)) {
 
108
 
 
109
      list($err) = exec_manual('which %s', $pylint_bin);
 
110
      if ($err) {
 
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.");
 
116
      }
 
117
    }
 
118
 
 
119
    return $pylint_bin;
 
120
  }
 
121
 
 
122
  private function getPyLintPythonPath() {
 
123
    // Get non-default install locations for pylint and its dependencies
 
124
    // libraries.
 
125
    $config = $this->getEngine()->getConfigurationManager();
 
126
    $prefixes = array(
 
127
      $config->getConfigFromAnySource('lint.pylint.prefix'),
 
128
      $config->getConfigFromAnySource('lint.pylint.logilab_astng.prefix'),
 
129
      $config->getConfigFromAnySource('lint.pylint.logilab_common.prefix'),
 
130
    );
 
131
 
 
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';
 
140
      }
 
141
    }
 
142
 
 
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(
 
149
            $config_path,
 
150
            $working_copy->getProjectRoot());
 
151
        }
 
152
      }
 
153
    }
 
154
 
 
155
    $python_path[] = '';
 
156
    return implode(':', $python_path);
 
157
  }
 
158
 
 
159
  private function getPyLintOptions() {
 
160
    // '-rn': don't print lint report/summary at end
 
161
    $options = array('-rn');
 
162
 
 
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');
 
166
    }
 
167
    // Version 1.x.x set the output specifically to the 0.x.x format
 
168
    else {
 
169
      array_push($options, "--msg-template='{msg_id}:{line:3d}: {obj}: {msg}'");
 
170
    }
 
171
 
 
172
    $working_copy = $this->getEngine()->getWorkingCopy();
 
173
    $config = $this->getEngine()->getConfigurationManager();
 
174
 
 
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
 
177
    // careful.
 
178
    $rcfile = $config->getConfigFromAnySource('lint.pylint.rcfile');
 
179
    if ($rcfile !== null) {
 
180
      $rcfile = Filesystem::resolvePath(
 
181
        $rcfile,
 
182
        $working_copy->getProjectRoot());
 
183
      $options[] = csprintf('--rcfile=%s', $rcfile);
 
184
    }
 
185
 
 
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);
 
190
    }
 
191
 
 
192
    return implode(' ', $options);
 
193
  }
 
194
 
 
195
  public function getLinterName() {
 
196
    return 'PyLint';
 
197
  }
 
198
 
 
199
  private function getLinterVersion() {
 
200
    $pylint_bin = $this->getPyLintPath();
 
201
    $options = '--version';
 
202
 
 
203
    list($stdout) = execx('%s %s', $pylint_bin, $options);
 
204
 
 
205
    $lines = phutil_split_lines($stdout, false);
 
206
    $matches = null;
 
207
 
 
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)) {
 
212
      return '999';
 
213
    }
 
214
 
 
215
    return $matches[1];
 
216
  }
 
217
 
 
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);
 
223
 
 
224
    try {
 
225
      list($stdout, $_) = execx(
 
226
        '/usr/bin/env PYTHONPATH=%s$PYTHONPATH %s %C %s',
 
227
        $python_path,
 
228
        $pylint_bin,
 
229
        $options,
 
230
        $path_on_disk);
 
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.
 
235
        throw $e;
 
236
      } else {
 
237
        // The other non-zero exit codes mean there were messages issued,
 
238
        // which is expected, so don't exit.
 
239
        $stdout = $e->getStdout();
 
240
      }
 
241
    }
 
242
 
 
243
    $lines = phutil_split_lines($stdout, false);
 
244
    $messages = array();
 
245
    foreach ($lines as $line) {
 
246
      $matches = null;
 
247
      $regex = '/([A-Z]\d+): *(\d+)(?:|,\d*): *(.*)$/';
 
248
      if (!preg_match($regex, $line, $matches)) {
 
249
        continue;
 
250
      }
 
251
      foreach ($matches as $key => $match) {
 
252
        $matches[$key] = trim($match);
 
253
      }
 
254
 
 
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);
 
263
    }
 
264
  }
 
265
 
 
266
}