diff --git a/resources/grep_linter.php b/resources/grep_linter.php new file mode 100755 --- /dev/null +++ b/resources/grep_linter.php @@ -0,0 +1,79 @@ +#!/usr/bin/env php +setTagline(pht('a grep linter')); +$args->setSynopsis(<<parseStandardArguments(); +$args->parse( + array( + array( + 'name' => 'advice', + 'param' => 'word', + 'help' => pht('TODO'), + ), + array( + 'name' => 'warning', + 'param' => 'word', + 'help' => pht('TODO'), + ), + array( + 'name' => 'error', + 'param' => 'word', + 'help' => pht('TODO'), + ), + array( + 'name' => 'path', + 'wildcard' => true, + ), + )); + +$paths = $args->getArg('path'); +if (!$paths) { + $args->printHelpAndExit(); +} + +$advice = $args->getArg('advice'); +$warning = $args->getArg('warning'); +$error = $args->getArg('error'); + +function get_regex($args, $severity) { + if (!$args->getArg($severity)) { + return null; + } + + return '/('.preg_quote(implode('|', explode(',', $args->getArg($severity)))).')/'; +} + +$regexes = array_filter(array( + 'advice' => get_regex($args, 'advice'), + 'warning' => get_regex($args, 'warning'), + 'error' => get_regex($args, 'error'), +)); + +foreach ($paths as $path) { + $data = Filesystem::readFile($path); + + foreach ($regexes as $severity => $regex) { + $matches = null; + + $preg = preg_match_all($regex, $data, $matches, PREG_OFFSET_CAPTURE); + + if (!$preg) { + continue; + } + + foreach ($matches[0] as $match) { + list($string, $offset) = $match; + + echo $severity.':'.$offset."\n"; + } + } +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -155,6 +155,7 @@ 'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php', 'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', + 'ArcanistScriptAndRegexLinterTestCase' => 'lint/linter/__tests__/ArcanistScriptAndRegexLinterTestCase.php', 'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php', 'ArcanistSettings' => 'configuration/ArcanistSettings.php', 'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php', @@ -332,7 +333,8 @@ 'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRubyLinter' => 'ArcanistExternalLinter', 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', - 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', + 'ArcanistScriptAndRegexLinter' => 'ArcanistExternalLinter', + 'ArcanistScriptAndRegexLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistSetConfigWorkflow' => 'ArcanistWorkflow', 'ArcanistShellCompleteWorkflow' => 'ArcanistWorkflow', 'ArcanistSingleLintEngine' => 'ArcanistLintEngine', diff --git a/src/lint/linter/ArcanistExternalLinter.php b/src/lint/linter/ArcanistExternalLinter.php --- a/src/lint/linter/ArcanistExternalLinter.php +++ b/src/lint/linter/ArcanistExternalLinter.php @@ -228,7 +228,7 @@ $interpreter, get_class($this))); } - if (!Filesystem::pathExists($binary)) { + if ($binary && !Filesystem::pathExists($binary)) { throw new ArcanistMissingLinterException( sprintf( "%s\n%s", @@ -241,21 +241,19 @@ '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()))); + } else if ($binary && !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()))); } - } } /** diff --git a/src/lint/linter/ArcanistScriptAndRegexLinter.php b/src/lint/linter/ArcanistScriptAndRegexLinter.php --- a/src/lint/linter/ArcanistScriptAndRegexLinter.php +++ b/src/lint/linter/ArcanistScriptAndRegexLinter.php @@ -2,17 +2,19 @@ /** * Simple glue linter which runs some script on each path, and then uses a - * regex to parse lint messages from the script's output. (This linter uses a - * script and a regex to interpret the results of some real linter, it does - * not itself lint both scripts and regexes). + * regular expression to parse lint messages from the script's output. (This + * linter uses a script and a regex to interpret the results of some real + * linter, it does not itself lint both scripts and regular expressions). * - * Configure this linter by setting these keys in your .arclint section: + * Configure this linter by setting these keys in your `.arclint` + * configuration: * - * - `script-and-regex.script` Script command to run. This can be - * the path to a linter script, but may also include flags or use shell - * features (see below for examples). - * - `script-and-regex.regex` The regex to process output with. This - * regex uses named capturing groups (detailed below) to interpret output. + * - `script-and-regex.script` Script command to run. This can be the path to + * a linter script, but may also include flags or use shell features + * (see below for examples). + * - `script-and-regex.regex` The regular expression to process output with. + * This regex uses named capturing groups (detailed below) to interpret + * output. * * The script will be invoked from the project root, so you can specify a * relative path like `scripts/lint.sh` or an absolute path like @@ -22,22 +24,22 @@ * linter which can perform custom processing, but may be somewhat simpler to * configure. * - * == Script... == + * == Script == * * The script will be invoked once for each file that is to be linted, with - * the file passed as the first argument. The file may begin with a "-"; ensure + * the file passed as the first argument. The file may begin with a `-`; ensure * your script will not interpret such files as flags (perhaps by ending your - * script configuration with "--", if its argument parser supports that). + * script configuration with `--`, if its argument parser supports that). * * Note that when run via `arc diff`, the list of files to be linted includes - * deleted files and files that were moved away by the change. The linter should - * not assume the path it is given exists, and it is not an error for the - * linter to be invoked with paths which are no longer there. (Every affected - * path is subject to lint because some linters may raise errors in other files - * when a file is removed, or raise an error about its removal.) + * deleted files and files that were moved away by the change. The linter + * should not assume the path it is given exists, and it is not an error for + * the linter to be invoked with paths which are no longer there. (Every + * affected path is subject to lint because some linters may raise errors in + * other files when a file is removed, or raise an error about its removal.) * * The script should emit lint messages to stdout, which will be parsed with - * the provided regex. + * the provided regular expression. * * For example, you might use a configuration like this: * @@ -49,7 +51,7 @@ * sh -c '/opt/lint/lint.sh "$0" 2>&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 + * reporting that the linter failed. If you have a script which exits non-zero * under normal circumstances, you can force it to always exit 0 by using a * configuration like this: * @@ -64,12 +66,13 @@ * 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. + * edge cases, because it is just a script and a regular expression. 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 == + * == Regex == * * The regex must be a valid PHP PCRE regex, including delimiters and flags. * @@ -86,14 +89,14 @@ * "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 + * 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 - * severity. Capturing groups like `error` supersede the `severity` - * capturing group. + * messages are raised with "error" severity. If multiple severity + * capturing groups are present, messages are raised with the highest + * captured severity. 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 @@ -102,12 +105,12 @@ * 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. + * - `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. + * 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 @@ -124,14 +127,14 @@ * - `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. + * - `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. + * - `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. * @@ -140,11 +143,12 @@ * error:13 Too many goats! * warning:22 Not enough boats. * - * ...you could use this regex to parse it: + * ...you could use this regular expression to parse it: * * /^(?Pwarning|error):(?P\d+) (?P.*)$/m * - * The simplest valid regex for line-oriented output is something like this: + * The simplest valid regular expression for line-oriented output is something + * like this: * * /^(?P.*)$/m * @@ -153,11 +157,24 @@ * @task parse Parsing Output * @task config Validating Configuration */ -final class ArcanistScriptAndRegexLinter extends ArcanistLinter { +final class ArcanistScriptAndRegexLinter extends ArcanistExternalLinter { private $script = null; private $regex = null; - private $output = array(); + + private $shouldExpectCommandErrors = null; + private $shouldLintBinaryFiles = null; + private $shouldLintDeletedFiles = null; + private $shouldLintDirectories = null; + private $shouldLintSymbolicLinks = null; + + public function getLinterName() { + return 'S&RX'; + } + + public function getLinterConfigurationName() { + return 'script-and-regex'; + } public function getInfoName() { return pht('Script and Regex'); @@ -170,38 +187,122 @@ 'run custom lint scripts.'); } -/* -( Linting )------------------------------------------------------------ */ + public function getLinterConfigurationOptions() { + $options = array( + 'script-and-regex.script' => array( + 'type' => 'string', + 'help' => pht('Script to execute.'), + ), + 'script-and-regex.regex' => array( + 'type' => 'regex', + 'help' => pht('The regex to process output with.'), + ), + 'script-and-regex.should-expect-command-errors' => array( + 'type' => 'optional bool', + 'help' => pht('Should expect command errors.'), + ), + 'script-and-regex.should-lint-binary-files' => array( + 'type' => 'optional bool', + 'help' => pht('Should binary files be linted.'), + ), + 'script-and-regex.should-lint-deleted-files' => array( + 'type' => 'optional bool', + 'help' => pht('Should deleted files be linted.'), + ), + 'script-and-regex.should-lint-directories' => array( + 'type' => 'optional bool', + 'help' => pht('Should directories be linted.'), + ), + 'script-and-regex.should-lint-symbolic-links' => array( + 'type' => 'optional bool', + 'help' => pht('Should symbolic links be linted.'), + ), + ); - /** - * Run the script on each file to be linted. - * - * @task lint - */ - public function willLintPaths(array $paths) { - $root = $this->getProjectRoot(); - - $futures = array(); - foreach ($paths as $path) { - $future = new ExecFuture('%C %s', $this->script, $path); - $future->setCWD($root); - $futures[$path] = $future; + return $options + parent::getLinterConfigurationOptions(); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'script-and-regex.script': + $this->script = $value; + return; + case 'script-and-regex.regex': + $this->regex = $value; + return; + + case 'script-and-regex.should-expect-command-errors': + $this->shouldExpectCommandErrors = $value; + return; + case 'script-and-regex.should-lint-binary-files': + $this->shouldLintBinaryFiles = $value; + return; + case 'script-and-regex.should-lint-deleted-files': + $this->shouldLintDeletedFiles = $value; + return; + case 'script-and-regex.should-lint-directories': + $this->shouldLintDirectories = $value; + return; + case 'script-and-regex.should-lint-symbolic-links': + $this->shouldLintSymbolicLinks = $value; + return; + + default: + return parent::setLinterConfigurationValue($key, $value); } + } + + public function getDefaultBinary() { + return null; + } + + public function getInstallInstructions() { + // TODO + return null; + } - $futures = id(new FutureIterator($futures)) - ->limit(4); - foreach ($futures as $path => $future) { - list($stdout) = $future->resolvex(); - $this->output[$path] = $stdout; + public function shouldExpectCommandErrors() { + if ($this->shouldExpectCommandErrors === null) { + return parent::shouldExpectCommandErrors(); } + + return $this->shouldExpectCommandErrors; } - /** - * Run the regex on the output of the script. - * - * @task lint - */ - public function lintPath($path) { + protected function shouldLintBinaryFiles() { + if ($this->shouldLintBinaryFiles === null) { + return parent::shouldLintBinaryFiles(); + } + + return $this->shouldLintBinaryFiles; + } + + protected function shouldLintDeletedFiles() { + if ($this->shouldLintDeletedFiles === null) { + return parent::shouldLintDeletedFiles(); + } + + return $this->shouldLintDeletedFiles; + } + + protected function shouldLintDirectories() { + if ($this->shouldLintDirectories === null) { + return parent::shouldLintDirectories(); + } + + return $this->shouldLintDirectories; + } + + protected function shouldLintSymbolicLinks() { + if ($this->shouldLintSymbolicLinks === null) { + return parent::shouldLintSymbolicLinks(); + } + + return $this->shouldLintSymbolicLinks; + } + + protected function parseLinterOutput($path, $err, $stdout, $stderr) { $output = idx($this->output, $path); if (!strlen($output)) { // No output, but it exited 0, so just move on. @@ -271,55 +372,6 @@ } } - -/* -( 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'; - } - - public function getLinterConfigurationOptions() { - // These fields are optional only to avoid breaking things. - $options = array( - 'script-and-regex.script' => array( - 'type' => 'string', - 'help' => pht('Script to execute.'), - ), - 'script-and-regex.regex' => array( - 'type' => 'regex', - 'help' => pht('The regex to process output with.'), - ), - ); - - return $options + parent::getLinterConfigurationOptions(); - } - - public function setLinterConfigurationValue($key, $value) { - switch ($key) { - case 'script-and-regex.script': - $this->script = $value; - return; - case 'script-and-regex.regex': - $this->regex = $value; - return; - } - - return parent::setLinterConfigurationValue($key, $value); - } - -/* -( Parsing Output )----------------------------------------------------- */ - /** * Get the line and character of the message from the regex match. * @@ -347,7 +399,7 @@ * a nonempty severity name group like 'error', or a group called 'severity' * with a valid name. * - * @param dict Captured groups from regex. + * @param dict Captured groups from regex. * @return const @{class:ArcanistLintSeverity} constant. * * @task parse diff --git a/src/lint/linter/__tests__/ArcanistScriptAndRegexLinterTestCase.php b/src/lint/linter/__tests__/ArcanistScriptAndRegexLinterTestCase.php new file mode 100644 --- /dev/null +++ b/src/lint/linter/__tests__/ArcanistScriptAndRegexLinterTestCase.php @@ -0,0 +1,10 @@ +executeTestsInDirectory(dirname(__FILE__).'/script-and-regex/'); + } + +} diff --git a/src/lint/linter/__tests__/script-and-regex/goats.lint-test b/src/lint/linter/__tests__/script-and-regex/goats.lint-test new file mode 100644 --- /dev/null +++ b/src/lint/linter/__tests__/script-and-regex/goats.lint-test @@ -0,0 +1,5 @@ +#!/bin/bash +~~~~~~~~~~ +~~~~~~~~~~ +~~~~~~~~~~ +{"mode": "0755"}