diff --git a/src/lint/linter/ArcanistCSSLintLinter.php b/src/lint/linter/ArcanistCSSLintLinter.php index 78fcbe25..0eb1f0b8 100644 --- a/src/lint/linter/ArcanistCSSLintLinter.php +++ b/src/lint/linter/ArcanistCSSLintLinter.php @@ -1,113 +1,126 @@ getEngine()->getConfigurationManager(); return $config->getConfigFromAnySource('lint.csslint.options', array()); } public function getDefaultBinary() { // TODO: Deprecation warning. $config = $this->getEngine()->getConfigurationManager(); return $config->getConfigFromAnySource('lint.csslint.bin', 'csslint'); } public function getVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); if (preg_match('/^v(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install CSSLint using `npm install -g csslint`.'); } public function shouldExpectCommandErrors() { return true; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $report_dom = new DOMDocument(); $ok = @$report_dom->loadXML($stdout); if (!$ok) { return false; } $files = $report_dom->getElementsByTagName('file'); $messages = array(); foreach ($files as $file) { foreach ($file->childNodes as $child) { if (!($child instanceof DOMElement)) { continue; } $data = $this->getData($path); $lines = explode("\n", $data); $name = $child->getAttribute('reason'); $severity = ($child->getAttribute('severity') == 'warning') ? ArcanistLintSeverity::SEVERITY_WARNING : ArcanistLintSeverity::SEVERITY_ERROR; $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($child->getAttribute('line')); $message->setChar($child->getAttribute('char')); $message->setCode('CSSLint'); $message->setDescription($child->getAttribute('reason')); $message->setSeverity($severity); if ($child->hasAttribute('line') && $child->getAttribute('line') > 0) { $line = $lines[$child->getAttribute('line') - 1]; $text = substr($line, $child->getAttribute('char') - 1); $message->setOriginalText($text); } $messages[] = $message; } } return $messages; } protected function getLintCodeFromLinterConfigurationKey($code) { // NOTE: We can't figure out which rule generated each message, so we // can not customize severities. I opened a pull request to add this // ability; see: // // https://github.com/stubbornella/csslint/pull/409 throw new Exception( pht( "CSSLint does not currently support custom severity levels, because ". "rules can't be identified from messages in output. ". "See Pull Request #409.")); } } diff --git a/src/lint/linter/ArcanistFilenameLinter.php b/src/lint/linter/ArcanistFilenameLinter.php index 63de545c..40d63250 100644 --- a/src/lint/linter/ArcanistFilenameLinter.php +++ b/src/lint/linter/ArcanistFilenameLinter.php @@ -1,44 +1,52 @@ pht('Bad Filename'), ); } public function lintPath($path) { if (!preg_match('@^[a-z0-9./\\\\_-]+$@i', $path)) { $this->raiseLintAtPath( self::LINT_BAD_FILENAME, pht( 'Name files using only letters, numbers, period, hyphen and '. 'underscore.')); } } public function shouldLintDirectories() { return true; } } diff --git a/src/lint/linter/ArcanistFlake8Linter.php b/src/lint/linter/ArcanistFlake8Linter.php index 3fc905c3..cfac19c7 100644 --- a/src/lint/linter/ArcanistFlake8Linter.php +++ b/src/lint/linter/ArcanistFlake8Linter.php @@ -1,131 +1,143 @@ getEngine()->getConfigurationManager(); return $config->getConfigFromAnySource('lint.flake8.options', array()); } public function getDefaultBinary() { $config = $this->getEngine()->getConfigurationManager(); $prefix = $config->getConfigFromAnySource('lint.flake8.prefix'); $bin = $config->getConfigFromAnySource('lint.flake8.bin', 'flake8'); if ($prefix || ($bin != 'flake8')) { return $prefix.'/'.$bin; } return 'flake8'; } public function getVersion() { list($stdout) = execx('%C --version', $this->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 `easy_install flake8`.'); } public function supportsReadDataFromStdin() { return true; } public function getReadDataFromStdinFilename() { return '-'; } public function shouldExpectCommandErrors() { return true; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stdout, $retain_endings = 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/ArcanistGeneratedLinter.php b/src/lint/linter/ArcanistGeneratedLinter.php index 6efdfb15..7cf90450 100644 --- a/src/lint/linter/ArcanistGeneratedLinter.php +++ b/src/lint/linter/ArcanistGeneratedLinter.php @@ -1,29 +1,38 @@ getData($path); if (preg_match('/@'.'generated/', $data)) { $this->stopAllLinters(); } } } diff --git a/src/lint/linter/ArcanistJSHintLinter.php b/src/lint/linter/ArcanistJSHintLinter.php index 3c241517..ef6ec9db 100644 --- a/src/lint/linter/ArcanistJSHintLinter.php +++ b/src/lint/linter/ArcanistJSHintLinter.php @@ -1,128 +1,139 @@ getEngine()->getConfigurationManager(); $prefix = $config->getConfigFromAnySource('lint.jshint.prefix'); $bin = $config->getConfigFromAnySource('lint.jshint.bin', 'jshint'); if ($prefix) { return $prefix.'/'.$bin; } else { return $bin; } } public function getVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); $regex = '/^jshint v(?P\d+\.\d+\.\d+)$/'; if (preg_match($regex, $stdout, $matches)) { return $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install JSHint using `npm install -g jshint`.'); } public function shouldExpectCommandErrors() { return true; } public function supportsReadDataFromStdin() { return true; } public function getReadDataFromStdinFilename() { return '-'; } protected function getMandatoryFlags() { return array( '--reporter='.dirname(realpath(__FILE__)).'/reporter.js', ); } protected function getDefaultFlags() { $config_manager = $this->getEngine()->getConfigurationManager(); $options = $config_manager->getConfigFromAnySource( 'lint.jshint.options', array()); $config = $config_manager->getConfigFromAnySource('lint.jshint.config'); if ($config) { $options[] = '--config='.$config; } return $options; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $errors = json_decode($stdout, true); if (!is_array($errors)) { // Something went wrong and we can't decode the output. Exit abnormally. throw new ArcanistUsageException( "JSHint returned unparseable output.\n". "stdout:\n\n{$stdout}". "stderr:\n\n{$stderr}"); } $messages = array(); foreach ($errors as $err) { $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine(idx($err, 'line')); $message->setChar(idx($err, 'col')); $message->setCode(idx($err, 'code')); $message->setName('JSHint'.idx($err, 'code')); $message->setDescription(idx($err, 'reason')); $message->setSeverity($this->getLintMessageSeverity(idx($err, 'code'))); $message->setOriginalText(idx($err, 'evidence')); $messages[] = $message; } return $messages; } protected function getLintCodeFromLinterConfigurationKey($code) { if (!preg_match('/^(E|W)\d+$/', $code)) { throw new Exception( pht( 'Unrecognized lint message code "%s". Expected a valid JSHint '. 'lint code like "%s" or "%s".', $code, 'E033', 'W093')); } return $code; } } diff --git a/src/lint/linter/ArcanistJSONLintLinter.php b/src/lint/linter/ArcanistJSONLintLinter.php index 60276e0c..fd49b89c 100644 --- a/src/lint/linter/ArcanistJSONLintLinter.php +++ b/src/lint/linter/ArcanistJSONLintLinter.php @@ -1,83 +1,96 @@ getExecutableCommand()); $matches = array(); if (preg_match('/^(?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { $version = $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install jsonlint using `npm install -g jsonlint`.'); } public function shouldExpectCommandErrors() { return true; } public function supportsReadDataFromStdin() { return true; } 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/ArcanistLesscLinter.php b/src/lint/linter/ArcanistLesscLinter.php index 9b25f9c8..1315e371 100644 --- a/src/lint/linter/ArcanistLesscLinter.php +++ b/src/lint/linter/ArcanistLesscLinter.php @@ -1,160 +1,174 @@ 'optional bool', 'lessc.strict-units' => 'optional bool', ); } 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 getCacheVersion() { list($stdout) = execx('%C --version', $this->getExecutableCommand()); $matches = array(); if (preg_match('/^lessc (?P\d+\.\d+\.\d+)$/', $stdout, $matches)) { $version = $matches['version']; } else { return false; } } public function getInstallInstructions() { return pht('Install lessc using `npm install -g less`.'); } public function shouldExpectCommandErrors() { return true; } public function supportsReadDataFromStdin() { // Technically `lessc` can read data from standard input however, when doing // so, relative imports cannot be resolved. Therefore, this functionality is // disabled. return false; } public function getReadDataFromStdinFilename() { return '-'; } protected function getMandatoryFlags() { return array( '--lint', '--no-color', '--strict-math='. ($this->getConfig('lessc.strict-math') ? 'on' : 'off'), '--strict-units='. ($this->getConfig('lessc.strict-units') ? '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/ArcanistMergeConflictLinter.php b/src/lint/linter/ArcanistMergeConflictLinter.php index 1b4d8d68..8144bd18 100644 --- a/src/lint/linter/ArcanistMergeConflictLinter.php +++ b/src/lint/linter/ArcanistMergeConflictLinter.php @@ -1,50 +1,59 @@ getData($path), false); foreach ($lines as $lineno => $line) { // An unresolved merge conflict will contain a series of seven // '<', '=', or '>'. if (preg_match('/^(>{7}|<{7}|={7})$/', $line)) { $this->raiseLintAtLine( $lineno + 1, 0, self::LINT_MERGECONFLICT, - 'This syntax indicates there is an unresolved merge conflict.'); + pht('This syntax indicates there is an unresolved merge conflict.')); } } } public function getLintSeverityMap() { return array( self::LINT_MERGECONFLICT => ArcanistLintSeverity::SEVERITY_ERROR, ); } public function getLintNameMap() { return array( - self::LINT_MERGECONFLICT => 'Unresolved merge conflict', + self::LINT_MERGECONFLICT => pht('Unresolved merge conflict'), ); } } diff --git a/src/lint/linter/ArcanistPuppetLintLinter.php b/src/lint/linter/ArcanistPuppetLintLinter.php index 7dab3ed7..74f6458b 100644 --- a/src/lint/linter/ArcanistPuppetLintLinter.php +++ b/src/lint/linter/ArcanistPuppetLintLinter.php @@ -1,92 +1,106 @@ 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 `gem install puppet-lint`.'); } public function shouldExpectCommandErrors() { return true; } public function supportsReadDataFromStdin() { return false; } protected function getMandatoryFlags() { return array(sprintf('--log-format=%s', implode('|', array( '%{linenumber}', '%{column}', '%{kind}', '%{check}', '%{message}')))); } 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) { $message = new ArcanistLintMessage(); $message->setPath($path); $message->setLine($matches[0]); $message->setChar($matches[1]); $message->setName(ucwords(str_replace('_', ' ', $matches[3]))); $message->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 6ca517cb..7617bf16 100644 --- a/src/lint/linter/ArcanistPyFlakesLinter.php +++ b/src/lint/linter/ArcanistPyFlakesLinter.php @@ -1,91 +1,103 @@ getEngine()->getConfigurationManager(); $prefix = $config->getConfigFromAnySource('lint.pyflakes.prefix'); $bin = $config->getConfigFromAnySource('lint.pyflakes.bin', 'pyflakes'); if ($prefix) { return $prefix.'/'.$bin; } else { return $bin; } } public function getVersion() { list($stdout) = execx('%C --version', $this->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 `pip install pyflakes`.'); } public function shouldExpectCommandErrors() { return true; } public function supportsReadDataFromStdin() { return true; } 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; } } diff --git a/src/lint/linter/ArcanistRubyLinter.php b/src/lint/linter/ArcanistRubyLinter.php index 15ba1b72..739585d5 100644 --- a/src/lint/linter/ArcanistRubyLinter.php +++ b/src/lint/linter/ArcanistRubyLinter.php @@ -1,98 +1,108 @@ getEngine()->getConfigurationManager(); $prefix = $config->getConfigFromAnySource('lint.ruby.prefix'); if ($prefix !== null) { $ruby_bin = $prefix.'ruby'; } return 'ruby'; } public function getVersion() { list($stdout) = execx('%C --version', $this->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 `ruby` from .'); } public function supportsReadDataFromStdin() { return true; } public function shouldExpectCommandErrors() { return true; } protected function getMandatoryFlags() { // -w: turn on warnings // -c: check syntax return array('-w', '-c'); } protected function getDefaultMessageSeverity($code) { return ArcanistLintSeverity::SEVERITY_ERROR; } protected function parseLinterOutput($path, $err, $stdout, $stderr) { $lines = phutil_split_lines($stderr, $retain_endings = 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; } } diff --git a/src/lint/linter/ArcanistScriptAndRegexLinter.php b/src/lint/linter/ArcanistScriptAndRegexLinter.php index 978b32f2..da4ebe11 100644 --- a/src/lint/linter/ArcanistScriptAndRegexLinter.php +++ b/src/lint/linter/ArcanistScriptAndRegexLinter.php @@ -1,405 +1,416 @@ &1' * * The return code of the script must be 0, or an exception will be raised * reporting that the linter failed. If you have a script which exits nonzero * under normal circumstances, you can force it to always exit 0 by using a * configuration like this: * * sh -c '/opt/lint/lint.sh "$0" || true' * * Multiple instances of the script will be run in parallel if there are * multiple files to be linted, so they should not use any unique resources. * For instance, this configuration would not work properly, because several * processes may attempt to write to the file at the same time: * * COUNTEREXAMPLE * sh -c '/opt/lint/lint.sh --output /tmp/lint.out "$0" && cat /tmp/lint.out' * * There are necessary limits to how gracefully this linter can deal with * edge cases, because it is just a script and a regex. If you need to do * things that this linter can't handle, you can write a phutil linter and move * the logic to handle those cases into PHP. PHP is a better general-purpose * programming language than regular expressions are, if only by a small margin. * * == ...and Regex == * * The regex must be a valid PHP PCRE regex, including delimiters and flags. * * The regex will be matched against the entire output of the script, so it * should generally be in this form if messages are one-per-line: * * /^...$/m * * The regex should capture these named patterns with `(?P...)`: * * - `message` (required) Text describing the lint message. For example, * "This is a syntax error.". * - `name` (optional) Text summarizing the lint message. For example, * "Syntax Error". * - `severity` (optional) The word "error", "warning", "autofix", "advice", * or "disabled", in any combination of upper and lower case. Instead, you * may match groups called `error`, `warning`, `advice`, `autofix`, or * `disabled`. These allow you to match output formats like "E123" and * "W123" to indicate errors and warnings, even though the word "error" is * not present in the output. If no severity capturing group is present, * messages are raised with "error" severity. If multiple severity capturing * groups are present, messages are raised with the highest captured * serverity. Capturing groups like `error` supersede the `severity` * capturing group. * - `error` (optional) Match some nonempty substring to indicate that this * message has "error" severity. * - `warning` (optional) Match some nonempty substring to indicate that this * message has "warning" severity. * - `advice` (optional) Match some nonempty substring to indicate that this * message has "advice" severity. * - `autofix` (optional) Match some nonempty substring to indicate that this * message has "autofix" severity. * - `disabled` (optional) Match some nonempty substring to indicate that this * message has "disabled" severity. * - `file` (optional) The name of the file to raise the lint message in. If * not specified, defaults to the linted file. It is generally not necessary * to capture this unless the linter can raise messages in files other than * the one it is linting. * - `line` (optional) The line number of the message. * - `char` (optional) The character offset of the message. * - `offset` (optional) The byte offset of the message. If captured, this * supersedes `line` and `char`. * - `original` (optional) The text the message affects. * - `replacement` (optional) The text that the range captured by `original` * should be automatically replaced by to resolve the message. * - `code` (optional) A short error type identifier which can be used * elsewhere to configure handling of specific types of messages. For * example, "EXAMPLE1", "EXAMPLE2", etc., where each code identifies a * class of message like "syntax error", "missing whitespace", etc. This * allows configuration to later change the severity of all whitespace * messages, for example. * - `ignore` (optional) Match some nonempty substring to ignore the match. * You can use this if your linter sometimes emits text like "No lint * errors". * - `stop` (optional) Match some nonempty substring to stop processing input. * Remaining matches for this file will be discarded, but linting will * continue with other linters and other files. * - `halt` (optional) Match some nonempty substring to halt all linting of * this file by any linter. Linting will continue with other files. * - `throw` (optional) Match some nonempty substring to throw an error, which * will stop `arc` completely. You can use this to fail abruptly if you * encounter unexpected output. All processing will abort. * * Numbered capturing groups are ignored. * * For example, if your lint script's output looks like this: * * error:13 Too many goats! * warning:22 Not enough boats. * * ...you could use this regex to parse it: * * /^(?Pwarning|error):(?P\d+) (?P.*)$/m * * The simplest valid regex for line-oriented output is something like this: * * /^(?P.*)$/m * * @task lint Linting * @task linterinfo Linter Information * @task parse Parsing Output * @task config Validating Configuration * * @group linter */ final class ArcanistScriptAndRegexLinter extends ArcanistLinter { private $output = array(); + public function getInfoName() { + return pht('Script and Regex'); + } + + public function getInfoDescription() { + return pht( + 'Run an external script, then parse its output with a regular '. + 'expression. This is a generic binding that can be used to '. + 'run custom lint scripts.'); + } + /* -( Linting )------------------------------------------------------------ */ /** * Run the script on each file to be linted. * * @task lint */ public function willLintPaths(array $paths) { $script = $this->getConfiguredScript(); $root = $this->getEngine()->getWorkingCopy()->getProjectRoot(); $futures = array(); foreach ($paths as $path) { $future = new ExecFuture('%C %s', $script, $path); $future->setCWD($root); $futures[$path] = $future; } foreach (Futures($futures)->limit(4) as $path => $future) { list($stdout) = $future->resolvex(); $this->output[$path] = $stdout; } } /** * Run the regex on the output of the script. * * @task lint */ public function lintPath($path) { $regex = $this->getConfiguredRegex(); $output = idx($this->output, $path); if (!strlen($output)) { // No output, but it exited 0, so just move on. return; } $matches = null; if (!preg_match_all($regex, $output, $matches, PREG_SET_ORDER)) { // Output with no matches. This might be a configuration error, but more // likely it's something like "No lint errors." and the user just hasn't // written a sufficiently powerful/ridiculous regexp to capture it into an // 'ignore' group. Don't make them figure this out; advanced users can // capture 'throw' to handle this case. return; } foreach ($matches as $match) { if (!empty($match['throw'])) { $throw = $match['throw']; throw new ArcanistUsageException( "ArcanistScriptAndRegexLinter: ". "configuration captured a 'throw' named capturing group, ". "'{$throw}'. Script output:\n". $output); } if (!empty($match['halt'])) { $this->stopAllLinters(); break; } if (!empty($match['stop'])) { break; } if (!empty($match['ignore'])) { continue; } list($line, $char) = $this->getMatchLineAndChar($match, $path); $dict = array( 'path' => idx($match, 'file', $path), 'line' => $line, 'char' => $char, 'code' => idx($match, 'code', $this->getLinterName()), 'severity' => $this->getMatchSeverity($match), 'name' => idx($match, 'name', 'Lint'), 'description' => idx($match, 'message', 'Undefined Lint Message'), ); $original = idx($match, 'original'); if ($original !== null) { $dict['original'] = $original; } $replacement = idx($match, 'replacement'); if ($replacement !== null) { $dict['replacement'] = $replacement; } $lint = ArcanistLintMessage::newFromDictionary($dict); $this->addLintMessage($lint); } } /* -( Linter Information )------------------------------------------------- */ /** * Return the short name of the linter. * * @return string Short linter identifier. * * @task linterinfo */ public function getLinterName() { return 'S&RX'; } public function getLinterConfigurationName() { return 'script-and-regex'; } /* -( Parsing Output )----------------------------------------------------- */ /** * Get the line and character of the message from the regex match. * * @param dict Captured groups from regex. * @return pair Line and character of the message. * * @task parse */ private function getMatchLineAndChar(array $match, $path) { if (!empty($match['offset'])) { list($line, $char) = $this->getEngine()->getLineAndCharFromOffset( idx($match, 'file', $path), $match['offset']); return array($line + 1, $char + 1); } $line = idx($match, 'line', 1); $char = idx($match, 'char'); return array($line, $char); } /** * Map the regex matching groups to a message severity. We look for either * a nonempty severity name group like 'error', or a group called 'severity' * with a valid name. * * @param dict Captured groups from regex. * @return const @{class:ArcanistLintSeverity} constant. * * @task parse */ private function getMatchSeverity(array $match) { $map = array( 'error' => ArcanistLintSeverity::SEVERITY_ERROR, 'warning' => ArcanistLintSeverity::SEVERITY_WARNING, 'autofix' => ArcanistLintSeverity::SEVERITY_AUTOFIX, 'advice' => ArcanistLintSeverity::SEVERITY_ADVICE, 'disabled' => ArcanistLintSeverity::SEVERITY_DISABLED, ); $severity_name = strtolower(idx($match, 'severity')); foreach ($map as $name => $severity) { if (!empty($match[$name])) { return $severity; } if ($severity_name == $name) { return $severity; } } return ArcanistLintSeverity::SEVERITY_ERROR; } /* -( Validating Configuration )------------------------------------------- */ /** * Load, validate, and return the "script" configuration. * * @return string The shell command fragment to use to run the linter. * * @task config */ private function getConfiguredScript() { $key = 'linter.scriptandregex.script'; $config = $this->getEngine() ->getConfigurationManager() ->getConfigFromAnySource($key); if (!$config) { throw new ArcanistUsageException( "ArcanistScriptAndRegexLinter: ". "You must configure '{$key}' to point to a script to execute."); } // NOTE: No additional validation since the "script" can be some random // shell command and/or include flags, so it does not need to point to some // file on disk. return $config; } /** * Load, validate, and return the "regex" configuration. * * @return string A valid PHP PCRE regular expression. * * @task config */ private function getConfiguredRegex() { $key = 'linter.scriptandregex.regex'; $config = $this->getEngine() ->getConfigurationManager() ->getConfigFromAnySource($key); if (!$config) { throw new ArcanistUsageException( "ArcanistScriptAndRegexLinter: ". "You must configure '{$key}' with a valid PHP PCRE regex."); } // NOTE: preg_match() returns 0 for no matches and false for compile error; // this won't match, but will validate the syntax of the regex. $ok = preg_match($config, 'syntax-check'); if ($ok === false) { throw new ArcanistUsageException( "ArcanistScriptAndRegexLinter: ". "Regex '{$config}' does not compile. You must configure '{$key}' with ". "a valid PHP PCRE regex, including delimiters."); } return $config; } }