diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8b081296..4a8b1c2d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -62,6 +62,7 @@ phutil_register_library_map(array( 'ArcanistPhutilTestCase' => 'unit/engine/phutil/testcase', 'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/exception', 'ArcanistPyFlakesLinter' => 'lint/linter/pyflakes', + 'ArcanistPyLintLinter' => 'lint/linter/pylint', 'ArcanistRepositoryAPI' => 'repository/api/base', 'ArcanistShellCompleteWorkflow' => 'workflow/shell-complete', 'ArcanistSubversionAPI' => 'repository/api/subversion', @@ -115,6 +116,7 @@ phutil_register_library_map(array( 'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPhutilModuleLinter' => 'ArcanistLinter', 'ArcanistPyFlakesLinter' => 'ArcanistLinter', + 'ArcanistPyLintLinter' => 'ArcanistLinter', 'ArcanistShellCompleteWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', 'ArcanistSvnHookPreCommitWorkflow' => 'ArcanistBaseWorkflow', diff --git a/src/lint/linter/pylint/ArcanistPyLintLinter.php b/src/lint/linter/pylint/ArcanistPyLintLinter.php new file mode 100644 index 00000000..7d5cf545 --- /dev/null +++ b/src/lint/linter/pylint/ArcanistPyLintLinter.php @@ -0,0 +1,168 @@ +getEngine()->getWorkingCopy(); + $code_map = array( + ArcanistLintSeverity::SEVERITY_ERROR => + $working_copy->getConfig('lint.pylint.codes.error'), + ArcanistLintSeverity::SEVERITY_WARNING => + $working_copy->getConfig('lint.pylint.codes.warning'), + ArcanistLintSeverity::SEVERITY_ADVICE => + $working_copy->getConfig('lint.pylint.codes.advice'), + ); + + foreach ($code_map as $sev => $codes) { + if ($codes !== null) { + foreach ($codes as $code_re) { + if (preg_match("/{$code_re}/", $code)) { + return $sev; + } + } + } + } + + // If the message code doesn't match any of the provided regex's, + // then just disable it. + return ArcanistLintSeverity::SEVERITY_DISABLED; + } + + private function getPyLintPath() { + $pylint_bin = "pylint"; + + // Use the PyLint prefix specified in the config file + $working_copy = $this->getEngine()->getWorkingCopy(); + $prefix = $working_copy->getConfig('lint.pylint.prefix'); + if ($prefix !== null) { + $pylint_bin = $prefix."/bin/".$pylint_bin; + } + + return $pylint_bin; + } + + private function getPyLintPythonPath() { + // Get non-default install locations for pylint and its dependencies + // libraries. + $working_copy = $this->getEngine()->getWorkingCopy(); + $prefixes = array( + $working_copy->getConfig('lint.pylint.prefix'), + $working_copy->getConfig('lint.pylint.logilab_astng.prefix'), + $working_copy->getConfig('lint.pylint.logilab_common.prefix'), + ); + + // Add the libraries to the python search path + $python_path = array(); + foreach ($prefixes as $prefix) { + if ($prefix !== null) { + $python_path[] = $prefix.'/lib/python2.6/site-packages'; + } + } + + $python_path[] = ''; + return implode(":", $python_path); + } + + private function getPyLintOptions() { + // Options to pass the PyLint + // - '-rn': don't print lint report/summary at end + // - '-iy': show message codes for lint warnings/errors + $options = array('-rn', '-iy'); + + // Add any options defined in the config file for PyLint + $working_copy = $this->getEngine()->getWorkingCopy(); + $config_options = $working_copy->getConfig('lint.pylint.options'); + if ($config_options !== null) { + $options += $config_options; + } + + return implode(" ", $options); + } + + public function willLintPaths(array $paths) { + return; + } + + public function getLinterName() { + return 'PyLint'; + } + + public function getLintSeverityMap() { + return array(); + } + + public function getLintNameMap() { + return array(); + } + + public function lintPath($path) { + $pylint_bin = $this->getPyLintPath(); + $python_path = $this->getPyLintPythonPath(); + $options = $this->getPyLintOptions(); + $path_on_disk = $this->getEngine()->getFilePathOnDisk($path); + + try { + list($stdout, $_) = execx( + "/usr/bin/env PYTHONPATH=%s\$PYTHONPATH ". + "{$pylint_bin} {$options} {$path_on_disk}", + $python_path); + } catch (CommandException $e) { + // PyLint will return a non-zero exit code if warnings/errors are found. + // Therefore we detect command failure by checking that the stderr is + // some non-expected value. + if ($e->getStderr() !== "No config file found, ". + "using default configuration\n") { + throw $e; + } + + $stdout = $e->getStdout(); + } + + $lines = explode("\n", $stdout); + $messages = array(); + foreach ($lines as $line) { + $matches = null; + if (!preg_match('/([A-Z]\d+): *(\d+): *(.*)$/', $line, $matches)) { + continue; + } + foreach ($matches as $key => $match) { + $matches[$key] = trim($match); + } + $message = new ArcanistLintMessage(); + $message->setPath($path); + $message->setLine($matches[2]); + $message->setCode($matches[1]); + $message->setName($this->getLinterName()." ".$matches[1]); + $message->setDescription($matches[3]); + $message->setSeverity($this->getMessageCodeSeverity($matches[1])); + $this->addLintMessage($message); + } + } + +} diff --git a/src/lint/linter/pylint/__init__.php b/src/lint/linter/pylint/__init__.php new file mode 100644 index 00000000..621bede3 --- /dev/null +++ b/src/lint/linter/pylint/__init__.php @@ -0,0 +1,16 @@ +