diff --git a/src/lint/linter/ArcanistCoffeeLintLinter.php b/src/lint/linter/ArcanistCoffeeLintLinter.php index 3301356a..d744146a 100644 --- a/src/lint/linter/ArcanistCoffeeLintLinter.php +++ b/src/lint/linter/ArcanistCoffeeLintLinter.php @@ -1,139 +1,135 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht( 'Install CoffeeLint using `%s`.', 'npm install -g coffeelint'); } protected function getMandatoryFlags() { $options = array( '--reporter=raw', ); if ($this->config) { $options[] = '--file='.$this->config; } return $options; } public function getLinterConfigurationOptions() { $options = array( 'coffeelint.config' => array( 'type' => 'optional string', 'help' => pht('A custom configuration file.'), ), ); return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'coffeelint.config': $this->config = $value; return; } return parent::setLinterConfigurationValue($key, $value); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $messages = array(); $output = phutil_json_decode($stdout); // We are only linting a single file. if (count($output) != 1) { return false; } foreach ($output as $reports) { foreach ($reports as $report) { // Column number is not provided in the output. // See https://github.com/clutchski/coffeelint/issues/87 $message = id(new ArcanistLintMessage()) ->setPath($path) ->setLine($report['lineNumber']) ->setCode($this->getLinterName()) ->setName(ucwords(str_replace('_', ' ', $report['name']))) ->setDescription($report['message']) ->setOriginalText(idx($report, 'line')); switch ($report['level']) { case 'warn': $message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING); break; case 'error': $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); break; default: $message->setSeverity(ArcanistLintSeverity::SEVERITY_ADVICE); break; } $messages[] = $message; } } - if ($err && !$messages) { - return false; - } - return $messages; } protected function getLintCodeFromLinterConfigurationKey($code) { // NOTE: We can't figure out which rule generated each message, so we // can not customize severities. throw new Exception( pht( "CoffeeLint does not currently support custom severity levels, ". "because rules can't be identified from messages in output.")); } } diff --git a/src/lint/linter/ArcanistCpplintLinter.php b/src/lint/linter/ArcanistCpplintLinter.php index f4a9123c..0680d474 100644 --- a/src/lint/linter/ArcanistCpplintLinter.php +++ b/src/lint/linter/ArcanistCpplintLinter.php @@ -1,80 +1,76 @@ $match) { $matches[$key] = trim($match); } $severity = $this->getLintMessageSeverity($matches[3]); $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[1]); $message->setCode($matches[3]); $message->setName($matches[3]); $message->setDescription($matches[2]); $message->setSeverity($severity); $messages[] = $message; } - if ($err && !$messages) { - return false; - } - return $messages; } protected function getLintCodeFromLinterConfigurationKey($code) { if (!preg_match('@^[a-z_]+/[a-z_]+$@', $code)) { throw new Exception( pht( 'Unrecognized lint message code "%s". Expected a valid cpplint '. 'lint code like "%s" or "%s".', $code, 'build/include_order', 'whitespace/braces')); } return $code; } } diff --git a/src/lint/linter/ArcanistExternalLinter.php b/src/lint/linter/ArcanistExternalLinter.php index 83586081..c10cbfdc 100644 --- a/src/lint/linter/ArcanistExternalLinter.php +++ b/src/lint/linter/ArcanistExternalLinter.php @@ -1,471 +1,478 @@ Mandatory flags, like `"--format=xml"`. * @task bin */ protected function getMandatoryFlags() { return array(); } /** * Provide default, overridable flags to the linter. Generally these are * configuration flags which affect behavior but aren't critical. Flags * which are required should be provided in @{method:getMandatoryFlags} * instead. * * Default flags can be overridden with @{method:setFlags}. * * @return list Overridable default flags. * @task bin */ protected function getDefaultFlags() { return array(); } /** * Override default flags with custom flags. If not overridden, flags provided * by @{method:getDefaultFlags} are used. * * @param list New flags. * @return this * @task bin */ final public function setFlags(array $flags) { $this->flags = $flags; return $this; } /** * Return the binary or script to execute. This method synthesizes defaults * and configuration. You can override the binary with @{method:setBinary}. * * @return string Binary to execute. * @task bin */ final public function getBinary() { return coalesce($this->bin, $this->getDefaultBinary()); } /** * Override the default binary with a new one. * * @param string New binary. * @return this * @task bin */ final public function setBinary($bin) { $this->bin = $bin; return $this; } /** * Return true if this linter should use an interpreter (like "python" or * "node") in addition to the script. * * After overriding this method to return `true`, override * @{method:getDefaultInterpreter} to set a default. * * @return bool True to use an interpreter. * @task bin */ public function shouldUseInterpreter() { return false; } /** * Return the default interpreter, like "python" or "node". This method is * only invoked if @{method:shouldUseInterpreter} has been overridden to * return `true`. * * @return string Default interpreter. * @task bin */ public function getDefaultInterpreter() { throw new PhutilMethodNotImplementedException(); } /** * Get the effective interpreter. This method synthesizes configuration and * defaults. * * @return string Effective interpreter. * @task bin */ final public function getInterpreter() { return coalesce($this->interpreter, $this->getDefaultInterpreter()); } /** * Set the interpreter, overriding any default. * * @param string New interpreter. * @return this * @task bin */ final public function setInterpreter($interpreter) { $this->interpreter = $interpreter; return $this; } /* -( Parsing Linter Output )---------------------------------------------- */ /** * Parse the output of the external lint program into objects of class * @{class:ArcanistLintMessage} which `arc` can consume. Generally, this * means examining the output and converting each warning or error into a * message. * * If parsing fails, returning `false` will cause the caller to throw an * appropriate exception. (You can also throw a more specific exception if * you're able to detect a more specific condition.) Otherwise, return a list * of messages. * * @param string Path to the file being linted. * @param int Exit code of the linter. * @param string Stdout of the linter. * @param string Stderr of the linter. * @return list|false List of lint messages, or false * to indicate parser failure. * @task parse */ abstract protected function parseLinterOutput($path, $err, $stdout, $stderr); /* -( Executing the Linter )----------------------------------------------- */ /** * Check that the binary and interpreter (if applicable) exist, and throw * an exception with a message about how to install them if they do not. * * @return void */ final public function checkBinaryConfiguration() { $interpreter = null; if ($this->shouldUseInterpreter()) { $interpreter = $this->getInterpreter(); } $binary = $this->getBinary(); // NOTE: If we have an interpreter, we don't require the script to be // executable (so we just check that the path exists). Otherwise, the // binary must be executable. if ($interpreter) { if (!Filesystem::binaryExists($interpreter)) { throw new ArcanistMissingLinterException( pht( 'Unable to locate interpreter "%s" to run linter %s. You may need '. 'to install the interpreter, or adjust your linter configuration.', $interpreter, get_class($this))); } if (!Filesystem::pathExists($binary)) { throw new ArcanistMissingLinterException( sprintf( "%s\n%s", pht( 'Unable to locate script "%s" to run linter %s. You may need '. 'to install the script, or adjust your linter configuration.', $binary, get_class($this)), pht( 'TO INSTALL: %s', $this->getInstallInstructions()))); } } else { if (!Filesystem::binaryExists($binary)) { throw new ArcanistMissingLinterException( sprintf( "%s\n%s", pht( 'Unable to locate binary "%s" to run linter %s. You may need '. 'to install the binary, or adjust your linter configuration.', $binary, get_class($this)), pht( 'TO INSTALL: %s', $this->getInstallInstructions()))); } } } /** * Get the composed executable command, including the interpreter and binary * but without flags or paths. This can be used to execute `--version` * commands. * * @return string Command to execute the raw linter. * @task exec */ final protected function getExecutableCommand() { $this->checkBinaryConfiguration(); $interpreter = null; if ($this->shouldUseInterpreter()) { $interpreter = $this->getInterpreter(); } $binary = $this->getBinary(); if ($interpreter) { $bin = csprintf('%s %s', $interpreter, $binary); } else { $bin = csprintf('%s', $binary); } return $bin; } /** * Get the composed flags for the executable, including both mandatory and * configured flags. * * @return list Composed flags. * @task exec */ final protected function getCommandFlags() { return array_merge( $this->getMandatoryFlags(), nonempty($this->flags, $this->getDefaultFlags())); } public function getCacheVersion() { try { $this->checkBinaryConfiguration(); } catch (ArcanistMissingLinterException $e) { return null; } $version = $this->getVersion(); if ($version) { return $version.'-'.json_encode($this->getCommandFlags()); } else { // Either we failed to parse the version number or the `getVersion` // function hasn't been implemented. return json_encode($this->getCommandFlags()); } } /** * Prepare the path to be added to the command string. * * This method is expected to return an already escaped string. * * @param string Path to the file being linted * @return string The command-ready file argument */ protected function getPathArgumentForLinterFuture($path) { return csprintf('%s', $path); } final protected function buildFutures(array $paths) { $executable = $this->getExecutableCommand(); $bin = csprintf('%C %Ls', $executable, $this->getCommandFlags()); $futures = array(); foreach ($paths as $path) { $disk_path = $this->getEngine()->getFilePathOnDisk($path); $path_argument = $this->getPathArgumentForLinterFuture($disk_path); $future = new ExecFuture('%C %C', $bin, $path_argument); $future->setCWD($this->getProjectRoot()); $futures[$path] = $future; } return $futures; } final protected function resolveFuture($path, Future $future) { list($err, $stdout, $stderr) = $future->resolve(); if ($err && !$this->shouldExpectCommandErrors()) { $future->resolvex(); } $messages = $this->parseLinterOutput($path, $err, $stdout, $stderr); + if ($err && $this->shouldExpectCommandErrors() && !$messages) { + // We assume that if the future exits with a non-zero status and we + // failed to parse any linter messages, then something must've gone wrong + // during parsing. + $messages = false; + } + if ($messages === false) { if ($err) { $future->resolvex(); } else { throw new Exception( sprintf( "%s\n\nSTDOUT\n%s\n\nSTDERR\n%s", pht('Linter failed to parse output!'), $stdout, $stderr)); } } foreach ($messages as $message) { $this->addLintMessage($message); } } public function getLinterConfigurationOptions() { $options = array( 'bin' => array( 'type' => 'optional string | list', 'help' => pht( 'Specify a string (or list of strings) identifying the binary '. 'which should be invoked to execute this linter. This overrides '. 'the default binary. If you provide a list of possible binaries, '. 'the first one which exists will be used.'), ), 'flags' => array( 'type' => 'optional list', 'help' => pht( 'Provide a list of additional flags to pass to the linter on the '. 'command line.'), ), ); if ($this->shouldUseInterpreter()) { $options['interpreter'] = array( 'type' => 'optional string | list', 'help' => pht( 'Specify a string (or list of strings) identifying the interpreter '. 'which should be used to invoke the linter binary. If you provide '. 'a list of possible interpreters, the first one that exists '. 'will be used.'), ); } return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'interpreter': $root = $this->getProjectRoot(); foreach ((array)$value as $path) { if (Filesystem::binaryExists($path)) { $this->setInterpreter($path); return; } $path = Filesystem::resolvePath($path, $root); if (Filesystem::binaryExists($path)) { $this->setInterpreter($path); return; } } throw new Exception( pht('None of the configured interpreters can be located.')); case 'bin': $is_script = $this->shouldUseInterpreter(); $root = $this->getProjectRoot(); foreach ((array)$value as $path) { if (!$is_script && Filesystem::binaryExists($path)) { $this->setBinary($path); return; } $path = Filesystem::resolvePath($path, $root); if ((!$is_script && Filesystem::binaryExists($path)) || ($is_script && Filesystem::pathExists($path))) { $this->setBinary($path); return; } } throw new Exception( pht('None of the configured binaries can be located.')); case 'flags': $this->setFlags($value); return; } return parent::setLinterConfigurationValue($key, $value); } /** * Map a configuration lint code to an `arc` lint code. Primarily, this is * intended for validation, but can also be used to normalize case or * otherwise be more permissive in accepted inputs. * * If the code is not recognized, you should throw an exception. * * @param string Code specified in configuration. * @return string Normalized code to use in severity map. */ protected function getLintCodeFromLinterConfigurationKey($code) { return $code; } } diff --git a/src/lint/linter/ArcanistFlake8Linter.php b/src/lint/linter/ArcanistFlake8Linter.php index dabc0b42..db8e9177 100644 --- a/src/lint/linter/ArcanistFlake8Linter.php +++ b/src/lint/linter/ArcanistFlake8Linter.php @@ -1,118 +1,114 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+(?:\.\d+)?)\b/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install flake8 using `%s`.', 'easy_install flake8'); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stdout, false); $messages = array(); foreach ($lines as $line) { $matches = null; // stdin:2: W802 undefined name 'foo' # pyflakes // stdin:3:1: E302 expected 2 blank lines, found 1 # pep8 $regexp = '/^(.*?):(\d+):(?:(\d+):)? (\S+) (.*)$/'; if (!preg_match($regexp, $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[2]); if (!empty($matches[3])) { $message->setChar($matches[3]); } $message->setCode($matches[4]); $message->setName($this->getLinterName().' '.$matches[3]); $message->setDescription($matches[5]); $message->setSeverity($this->getLintMessageSeverity($matches[4])); $messages[] = $message; } - if ($err && !$messages) { - return false; - } - return $messages; } protected function getDefaultMessageSeverity($code) { if (preg_match('/^C/', $code)) { // "C": Cyclomatic complexity return ArcanistLintSeverity::SEVERITY_ADVICE; } else if (preg_match('/^W/', $code)) { // "W": PEP8 Warning return ArcanistLintSeverity::SEVERITY_WARNING; } else { // "E": PEP8 Error // "F": PyFlakes Error return ArcanistLintSeverity::SEVERITY_ERROR; } } protected function getLintCodeFromLinterConfigurationKey($code) { if (!preg_match('/^(E|W|C|F)\d+$/', $code)) { throw new Exception( pht( 'Unrecognized lint message code "%s". Expected a valid flake8 '. 'lint code like "%s", or "%s", or "%s", or "%s".', $code, 'E225', 'W291', 'F811', 'C901')); } return $code; } } diff --git a/src/lint/linter/ArcanistJSONLintLinter.php b/src/lint/linter/ArcanistJSONLintLinter.php index 2ac0dbbe..d6b13103 100644 --- a/src/lint/linter/ArcanistJSONLintLinter.php +++ b/src/lint/linter/ArcanistJSONLintLinter.php @@ -1,89 +1,85 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install jsonlint using `%s`.', 'npm install -g jsonlint'); } protected function getMandatoryFlags() { return array( '--compact', ); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stderr, false); $messages = array(); foreach ($lines as $line) { $matches = null; $match = preg_match( '/^(?:(?.+): )?'. 'line (?\d+), col (?\d+), '. '(?.*)$/', $line, $matches); if ($match) { $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches['line']); $message->setChar($matches['column']); $message->setCode($this->getLinterName()); $message->setDescription(ucfirst($matches['description'])); $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); $messages[] = $message; } } - if ($err && !$messages) { - return false; - } - return $messages; } } diff --git a/src/lint/linter/ArcanistJscsLinter.php b/src/lint/linter/ArcanistJscsLinter.php index 6d8f2537..d1c0b3c4 100644 --- a/src/lint/linter/ArcanistJscsLinter.php +++ b/src/lint/linter/ArcanistJscsLinter.php @@ -1,152 +1,148 @@ getExecutableCommand()); $matches = array(); $regex = '/^(?P\d+\.\d+\.\d+)$/'; if (preg_match($regex, $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install JSCS using `%s`.', 'npm install -g jscs'); } protected function getMandatoryFlags() { $options = array(); $options[] = '--reporter=checkstyle'; $options[] = '--no-colors'; if ($this->config) { $options[] = '--config='.$this->config; } if ($this->preset) { $options[] = '--preset='.$this->preset; } return $options; } public function getLinterConfigurationOptions() { $options = array( 'jscs.config' => array( 'type' => 'optional string', 'help' => pht('Pass in a custom %s file path.', 'jscsrc'), ), 'jscs.preset' => array( 'type' => 'optional string', 'help' => pht('Custom preset.'), ), ); return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'jscs.config': $this->config = $value; return; case 'jscs.preset': $this->preset = $value; return; } return parent::setLinterConfigurationValue($key, $value); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $report_dom = new DOMDocument(); $ok = @$report_dom->loadXML($stdout); if (!$ok) { return false; } $messages = array(); foreach ($report_dom->getElementsByTagName('file') as $file) { foreach ($file->getElementsByTagName('error') as $error) { $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($error->getAttribute('line')); $message->setChar($error->getAttribute('column')); $message->setCode('JSCS'); $message->setDescription($error->getAttribute('message')); switch ($error->getAttribute('severity')) { case 'error': $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); break; case 'warning': $message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING); break; default: $message->setSeverity(ArcanistLintSeverity::SEVERITY_ADVICE); break; } $messages[] = $message; } } - if ($err && !$messages) { - return false; - } - return $messages; } protected function getLintCodeFromLinterConfigurationKey($code) { // NOTE: We can't figure out which rule generated each message, so we // can not customize severities. // // See https://github.com/mdevils/node-jscs/issues/224 throw new Exception( pht( "JSCS does not currently support custom severity levels, because ". "rules can't be identified from messages in output.")); } } diff --git a/src/lint/linter/ArcanistLesscLinter.php b/src/lint/linter/ArcanistLesscLinter.php index f51660e2..4032b1ed 100644 --- a/src/lint/linter/ArcanistLesscLinter.php +++ b/src/lint/linter/ArcanistLesscLinter.php @@ -1,187 +1,183 @@ array( 'type' => 'optional bool', 'help' => pht( 'Enable strict math, which only processes mathematical expressions '. 'inside extraneous parentheses.'), ), 'lessc.strict-units' => array( 'type' => 'optional bool', 'help' => pht('Enable strict handling of units in expressions.'), ), ); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'lessc.strict-math': $this->strictMath = $value; return; case 'lessc.strict-units': $this->strictUnits = $value; return; } return parent::setLinterConfigurationValue($key, $value); } public function getLintNameMap() { return array( self::LINT_RUNTIME_ERROR => pht('Runtime Error'), self::LINT_ARGUMENT_ERROR => pht('Argument Error'), self::LINT_FILE_ERROR => pht('File Error'), self::LINT_NAME_ERROR => pht('Name Error'), self::LINT_OPERATION_ERROR => pht('Operation Error'), self::LINT_PARSE_ERROR => pht('Parse Error'), self::LINT_SYNTAX_ERROR => pht('Syntax Error'), ); } public function getDefaultBinary() { return 'lessc'; } public function getVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); $regex = '/^lessc (?P\d+\.\d+\.\d+)\b/'; if (preg_match($regex, $stdout, $matches)) { $version = $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install lessc using `%s`.', 'npm install -g less'); } protected function getMandatoryFlags() { return array( '--lint', '--no-color', '--strict-math='.($this->strictMath ? 'on' : 'off'), '--strict-units='.($this->strictUnits ? 'on' : 'off'), ); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stderr, false); $messages = array(); foreach ($lines as $line) { $matches = null; $match = preg_match( '/^(?P\w+): (?P.+) '. 'in (?P.+|-) '. 'on line (?P\d+), column (?P\d+):$/', $line, $matches); if ($match) { switch ($matches['name']) { case 'RuntimeError': $code = self::LINT_RUNTIME_ERROR; break; case 'ArgumentError': $code = self::LINT_ARGUMENT_ERROR; break; case 'FileError': $code = self::LINT_FILE_ERROR; break; case 'NameError': $code = self::LINT_NAME_ERROR; break; case 'OperationError': $code = self::LINT_OPERATION_ERROR; break; case 'ParseError': $code = self::LINT_PARSE_ERROR; break; case 'SyntaxError': $code = self::LINT_SYNTAX_ERROR; break; default: throw new RuntimeException( pht( 'Unrecognized lint message code "%s".', $code)); } $code = $this->getLintCodeFromLinterConfigurationKey($matches['name']); $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches['line']); $message->setChar($matches['column']); $message->setCode($this->getLintMessageFullCode($code)); $message->setSeverity($this->getLintMessageSeverity($code)); $message->setName($this->getLintMessageName($code)); $message->setDescription(ucfirst($matches['description'])); $messages[] = $message; } } - if ($err && !$messages) { - return false; - } - return $messages; } } diff --git a/src/lint/linter/ArcanistPEP8Linter.php b/src/lint/linter/ArcanistPEP8Linter.php index a021ba83..f1cb21ee 100644 --- a/src/lint/linter/ArcanistPEP8Linter.php +++ b/src/lint/linter/ArcanistPEP8Linter.php @@ -1,102 +1,98 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+(?:\.\d+)?)\b/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install PEP8 using `%s`.', 'easy_install pep8'); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stdout, false); $messages = array(); foreach ($lines as $line) { $matches = null; if (!preg_match('/^(.*?):(\d+):(\d+): (\S+) (.*)$/', $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[2]); $message->setChar($matches[3]); $message->setCode($matches[4]); $message->setName('PEP8 '.$matches[4]); $message->setDescription($matches[5]); $message->setSeverity($this->getLintMessageSeverity($matches[4])); $messages[] = $message; } - if ($err && !$messages) { - return false; - } - return $messages; } protected function getDefaultMessageSeverity($code) { if (preg_match('/^W/', $code)) { return ArcanistLintSeverity::SEVERITY_WARNING; } else { return ArcanistLintSeverity::SEVERITY_ERROR; } } protected function getLintCodeFromLinterConfigurationKey($code) { if (!preg_match('/^(E|W)\d+$/', $code)) { throw new Exception( pht( 'Unrecognized lint message code "%s". Expected a valid PEP8 '. 'lint code like "%s" or "%s".', $code, 'E101', 'W291')); } return $code; } } diff --git a/src/lint/linter/ArcanistPuppetLintLinter.php b/src/lint/linter/ArcanistPuppetLintLinter.php index ea5b66cf..8336cd42 100644 --- a/src/lint/linter/ArcanistPuppetLintLinter.php +++ b/src/lint/linter/ArcanistPuppetLintLinter.php @@ -1,143 +1,139 @@ getExecutableCommand()); $matches = array(); $regex = '/^puppet-lint (?P\d+\.\d+\.\d+)$/'; if (preg_match($regex, $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht( 'Install puppet-lint using `%s`.', 'gem install puppet-lint'); } protected function getMandatoryFlags() { return array( '--error-level=all', sprintf('--log-format=%s', implode('|', array( '%{linenumber}', '%{column}', '%{kind}', '%{check}', '%{message}', ))), ); } public function getLinterConfigurationOptions() { $options = array( 'puppet-lint.config' => array( 'type' => 'optional string', 'help' => pht('Pass in a custom configuration file path.'), ), ); return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'puppet-lint.config': $this->config = $value; return; default: return parent::setLinterConfigurationValue($key, $value); } } protected function getDefaultFlags() { $options = array(); if ($this->config) { $options[] = '--config='.$this->config; } return $options; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stdout, false); $messages = array(); foreach ($lines as $line) { $matches = explode('|', $line, 5); if (count($matches) < 5) { continue; } $message = id(new ArcanistLintMessage()) ->setPath($path) ->setLine($matches[0]) ->setChar($matches[1]) ->setCode($this->getLinterName()) ->setName(ucwords(str_replace('_', ' ', $matches[3]))) ->setDescription(ucfirst($matches[4])); switch ($matches[2]) { case 'warning': $message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING); break; case 'error': $message->setSeverity(ArcanistLintSeverity::SEVERITY_ERROR); break; default: $message->setSeverity(ArcanistLintSeverity::SEVERITY_ADVICE); break; } $messages[] = $message; } - if ($err && !$messages) { - return false; - } - return $messages; } } diff --git a/src/lint/linter/ArcanistPyFlakesLinter.php b/src/lint/linter/ArcanistPyFlakesLinter.php index 11ede0c3..3a79e02e 100644 --- a/src/lint/linter/ArcanistPyFlakesLinter.php +++ b/src/lint/linter/ArcanistPyFlakesLinter.php @@ -1,91 +1,87 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install pyflakes with `%s`.', 'pip install pyflakes'); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stdout, false); $messages = array(); foreach ($lines as $line) { $matches = null; if (!preg_match('/^(.*?):(\d+): (.*)$/', $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } $severity = ArcanistLintSeverity::SEVERITY_WARNING; $description = $matches[3]; $error_regexp = '/(^undefined|^duplicate|before assignment$)/'; if (preg_match($error_regexp, $description)) { $severity = ArcanistLintSeverity::SEVERITY_ERROR; } $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[2]); $message->setCode($this->getLinterName()); $message->setDescription($description); $message->setSeverity($severity); $messages[] = $message; } - if ($err && !$messages) { - return false; - } - return $messages; } protected function canCustomizeLintSeverities() { return false; } } diff --git a/src/lint/linter/ArcanistPyLintLinter.php b/src/lint/linter/ArcanistPyLintLinter.php index 46be2f45..111cc27e 100644 --- a/src/lint/linter/ArcanistPyLintLinter.php +++ b/src/lint/linter/ArcanistPyLintLinter.php @@ -1,182 +1,178 @@ getExecutableCommand()); $matches = array(); $regex = '/^pylint (?P\d+\.\d+\.\d+),/'; if (preg_match($regex, $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht( 'Install PyLint using `%s`.', 'pip install pylint'); } public function shouldExpectCommandErrors() { return true; } public function getLinterConfigurationOptions() { $options = array( 'pylint.config' => array( 'type' => 'optional string', 'help' => pht('Pass in a custom configuration file path.'), ), ); return $options + parent::getLinterConfigurationOptions(); } public function setLinterConfigurationValue($key, $value) { switch ($key) { case 'pylint.config': $this->config = $value; return; default: return parent::setLinterConfigurationValue($key, $value); } } protected function getMandatoryFlags() { $options = array(); $options[] = '--reports=no'; $options[] = '--msg-template="{line}|{column}|{msg_id}|{symbol}|{msg}"'; // Specify an `--rcfile`, either absolute or relative to the project root. // Stupidly, the command line args above are overridden by rcfile, so be // careful. $config = $this->config; if ($config !== null) { $options[] = '--rcfile='.$config; } return $options; } protected function getDefaultFlags() { $options = array(); $installed_version = $this->getVersion(); $minimum_version = '1.0.0'; if (version_compare($installed_version, $minimum_version, '<')) { throw new ArcanistMissingLinterException( pht( '%s is not compatible with the installed version of pylint. '. 'Minimum version: %s; installed version: %s.', __CLASS__, $minimum_version, $installed_version)); } return $options; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { if ($err === 32) { // According to `man pylint` the exit status of 32 means there was a // usage error. That's bad, so actually exit abnormally. return false; } $lines = phutil_split_lines($stdout, false); $messages = array(); foreach ($lines as $line) { $matches = explode('|', $line, 5); if (count($matches) < 5) { continue; } $message = id(new ArcanistLintMessage()) ->setPath($path) ->setLine($matches[0]) ->setChar($matches[1]) ->setCode($matches[2]) ->setSeverity($this->getLintMessageSeverity($matches[2])) ->setName(ucwords(str_replace('-', ' ', $matches[3]))) ->setDescription($matches[4]); $messages[] = $message; } - if ($err && !$messages) { - return false; - } - return $messages; } protected function getDefaultMessageSeverity($code) { switch (substr($code, 0, 1)) { case 'R': case 'C': return ArcanistLintSeverity::SEVERITY_ADVICE; case 'W': return ArcanistLintSeverity::SEVERITY_WARNING; case 'E': case 'F': return ArcanistLintSeverity::SEVERITY_ERROR; default: return ArcanistLintSeverity::SEVERITY_DISABLED; } } protected function getLintCodeFromLinterConfigurationKey($code) { if (!preg_match('/^(R|C|W|E|F)\d{4}$/', $code)) { throw new Exception( pht( 'Unrecognized lint message code "%s". Expected a valid Pylint '. 'lint code like "%s", or "%s", or "%s".', $code, 'C0111', 'E0602', 'W0611')); } return $code; } } diff --git a/src/lint/linter/ArcanistRubyLinter.php b/src/lint/linter/ArcanistRubyLinter.php index 3a9845d4..bb29f381 100644 --- a/src/lint/linter/ArcanistRubyLinter.php +++ b/src/lint/linter/ArcanistRubyLinter.php @@ -1,91 +1,87 @@ getExecutableCommand()); $matches = array(); $regex = '/^ruby (?P\d+\.\d+\.\d+)p\d+/'; if (preg_match($regex, $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install `%s` from <%s>.', 'ruby', 'http://www.ruby-lang.org/'); } protected function getMandatoryFlags() { // -w: turn on warnings // -c: check syntax return array('-w', '-c'); } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stderr, false); $messages = array(); foreach ($lines as $line) { $matches = null; if (!preg_match('/(.*?):(\d+): (.*?)$/', $line, $matches)) { continue; } foreach ($matches as $key => $match) { $matches[$key] = trim($match); } $code = head(explode(',', $matches[3])); $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[2]); $message->setCode($this->getLinterName()); $message->setName(pht('Syntax Error')); $message->setDescription($matches[3]); $message->setSeverity($this->getLintMessageSeverity($code)); $messages[] = $message; } - if ($err && !$messages) { - return false; - } - return $messages; } }