Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -98,6 +98,7 @@ 'ArcanistEventType' => 'events/constant/ArcanistEventType.php', 'ArcanistExitExpressionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExitExpressionXHPASTLinterRule.php', 'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php', + 'ArcanistExternalJsonLinter' => 'lint/linter/ArcanistExternalJsonLinter.php', 'ArcanistExternalLinter' => 'lint/linter/ArcanistExternalLinter.php', 'ArcanistExternalLinterTestCase' => 'lint/linter/__tests__/ArcanistExternalLinterTestCase.php', 'ArcanistExtractUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php', @@ -393,6 +394,7 @@ 'ArcanistEventType' => 'PhutilEventType', 'ArcanistExitExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistExportWorkflow' => 'ArcanistWorkflow', + 'ArcanistExternalJsonLinter' => 'ArcanistLinter', 'ArcanistExternalLinter' => 'ArcanistFutureLinter', 'ArcanistExternalLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', Index: src/lint/linter/ArcanistExternalJsonLinter.php =================================================================== --- /dev/null +++ src/lint/linter/ArcanistExternalJsonLinter.php @@ -0,0 +1,272 @@ +getProjectRoot(); + + $futures = array(); + foreach ($paths as $path) { + $future = new ExecFuture('%C %s', $this->script, $path); + $future->setCWD($root); + $futures[$path] = $future; + } + + $futures = id(new FutureIterator($futures)) + ->limit(4); + foreach ($futures 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) { + $output = idx($this->output, $path); + if (!strlen($output)) { + // No output, but it exited 0, so just move on. + return; + } + + $messages = json_decode($output, true); + + foreach ($messages as $message) { + if (!empty($message['throw'])) { + $throw = $message['throw']; + throw new ArcanistUsageException( + pht( + "%s: linter threw an exception: '%s'\n", + __CLASS__, + $throw)); + } + + $line = idx($message, 'line'); + if ($line) { + $line = (int)$line; + } else { + $line = null; + } + $char = idx($message, 'char'); + if ($char) { + $char = (int)$char; + } else { + $char = null; + } + + $dict = array( + 'path' => idx($message, 'file', $path), + 'line' => $line, + 'char' => $char, + 'code' => idx($message, 'code', $this->getLinterName()), + 'severity' => $this->getMessageSeverity($message), + 'name' => idx($message, 'name', 'Lint'), + 'description' => idx($message, 'message', + pht('Undefined Lint Message')), + ); + + $original = idx($message, 'original'); + if ($original !== null) { + $dict['original'] = $original; + } + + $replacement = idx($message, '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 'ExtJson'; + } + + public function getLinterConfigurationName() { + return 'external-json'; + } + + public function getLinterConfigurationOptions() { + // These fields are optional only to avoid breaking things. + $options = array( + 'external-json.script' => array( + 'type' => 'string', + 'help' => pht('Script to execute.'), + ), + ); + + return $options + parent::getLinterConfigurationOptions(); + } + + public function setLinterConfigurationValue($key, $value) { + switch ($key) { + case 'external-json.script': + $this->script = $value; + return; + } + + return parent::setLinterConfigurationValue($key, $value); + } + +/* -( Parsing Output )----------------------------------------------------- */ + + /** + * 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 message object + * @return const @{class:ArcanistLintSeverity} constant. + * + * @task parse + */ + private function getMessageSeverity(array $message) { + $map = array( + 'error' => ArcanistLintSeverity::SEVERITY_ERROR, + 'warning' => ArcanistLintSeverity::SEVERITY_WARNING, + 'autofix' => ArcanistLintSeverity::SEVERITY_AUTOFIX, + 'advice' => ArcanistLintSeverity::SEVERITY_ADVICE, + 'disabled' => ArcanistLintSeverity::SEVERITY_DISABLED, + ); + + if (idx($message, 'severity')) { + $severity_name = strtolower(idx($message, 'severity')); + if (!idx($map, $severity_name)) { + throw new ArcanistUsageException( + pht('%s: Unknown severity %s', __CLASS__, $severity_name)); + } else { + return $map[$severity_name]; + } + } else { + return ArcanistLintSeverity::SEVERITY_ERROR; + } + } + +}