getEngine()->getConfigurationManager(); $error_regexp = $config->getConfigFromAnySource( 'lint.pylint.codes.error'); $warning_regexp = $config->getConfigFromAnySource( 'lint.pylint.codes.warning'); $advice_regexp = $config->getConfigFromAnySource( 'lint.pylint.codes.advice'); if (!$error_regexp && !$warning_regexp && !$advice_regexp) { throw new ArcanistUsageException( "You are invoking the PyLint linter but have not configured any of ". "'lint.pylint.codes.error', 'lint.pylint.codes.warning', or ". "'lint.pylint.codes.advice'. Consult the documentation for ". "ArcanistPyLintLinter."); } $code_map = array( ArcanistLintSeverity::SEVERITY_ERROR => $error_regexp, ArcanistLintSeverity::SEVERITY_WARNING => $warning_regexp, ArcanistLintSeverity::SEVERITY_ADVICE => $advice_regexp, ); foreach ($code_map as $sev => $codes) { if ($codes === null) { continue; } if (!is_array($codes)) { $codes = array($codes); } 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 $config = $this->getEngine()->getConfigurationManager(); $prefix = $config->getConfigFromAnySource('lint.pylint.prefix'); if ($prefix !== null) { $pylint_bin = $prefix.'/bin/'.$pylint_bin; } if (!Filesystem::pathExists($pylint_bin)) { list($err) = exec_manual('which %s', $pylint_bin); if ($err) { throw new ArcanistUsageException( "PyLint does not appear to be installed on this system. Install it ". "(e.g., with 'sudo easy_install pylint') or configure ". "'lint.pylint.prefix' in your .arcconfig to point to the directory ". "where it resides."); } } return $pylint_bin; } private function getPyLintPythonPath() { // Get non-default install locations for pylint and its dependencies // libraries. $config = $this->getEngine()->getConfigurationManager(); $prefixes = array( $config->getConfigFromAnySource('lint.pylint.prefix'), $config->getConfigFromAnySource('lint.pylint.logilab_astng.prefix'), $config->getConfigFromAnySource('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.7/site-packages'; $python_path[] = $prefix.'/lib/python2.7/dist-packages'; $python_path[] = $prefix.'/lib/python2.6/site-packages'; $python_path[] = $prefix.'/lib/python2.6/dist-packages'; } } $working_copy = $this->getEngine()->getWorkingCopy(); $config_paths = $config->getConfigFromAnySource('lint.pylint.pythonpath'); if ($config_paths !== null) { foreach ($config_paths as $config_path) { if ($config_path !== null) { $python_path[] = Filesystem::resolvePath( $config_path, $working_copy->getProjectRoot()); } } } $python_path[] = ''; return implode(':', $python_path); } private function getPyLintOptions() { // '-rn': don't print lint report/summary at end $options = array('-rn'); // Version 0.x.x include the pylint message ids in the output if (version_compare($this->getLinterVersion(), '1', 'lt')) { array_push($options, '-iy', '--output-format=text'); } // Version 1.x.x set the output specifically to the 0.x.x format else { array_push($options, "--msg-template='{msg_id}:{line:3d}: {obj}: {msg}'"); } $working_copy = $this->getEngine()->getWorkingCopy(); $config = $this->getEngine()->getConfigurationManager(); // Specify an --rcfile, either absolute or relative to the project root. // Stupidly, the command line args above are overridden by rcfile, so be // careful. $rcfile = $config->getConfigFromAnySource('lint.pylint.rcfile'); if ($rcfile !== null) { $rcfile = Filesystem::resolvePath( $rcfile, $working_copy->getProjectRoot()); $options[] = csprintf('--rcfile=%s', $rcfile); } // Add any options defined in the config file for PyLint $config_options = $config->getConfigFromAnySource('lint.pylint.options'); if ($config_options !== null) { $options = array_merge($options, $config_options); } return implode(' ', $options); } public function getLinterName() { return 'PyLint'; } private function getLinterVersion() { $pylint_bin = $this->getPyLintPath(); $options = '--version'; list($stdout) = execx('%s %s', $pylint_bin, $options); $lines = phutil_split_lines($stdout, false); $matches = null; // If the version command didn't return anything or the regex didn't match // Assume a future version that at least is compatible with 1.x.x if (count($lines) == 0 || !preg_match('/pylint\s((?:\d+\.?)+)/', $lines[0], $matches)) { return '999'; } return $matches[1]; } 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 %s %C %s', $python_path, $pylint_bin, $options, $path_on_disk); } catch (CommandException $e) { if ($e->getError() == 32) { // According to ##man pylint## the exit status of 32 means there was a // usage error. That's bad, so actually exit abnormally. throw $e; } else { // The other non-zero exit codes mean there were messages issued, // which is expected, so don't exit. $stdout = $e->getStdout(); } } $lines = phutil_split_lines($stdout, false); $messages = array(); foreach ($lines as $line) { $matches = null; $regex = '/([A-Z]\d+): *(\d+)(?:|,\d*): *(.*)$/'; if (!preg_match($regex, $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); } } }