diff --git a/src/lint/engine/ArcanistLintEngine.php b/src/lint/engine/ArcanistLintEngine.php index ee80e6c2..f737fcd4 100644 --- a/src/lint/engine/ArcanistLintEngine.php +++ b/src/lint/engine/ArcanistLintEngine.php @@ -1,628 +1,618 @@ configurationManager = $configuration_manager; return $this; } final public function getConfigurationManager() { return $this->configurationManager; } final public function setWorkingCopy( ArcanistWorkingCopyIdentity $working_copy) { $this->workingCopy = $working_copy; return $this; } final public function getWorkingCopy() { return $this->workingCopy; } final public function setPaths($paths) { $this->paths = $paths; return $this; } public function getPaths() { return $this->paths; } final public function setPathChangedLines($path, $changed) { if ($changed === null) { $this->changedLines[$path] = null; } else { $this->changedLines[$path] = array_fill_keys($changed, true); } return $this; } final public function getPathChangedLines($path) { return idx($this->changedLines, $path); } final public function setFileData($data) { $this->fileData = $data + $this->fileData; return $this; } - final public function setEnableAsyncLint($enable_async_lint) { - $this->enableAsyncLint = $enable_async_lint; - return $this; - } - - final public function getEnableAsyncLint() { - return $this->enableAsyncLint; - } - final public function loadData($path) { if (!isset($this->fileData[$path])) { $disk_path = $this->getFilePathOnDisk($path); $this->fileData[$path] = Filesystem::readFile($disk_path); } return $this->fileData[$path]; } public function pathExists($path) { $disk_path = $this->getFilePathOnDisk($path); return Filesystem::pathExists($disk_path); } final public function isDirectory($path) { $disk_path = $this->getFilePathOnDisk($path); return is_dir($disk_path); } final public function isBinaryFile($path) { try { $data = $this->loadData($path); } catch (Exception $ex) { return false; } return ArcanistDiffUtils::isHeuristicBinaryFile($data); } final public function isSymbolicLink($path) { return is_link($this->getFilePathOnDisk($path)); } final public function getFilePathOnDisk($path) { return Filesystem::resolvePath( $path, $this->getWorkingCopy()->getProjectRoot()); } final public function setMinimumSeverity($severity) { $this->minimumSeverity = $severity; return $this; } final public function run() { $linters = $this->buildLinters(); if (!$linters) { throw new ArcanistNoEffectException(pht('No linters to run.')); } foreach ($linters as $key => $linter) { $linter->setLinterID($key); } $linters = msort($linters, 'getLinterPriority'); foreach ($linters as $linter) { $linter->setEngine($this); } $have_paths = false; foreach ($linters as $linter) { if ($linter->getPaths()) { $have_paths = true; break; } } if (!$have_paths) { throw new ArcanistNoEffectException(pht('No paths are lintable.')); } $versions = array($this->getCacheVersion()); foreach ($linters as $linter) { $version = get_class($linter).':'.$linter->getCacheVersion(); $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setName(get_class($linter)) ->selectSymbolsWithoutLoading(); $symbol = idx($symbols, 'class$'.get_class($linter)); if ($symbol) { $version .= ':'.md5_file( phutil_get_library_root($symbol['library']).'/'.$symbol['where']); } $versions[] = $version; } $this->cacheVersion = crc32(implode("\n", $versions)); $runnable = $this->getRunnableLinters($linters); $this->stopped = array(); $exceptions = $this->executeLinters($runnable); foreach ($runnable as $linter) { foreach ($linter->getLintMessages() as $message) { $this->validateLintMessage($linter, $message); if (!$this->isSeverityEnabled($message->getSeverity())) { continue; } if (!$this->isRelevantMessage($message)) { continue; } $message->setGranularity($linter->getCacheGranularity()); $result = $this->getResultForPath($message->getPath()); $result->addMessage($message); } } if ($this->cachedResults) { foreach ($this->cachedResults as $path => $messages) { $messages = idx($messages, $this->cacheVersion, array()); $repository_version = idx($messages, 'repository_version'); unset($messages['stopped']); unset($messages['repository_version']); foreach ($messages as $message) { $use_cache = $this->shouldUseCache( idx($message, 'granularity'), $repository_version); if ($use_cache) { $this->getResultForPath($path)->addMessage( ArcanistLintMessage::newFromDictionary($message)); } } } } foreach ($this->results as $path => $result) { $disk_path = $this->getFilePathOnDisk($path); $result->setFilePathOnDisk($disk_path); if (isset($this->fileData[$path])) { $result->setData($this->fileData[$path]); } else if ($disk_path && Filesystem::pathExists($disk_path)) { // TODO: this may cause us to, e.g., load a large binary when we only // raised an error about its filename. We could refine this by looking // through the lint messages and doing this load only if any of them // have original/replacement text or something like that. try { $this->fileData[$path] = Filesystem::readFile($disk_path); $result->setData($this->fileData[$path]); } catch (FilesystemException $ex) { // Ignore this, it's noncritical that we access this data and it // might be unreadable or a directory or whatever else for plenty // of legitimate reasons. } } } if ($exceptions) { throw new PhutilAggregateException( pht('Some linters failed:'), $exceptions); } return $this->results; } final public function isSeverityEnabled($severity) { $minimum = $this->minimumSeverity; return ArcanistLintSeverity::isAtLeastAsSevere($severity, $minimum); } final private function shouldUseCache( $cache_granularity, $repository_version) { switch ($cache_granularity) { case ArcanistLinter::GRANULARITY_FILE: return true; case ArcanistLinter::GRANULARITY_DIRECTORY: case ArcanistLinter::GRANULARITY_REPOSITORY: return ($this->repositoryVersion == $repository_version); default: return false; } } /** * @param dict>> * @return this */ final public function setCachedResults(array $results) { $this->cachedResults = $results; return $this; } final public function getResults() { return $this->results; } final public function getStoppedPaths() { return $this->stopped; } abstract public function buildLinters(); final public function setRepositoryVersion($version) { $this->repositoryVersion = $version; return $this; } final private function isRelevantMessage(ArcanistLintMessage $message) { // When a user runs "arc lint", we default to raising only warnings on // lines they have changed (errors are still raised anywhere in the // file). The list of $changed lines may be null, to indicate that the // path is a directory or a binary file so we should not exclude // warnings. if (!$this->changedLines || $message->isError() || $message->shouldBypassChangedLineFiltering()) { return true; } $locations = $message->getOtherLocations(); $locations[] = $message->toDictionary(); foreach ($locations as $location) { $path = idx($location, 'path', $message->getPath()); if (!array_key_exists($path, $this->changedLines)) { if (phutil_is_windows()) { // We try checking the UNIX path form as well, on Windows. Linters // store noramlized paths, which use the Windows-style "\" as a // delimiter; as such, they don't match the UNIX-style paths stored // in changedLines, which come from the VCS. $path = str_replace('\\', '/', $path); if (!array_key_exists($path, $this->changedLines)) { continue; } } else { continue; } } $changed = $this->getPathChangedLines($path); if ($changed === null || !$location['line']) { return true; } $last_line = $location['line']; if (isset($location['original'])) { $last_line += substr_count($location['original'], "\n"); } for ($l = $location['line']; $l <= $last_line; $l++) { if (!empty($changed[$l])) { return true; } } } return false; } final protected function getResultForPath($path) { if (empty($this->results[$path])) { $result = new ArcanistLintResult(); $result->setPath($path); $result->setCacheVersion($this->cacheVersion); $this->results[$path] = $result; } return $this->results[$path]; } final public function getLineAndCharFromOffset($path, $offset) { if (!isset($this->charToLine[$path])) { $char_to_line = array(); $line_to_first_char = array(); $lines = explode("\n", $this->loadData($path)); $line_number = 0; $line_start = 0; foreach ($lines as $line) { $len = strlen($line) + 1; // Account for "\n". $line_to_first_char[] = $line_start; $line_start += $len; for ($ii = 0; $ii < $len; $ii++) { $char_to_line[] = $line_number; } $line_number++; } $this->charToLine[$path] = $char_to_line; $this->lineToFirstChar[$path] = $line_to_first_char; } $line = $this->charToLine[$path][$offset]; $char = $offset - $this->lineToFirstChar[$path][$line]; return array($line, $char); } protected function getCacheVersion() { return 1; } /** * Get a named linter resource shared by another linter. * * This mechanism allows linters to share arbitrary resources, like the * results of computation. If several linters need to perform the same * expensive computation step, they can use a named resource to synchronize * construction of the result so it doesn't need to be built multiple * times. * * @param string Resource identifier. * @param wild Optionally, default value to return if resource does not * exist. * @return wild Resource, or default value if not present. */ public function getLinterResource($key, $default = null) { return idx($this->linterResources, $key, $default); } /** * Set a linter resource that other linters can access. * * See @{method:getLinterResource} for a description of this mechanism. * * @param string Resource identifier. * @param wild Resource. * @return this */ public function setLinterResource($key, $value) { $this->linterResources[$key] = $value; return $this; } private function getRunnableLinters(array $linters) { assert_instances_of($linters, 'ArcanistLinter'); // TODO: The canRun() mechanism is only used by one linter, and just // silently disables the linter. Almost every other linter handles this // by throwing `ArcanistMissingLinterException`. Both mechanisms are not // ideal; linters which can not run should emit a message, get marked as // "skipped", and allow execution to continue. See T7045. $runnable = array(); foreach ($linters as $key => $linter) { if ($linter->canRun()) { $runnable[$key] = $linter; } } return $runnable; } private function executeLinters(array $runnable) { assert_instances_of($runnable, 'ArcanistLinter'); $all_paths = $this->getPaths(); $path_chunks = array_chunk($all_paths, 32, $preserve_keys = true); $exception_lists = array(); foreach ($path_chunks as $chunk) { $exception_lists[] = $this->executeLintersOnChunk($runnable, $chunk); } return array_mergev($exception_lists); } private function executeLintersOnChunk(array $runnable, array $path_list) { assert_instances_of($runnable, 'ArcanistLinter'); $path_map = array_fuse($path_list); $exceptions = array(); $did_lint = array(); foreach ($runnable as $linter) { $linter_id = $linter->getLinterID(); $paths = $linter->getPaths(); foreach ($paths as $key => $path) { // If we aren't running this path in the current chunk of paths, // skip it completely. if (empty($path_map[$path])) { unset($paths[$key]); continue; } // Make sure each path has a result generated, even if it is empty // (i.e., the file has no lint messages). $result = $this->getResultForPath($path); // If a linter has stopped all other linters for this path, don't // actually run the linter. if (isset($this->stopped[$path])) { unset($paths[$key]); continue; } // If we have a cached result for this path, don't actually run the // linter. if (isset($this->cachedResults[$path][$this->cacheVersion])) { $cached_result = $this->cachedResults[$path][$this->cacheVersion]; $use_cache = $this->shouldUseCache( $linter->getCacheGranularity(), idx($cached_result, 'repository_version')); if ($use_cache) { unset($paths[$key]); if (idx($cached_result, 'stopped') == $linter_id) { $this->stopped[$path] = $linter_id; } } } } $paths = array_values($paths); if (!$paths) { continue; } try { $this->executeLinterOnPaths($linter, $paths); $did_lint[] = array($linter, $paths); } catch (Exception $ex) { $exceptions[] = $ex; } } foreach ($did_lint as $info) { list($linter, $paths) = $info; try { $this->executeDidLintOnPaths($linter, $paths); } catch (Exception $ex) { $exceptions[] = $ex; } } return $exceptions; } private function beginLintServiceCall(ArcanistLinter $linter, array $paths) { $profiler = PhutilServiceProfiler::getInstance(); return $profiler->beginServiceCall( array( 'type' => 'lint', 'linter' => $linter->getInfoName(), 'paths' => $paths, )); } private function endLintServiceCall($call_id) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall($call_id, array()); } private function executeLinterOnPaths(ArcanistLinter $linter, array $paths) { $call_id = $this->beginLintServiceCall($linter, $paths); try { $linter->willLintPaths($paths); foreach ($paths as $path) { $linter->setActivePath($path); $linter->lintPath($path); if ($linter->didStopAllLinters()) { $this->stopped[$path] = $linter->getLinterID(); } } } catch (Exception $ex) { $this->endLintServiceCall($call_id); throw $ex; } $this->endLintServiceCall($call_id); } private function executeDidLintOnPaths(ArcanistLinter $linter, array $paths) { $call_id = $this->beginLintServiceCall($linter, $paths); try { $linter->didLintPaths($paths); } catch (Exception $ex) { $this->endLintServiceCall($call_id); throw $ex; } $this->endLintServiceCall($call_id); } private function validateLintMessage( ArcanistLinter $linter, ArcanistLintMessage $message) { $name = $message->getName(); if (!strlen($name)) { throw new Exception( pht( 'Linter "%s" generated a lint message that is invalid because it '. 'does not have a name. Lint messages must have a name.', get_class($linter))); } } } diff --git a/src/lint/renderer/ArcanistCheckstyleXMLLintRenderer.php b/src/lint/renderer/ArcanistCheckstyleXMLLintRenderer.php index 32091bf5..7e43ec10 100644 --- a/src/lint/renderer/ArcanistCheckstyleXMLLintRenderer.php +++ b/src/lint/renderer/ArcanistCheckstyleXMLLintRenderer.php @@ -1,70 +1,65 @@ writer = new XMLWriter(); $this->writer->openMemory(); $this->writer->setIndent(true); $this->writer->setIndentString(' '); } - public function renderPreamble() { + public function willRenderResults() { $this->writer->startDocument('1.0', 'UTF-8'); $this->writer->startElement('checkstyle'); $this->writer->writeAttribute('version', '4.3'); - return $this->writer->flush(); + $this->writeOut($this->writer->flush()); } public function renderLintResult(ArcanistLintResult $result) { $this->writer->startElement('file'); $this->writer->writeAttribute('name', $result->getPath()); foreach ($result->getMessages() as $message) { $this->writer->startElement('error'); $this->writer->writeAttribute('line', $message->getLine()); $this->writer->writeAttribute('column', $message->getChar()); $this->writer->writeAttribute('severity', $this->getStringForSeverity($message->getSeverity())); $this->writer->writeAttribute('message', $message->getDescription()); $this->writer->writeAttribute('source', $message->getCode()); $this->writer->endElement(); } $this->writer->endElement(); - return $this->writer->flush(); - } - - public function renderOkayResult() { - return ''; + $this->writeOut($this->writer->flush()); } - public function renderPostamble() { + public function didRenderResults() { $this->writer->endElement(); $this->writer->endDocument(); - return $this->writer->flush(); + $this->writeOut($this->writer->flush()); } private function getStringForSeverity($severity) { switch ($severity) { case ArcanistLintSeverity::SEVERITY_ADVICE: return 'info'; case ArcanistLintSeverity::SEVERITY_AUTOFIX: return 'info'; case ArcanistLintSeverity::SEVERITY_WARNING: return 'warning'; case ArcanistLintSeverity::SEVERITY_ERROR: return 'error'; case ArcanistLintSeverity::SEVERITY_DISABLED: return 'ignore'; } } } diff --git a/src/lint/renderer/ArcanistCompilerLintRenderer.php b/src/lint/renderer/ArcanistCompilerLintRenderer.php index 264750b1..947274d4 100644 --- a/src/lint/renderer/ArcanistCompilerLintRenderer.php +++ b/src/lint/renderer/ArcanistCompilerLintRenderer.php @@ -1,35 +1,30 @@ getMessages(); $path = $result->getPath(); foreach ($messages as $message) { $severity = ArcanistLintSeverity::getStringForSeverity( $message->getSeverity()); $line = $message->getLine(); $code = $message->getCode(); $description = $message->getDescription(); $lines[] = sprintf( "%s:%d:%s (%s) %s\n", $path, $line, $severity, $code, $description); } - return implode('', $lines); - } - - public function renderOkayResult() { - return ''; + $this->writeOut(implode('', $lines)); } } diff --git a/src/lint/renderer/ArcanistConsoleLintRenderer.php b/src/lint/renderer/ArcanistConsoleLintRenderer.php index 4862f47f..5b99e7c5 100644 --- a/src/lint/renderer/ArcanistConsoleLintRenderer.php +++ b/src/lint/renderer/ArcanistConsoleLintRenderer.php @@ -1,331 +1,343 @@ showAutofixPatches = $show_autofix_patches; - return $this; - } + private $testableMode; public function setTestableMode($testable_mode) { $this->testableMode = $testable_mode; return $this; } public function getTestableMode() { return $this->testableMode; } + public function supportsPatching() { + return true; + } + + public function renderResultCode($result_code) { + if ($result_code == ArcanistLintWorkflow::RESULT_OKAY) { + $view = new PhutilConsoleInfo( + pht('OKAY'), + pht('No lint messages.')); + $this->writeOut($view->drawConsoleString()); + } + } + + public function promptForPatch( + ArcanistLintResult $result, + $old_path, + $new_path) { + + if ($old_path === null) { + $old_path = '/dev/null'; + } + + list($err, $stdout) = exec_manual('diff -u %s %s', $old_path, $new_path); + $this->writeOut($stdout); + + $prompt = pht( + 'Apply this patch to %s?', + tsprintf('__%s__', $result->getPath())); + + return phutil_console_confirm($prompt, $default_no = false); + } + public function renderLintResult(ArcanistLintResult $result) { $messages = $result->getMessages(); $path = $result->getPath(); $data = $result->getData(); $line_map = $this->newOffsetMap($data); $text = array(); foreach ($messages as $message) { - if (!$this->showAutofixPatches && $message->isAutofix()) { - continue; - } - if ($message->isError()) { $color = 'red'; } else { $color = 'yellow'; } $severity = ArcanistLintSeverity::getStringForSeverity( $message->getSeverity()); $code = $message->getCode(); $name = $message->getName(); $description = $message->getDescription(); if ($message->getOtherLocations()) { $locations = array(); foreach ($message->getOtherLocations() as $location) { $locations[] = idx($location, 'path', $path). (!empty($location['line']) ? ":{$location['line']}" : ''); } $description .= "\n".pht( 'Other locations: %s', implode(', ', $locations)); } $text[] = phutil_console_format( " ** %s ** (%s) __%s__\n%s\n", $severity, $code, $name, phutil_console_wrap($description, 4)); if ($message->hasFileContext()) { $text[] = $this->renderContext($message, $data, $line_map); } } if ($text) { $prefix = phutil_console_format( "**>>>** %s\n\n\n", pht( 'Lint for %s:', phutil_console_format('__%s__', $path))); - return $prefix.implode("\n", $text); - } else { - return null; + $this->writeOut($prefix.implode("\n", $text)); } } protected function renderContext( ArcanistLintMessage $message, $data, array $line_map) { $context = 3; $message = $message->newTrimmedMessage(); $original = $message->getOriginalText(); $replacement = $message->getReplacementText(); $line = $message->getLine(); $char = $message->getChar(); $old = $data; $old_lines = phutil_split_lines($old); $old_impact = substr_count($original, "\n") + 1; $start = $line; if ($message->isPatchable()) { $patch_offset = $line_map[$line] + ($char - 1); $new = substr_replace( $old, $replacement, $patch_offset, strlen($original)); $new_lines = phutil_split_lines($new); // Figure out how many "-" and "+" lines we have by counting the newlines // for the relevant patches. This may overestimate things if we are adding // or removing entire lines, but we'll adjust things below. $new_impact = substr_count($replacement, "\n") + 1; // If this is a change on a single line, we'll try to highlight the // changed character range to make it easier to pick out. if ($old_impact === 1 && $new_impact === 1) { $old_lines[$start - 1] = substr_replace( $old_lines[$start - 1], $this->highlightText($original), $char - 1, strlen($original)); $new_lines[$start - 1] = substr_replace( $new_lines[$start - 1], $this->highlightText($replacement), $char - 1, strlen($replacement)); } // If lines at the beginning of the changed line range are actually the // same, shrink the range. This happens when a patch just adds a line. do { $old_line = idx($old_lines, $start - 1, null); $new_line = idx($new_lines, $start - 1, null); if ($old_line !== $new_line) { break; } $start++; $old_impact--; $new_impact--; // We can end up here if a patch removes a line which occurs before // another identical line. if ($old_impact <= 0 || $new_impact <= 0) { break; } } while (true); // If the lines at the end of the changed line range are actually the // same, shrink the range. This happens when a patch just removes a // line. if ($old_impact > 0 && $new_impact > 0) { do { $old_suffix = idx($old_lines, $start + $old_impact - 2, null); $new_suffix = idx($new_lines, $start + $new_impact - 2, null); if ($old_suffix !== $new_suffix) { break; } $old_impact--; $new_impact--; // We can end up here if a patch removes a line which occurs after // another identical line. if ($old_impact <= 0 || $new_impact <= 0) { break; } } while (true); } } else { // If we have "original" text and it is contained on a single line, // highlight the affected area. If we don't have any text, we'll mark // the character with a caret (below, in rendering) instead. if ($old_impact == 1 && strlen($original)) { $old_lines[$start - 1] = substr_replace( $old_lines[$start - 1], $this->highlightText($original), $char - 1, strlen($original)); } $old_impact = 0; $new_impact = 0; } $out = array(); $head = max(1, $start - $context); for ($ii = $head; $ii < $start; $ii++) { $out[] = array( 'text' => $old_lines[$ii - 1], 'number' => $ii, ); } for ($ii = $start; $ii < $start + $old_impact; $ii++) { $out[] = array( 'text' => $old_lines[$ii - 1], 'number' => $ii, 'type' => '-', 'chevron' => ($ii == $start), ); } for ($ii = $start; $ii < $start + $new_impact; $ii++) { // If the patch was at the end of the file and ends with a newline, we // won't have an actual entry in the array for the last line, even though // we want to show it in the diff. $out[] = array( 'text' => idx($new_lines, $ii - 1, ''), 'type' => '+', 'chevron' => ($ii == $start), ); } $cursor = $start + $old_impact; $foot = min(count($old_lines), $cursor + $context); for ($ii = $cursor; $ii <= $foot; $ii++) { $out[] = array( 'text' => $old_lines[$ii - 1], 'number' => $ii, 'chevron' => ($ii == $cursor), ); } $result = array(); $seen_chevron = false; foreach ($out as $spec) { if ($seen_chevron) { $chevron = false; } else { $chevron = !empty($spec['chevron']); if ($chevron) { $seen_chevron = true; } } // If the line doesn't actually end in a newline, add one so the layout // doesn't mess up. This can happen when the last line of the old file // didn't have a newline at the end. $text = $spec['text']; if (!preg_match('/\n\z/', $spec['text'])) { $text .= "\n"; } $result[] = $this->renderLine( idx($spec, 'number'), $text, $chevron, idx($spec, 'type')); // If this is just a message and does not have a patch, put a little // caret underneath the line to point out where the issue is. if ($chevron) { if (!$message->isPatchable() && !strlen($original)) { $result[] = $this->renderCaret($char)."\n"; } } } return implode('', $result); } private function renderCaret($pos) { return str_repeat(' ', 16 + $pos).'^'; } protected function renderLine($line, $data, $chevron = false, $diff = null) { $chevron = $chevron ? '>>>' : ''; return sprintf( ' %3s %1s %6s %s', $chevron, $diff, $line, $data); } - public function renderOkayResult() { - return phutil_console_format( - "** %s ** %s\n", - pht('OKAY'), - pht('No lint warnings.')); - } - private function newOffsetMap($data) { $lines = phutil_split_lines($data); $line_map = array(); $number = 1; $offset = 0; foreach ($lines as $line) { $line_map[$number] = $offset; $number++; $offset += strlen($line); } // If the last line ends in a newline, add a virtual offset for the final // line with no characters on it. This allows lint messages to target the // last line of the file at character 1. if ($lines) { if (preg_match('/\n\z/', $line)) { $line_map[$number] = $offset; } } return $line_map; } private function highlightText($text) { if ($this->getTestableMode()) { return '>'.$text.'<'; } else { return (string)tsprintf('##%s##', $text); } } } diff --git a/src/lint/renderer/ArcanistJSONLintRenderer.php b/src/lint/renderer/ArcanistJSONLintRenderer.php index 04432344..034ade8d 100644 --- a/src/lint/renderer/ArcanistJSONLintRenderer.php +++ b/src/lint/renderer/ArcanistJSONLintRenderer.php @@ -1,35 +1,30 @@ getMessages(); $path = $result->getPath(); $data = explode("\n", $result->getData()); array_unshift($data, ''); // make the line numbers work as array indices $output = array($path => array()); foreach ($messages as $message) { $dictionary = $message->toDictionary(); $dictionary['context'] = implode("\n", array_slice( $data, max(1, $message->getLine() - self::LINES_OF_CONTEXT), self::LINES_OF_CONTEXT * 2 + 1)); unset($dictionary['path']); $output[$path][] = $dictionary; } - return json_encode($output)."\n"; - } - - public function renderOkayResult() { - return ''; + $this->writeOut(json_encode($output)."\n"); } } diff --git a/src/lint/renderer/ArcanistLintRenderer.php b/src/lint/renderer/ArcanistLintRenderer.php index f153967d..1999fa6f 100644 --- a/src/lint/renderer/ArcanistLintRenderer.php +++ b/src/lint/renderer/ArcanistLintRenderer.php @@ -1,19 +1,61 @@ getPhobjectClassConstant('RENDERERKEY'); + } + + final public static function getAllRenderers() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getRendererKey') + ->execute(); + } + + final public function setOutputPath($path) { + $this->output = $path; + return $this; + } + + + /** + * Does this renderer support applying lint patches? + * + * @return bool True if patches should be applied when using this renderer. + */ + public function supportsPatching() { + return false; + } + + public function willRenderResults() { + return null; + } + + public function didRenderResults() { + return null; + } + + public function renderResultCode($result_code) { + return null; + } + + public function handleException(Exception $ex) { + throw $ex; } abstract public function renderLintResult(ArcanistLintResult $result); - abstract public function renderOkayResult(); - public function renderPostamble() { - return ''; + protected function writeOut($message) { + if ($this->output) { + Filesystem::appendFile($this->output, $message); + } else { + echo $message; + } + + return $this; } } diff --git a/src/lint/renderer/ArcanistNoneLintRenderer.php b/src/lint/renderer/ArcanistNoneLintRenderer.php index ce74e395..c28e2cb0 100644 --- a/src/lint/renderer/ArcanistNoneLintRenderer.php +++ b/src/lint/renderer/ArcanistNoneLintRenderer.php @@ -1,13 +1,11 @@ getMessages(); $path = $result->getPath(); $text = array(); foreach ($messages as $message) { $name = $message->getName(); $severity = ArcanistLintSeverity::getStringForSeverity( $message->getSeverity()); $line = $message->getLine(); $text[] = "{$path}:{$line}:{$severity}: {$name}\n"; } - return implode('', $text); + $this->writeOut(implode('', $text)); } - public function renderOkayResult() { - return phutil_console_format( - "** %s ** %s\n", - pht('OKAY'), - pht('No lint warnings.')); + public function renderResultCode($result_code) { + if ($result_code == ArcanistLintWorkflow::RESULT_OKAY) { + $view = new PhutilConsoleInfo( + pht('OKAY'), + pht('No lint messages.')); + $this->writeOut($view->drawConsoleString()); + } } } diff --git a/src/lint/renderer/__tests__/ArcanistConsoleLintRendererTestCase.php b/src/lint/renderer/__tests__/ArcanistConsoleLintRendererTestCase.php index f65e40c9..98149a58 100644 --- a/src/lint/renderer/__tests__/ArcanistConsoleLintRendererTestCase.php +++ b/src/lint/renderer/__tests__/ArcanistConsoleLintRendererTestCase.php @@ -1,241 +1,247 @@ array( 'line' => 1, 'char' => 1, 'original' => 'a', 'replacement' => 'z', ), 'inline' => array( 'line' => 1, 'char' => 7, 'original' => 'cat', 'replacement' => 'dog', ), // In this test, the original and replacement texts have a large // amount of overlap. 'overlap' => array( 'line' => 1, 'char' => 1, 'original' => 'tantawount', 'replacement' => 'tantamount', ), 'newline' => array( 'line' => 6, 'char' => 1, 'original' => "\n", 'replacement' => '', ), 'addline' => array( 'line' => 3, 'char' => 1, 'original' => '', 'replacement' => "cherry\n", ), 'addlinesuffix' => array( 'line' => 2, 'char' => 7, 'original' => '', 'replacement' => "\ncherry", ), 'xml' => array( 'line' => 3, 'char' => 6, 'original' => '', 'replacement' => "\n", ), 'caret' => array( 'line' => 2, 'char' => 13, 'name' => 'Fruit Misinformation', 'description' => 'Arguably untrue.', ), 'original' => array( 'line' => 1, 'char' => 4, 'original' => 'should of', ), 'midline' => array( 'line' => 1, 'char' => 1, 'original' => $midline_original, 'replacement' => $midline_replacement, ), 'remline' => array( 'line' => 1, 'char' => 1, 'original' => $remline_original, 'replacement' => $remline_replacement, ), 'extrawhitespace' => array( 'line' => 2, 'char' => 1, 'original' => "\n", 'replacement' => '', ), 'eofnewline' => array( 'line' => 1, 'char' => 7, 'original' => '', 'replacement' => "\n", ), 'eofmultilinechar' => array( 'line' => 5, 'char' => 3, 'original' => '', 'replacement' => "\nX\nY\n", ), 'eofmultilineline' => array( 'line' => 6, 'char' => 1, 'original' => '', 'replacement' => "\nX\nY\n", ), 'rmmulti' => array( 'line' => 2, 'char' => 1, 'original' => "\n", 'replacement' => '', ), 'rmmulti2' => array( 'line' => 1, 'char' => 2, 'original' => "\n", 'replacement' => '', ), ); $defaults = array( 'severity' => ArcanistLintSeverity::SEVERITY_WARNING, 'name' => 'Lint Warning', 'path' => 'path/to/example.c', 'description' => 'Consider this.', 'code' => 'WARN123', ); foreach ($map as $key => $test_case) { $data = $this->readTestData("{$key}.txt"); $expect = $this->readTestData("{$key}.expect"); $test_case = $test_case + $defaults; $path = $test_case['path']; $severity = $test_case['severity']; $name = $test_case['name']; $description = $test_case['description']; $code = $test_case['code']; $line = $test_case['line']; $char = $test_case['char']; $original = idx($test_case, 'original'); $replacement = idx($test_case, 'replacement'); $message = id(new ArcanistLintMessage()) ->setPath($path) ->setSeverity($severity) ->setName($name) ->setDescription($description) ->setCode($code) ->setLine($line) ->setChar($char) ->setOriginalText($original) ->setReplacementText($replacement); $result = id(new ArcanistLintResult()) ->setPath($path) ->setData($data) ->addMessage($message); $renderer = id(new ArcanistConsoleLintRenderer()) ->setTestableMode(true); try { PhutilConsoleFormatter::disableANSI(true); - $actual = $renderer->renderLintResult($result); + + $tmp = new TempFile(); + $renderer->setOutputPath($tmp); + $renderer->renderLintResult($result); + $actual = Filesystem::readFile($tmp); + unset($tmp); + PhutilConsoleFormatter::disableANSI(false); } catch (Exception $ex) { PhutilConsoleFormatter::disableANSI(false); throw $ex; } $this->assertEqual( $expect, $actual, pht( 'Lint rendering for "%s".', $key)); } } private function readTestData($filename) { $path = dirname(__FILE__).'/data/'.$filename; $data = Filesystem::readFile($path); // If we find "~~~" at the end of the file, get rid of it and any whitespace // afterwards. This allows specifying data files with trailing empty // lines. $data = preg_replace('/~~~\s*\z/', '', $data); // Trim "~" off the ends of lines. This allows the "expect" file to test // for trailing whitespace without actually containing trailing // whitespace. $data = preg_replace('/~$/m', '', $data); return $data; } } diff --git a/src/workflow/ArcanistLintWorkflow.php b/src/workflow/ArcanistLintWorkflow.php index d61091a8..9ab1aca2 100644 --- a/src/workflow/ArcanistLintWorkflow.php +++ b/src/workflow/ArcanistLintWorkflow.php @@ -1,666 +1,393 @@ shouldAmendChanges = $should_amend; return $this; } public function setShouldAmendWithoutPrompt($should_amend) { $this->shouldAmendWithoutPrompt = $should_amend; return $this; } public function setShouldAmendAutofixesWithoutPrompt($should_amend) { $this->shouldAmendAutofixesWithoutPrompt = $should_amend; return $this; } public function getCommandSynopses() { return phutil_console_format(<< array( 'help' => pht( 'Show all lint warnings, not just those on changed lines. When '. 'paths are specified, this is the default behavior.'), - 'conflicts' => array( - 'only-changed' => true, - ), - ), - 'only-changed' => array( - 'help' => pht( - 'Show lint warnings just on changed lines. When no paths are '. - 'specified, this is the default. This differs from only-new '. - 'in cases where line modifications introduce lint on other '. - 'unmodified lines.'), - 'conflicts' => array( - 'lintall' => true, - ), ), 'rev' => array( 'param' => 'revision', 'help' => pht('Lint changes since a specific revision.'), 'supports' => array( 'git', 'hg', ), 'nosupport' => array( 'svn' => pht('Lint does not currently support %s in SVN.', '--rev'), ), ), 'output' => array( 'param' => 'format', - 'help' => pht( - "With 'summary', show lint warnings in a more compact format. ". - "With 'json', show lint warnings in machine-readable JSON format. ". - "With 'none', show no lint warnings. ". - "With 'compiler', show lint warnings in suitable for your editor. ". - "With 'xml', show lint warnings in the Checkstyle XML format."), + 'help' => pht('Select an output format.'), ), 'outfile' => array( 'param' => 'path', 'help' => pht( 'Output the linter results to a file. Defaults to stdout.'), ), - 'only-new' => array( - 'param' => 'bool', - 'supports' => array('git', 'hg'), // TODO: svn - 'help' => pht( - 'Display only messages not present in the original code.'), - ), 'engine' => array( 'param' => 'classname', 'help' => pht('Override configured lint engine for this project.'), ), 'apply-patches' => array( 'help' => pht( 'Apply patches suggested by lint to the working copy without '. 'prompting.'), 'conflicts' => array( 'never-apply-patches' => true, ), ), 'never-apply-patches' => array( 'help' => pht('Never apply patches suggested by lint.'), 'conflicts' => array( 'apply-patches' => true, ), ), 'amend-all' => array( 'help' => pht( 'When linting git repositories, amend HEAD with all patches '. 'suggested by lint without prompting.'), ), 'amend-autofixes' => array( 'help' => pht( 'When linting git repositories, amend HEAD with autofix '. 'patches suggested by lint without prompting.'), ), 'everything' => array( 'help' => pht( 'Lint all tracked files in the working copy. Ignored files and '. 'untracked files will not be linted.'), 'conflicts' => array( - 'cache' => pht('%s lints all files', '--everything'), 'rev' => pht('%s lints all files', '--everything'), ), ), 'severity' => array( 'param' => 'string', 'help' => pht( "Set minimum message severity. One of: %s. Defaults to '%s'.", sprintf( "'%s'", implode( "', '", array_keys(ArcanistLintSeverity::getLintSeverities()))), self::DEFAULT_SEVERITY), ), - 'cache' => array( - 'param' => 'bool', - 'help' => pht( - "%d to disable cache, %d to enable. The default value is determined ". - "by '%s' in configuration, which defaults to off. See notes in '%s'.", - 0, - 1, - 'arc.lint.cache', - 'arc.lint.cache'), - ), '*' => 'paths', ); } public function requiresAuthentication() { - return (bool)$this->getArgument('only-new'); + return false; } public function requiresWorkingCopy() { return true; } public function requiresRepositoryAPI() { return true; } - private function getCacheKey() { - return implode("\n", array( - get_class($this->engine), - $this->getArgument('severity', self::DEFAULT_SEVERITY), - $this->shouldLintAll, - )); - } - public function run() { $console = PhutilConsole::getConsole(); $working_copy = $this->getWorkingCopy(); $configuration_manager = $this->getConfigurationManager(); $engine = $this->newLintEngine($this->getArgument('engine')); $rev = $this->getArgument('rev'); $paths = $this->getArgument('paths'); - $use_cache = $this->getArgument('cache', null); $everything = $this->getArgument('everything'); if ($everything && $paths) { throw new ArcanistUsageException( pht( 'You can not specify paths with %s. The %s flag lints every '. 'tracked file in the working copy.', '--everything', '--everything')); } - if ($use_cache === null) { - $use_cache = (bool)$configuration_manager->getConfigFromAnySource( - 'arc.lint.cache', - false); - } - if ($rev && $paths) { - throw new ArcanistUsageException( - pht('Specify either %s or paths.', '--rev')); + if ($rev !== null) { + $this->parseBaseCommitArgument(array($rev)); } + // Sometimes, we hide low-severity messages which occur on lines which + // were not changed. This is the default behavior when you run "arc lint" + // with no arguments: if you touched a file, but there was already some + // minor warning about whitespace or spelling elsewhere in the file, you + // don't need to correct it. - // NOTE: When the user specifies paths, we imply --lintall and show all - // warnings for the paths in question. This is easier to deal with for - // us and less confusing for users. - $this->shouldLintAll = $paths ? true : false; + // In other modes, notably "arc lint ", this is not the defualt + // behavior. If you ask us to lint a specific file, we show you all the + // lint messages in the file. + + // You can change this behavior with various flags, including "--lintall", + // "--rev", and "--everything". if ($this->getArgument('lintall')) { - $this->shouldLintAll = true; - } else if ($this->getArgument('only-changed')) { - $this->shouldLintAll = false; + $lint_all = true; + } else if ($rev !== null) { + $lint_all = false; + } else if ($paths || $everything) { + $lint_all = true; + } else { + $lint_all = false; } if ($everything) { $paths = iterator_to_array($this->getRepositoryAPI()->getAllFiles()); - $this->shouldLintAll = true; } else { $paths = $this->selectPathsForWorkflow($paths, $rev); } $this->engine = $engine; $engine->setMinimumSeverity( $this->getArgument('severity', self::DEFAULT_SEVERITY)); - $file_hashes = array(); - if ($use_cache) { - $engine->setRepositoryVersion($this->getRepositoryVersion()); - $cache = $this->readScratchJSONFile('lint-cache.json'); - $cache = idx($cache, $this->getCacheKey(), array()); - $cached = array(); - - foreach ($paths as $path) { - $abs_path = $engine->getFilePathOnDisk($path); - if (!Filesystem::pathExists($abs_path)) { - continue; - } - $file_hashes[$abs_path] = md5_file($abs_path); - - if (!isset($cache[$path])) { - continue; - } - $messages = idx($cache[$path], $file_hashes[$abs_path]); - if ($messages !== null) { - $cached[$path] = $messages; - } - } - - if ($cached) { - $console->writeErr( - "%s\n", - pht( - "Using lint cache, use '%s' to disable it.", - '--cache 0')); - } - - $engine->setCachedResults($cached); - } - // Propagate information about which lines changed to the lint engine. // This is used so that the lint engine can drop warning messages // concerning lines that weren't in the change. $engine->setPaths($paths); - if (!$this->shouldLintAll) { + if (!$lint_all) { foreach ($paths as $path) { // Note that getChangedLines() returns null to indicate that a file // is binary or a directory (i.e., changed lines are not relevant). $engine->setPathChangedLines( $path, $this->getChangedLines($path, 'new')); } } - // Enable possible async linting only for 'arc diff' not 'arc lint' - if ($this->getParentWorkflow()) { - $engine->setEnableAsyncLint(true); - } else { - $engine->setEnableAsyncLint(false); - } - - if ($this->getArgument('only-new')) { - $conduit = $this->getConduit(); - $api = $this->getRepositoryAPI(); - if ($rev) { - $api->setBaseCommit($rev); - } - $svn_root = id(new PhutilURI($api->getSourceControlPath()))->getPath(); - - $all_paths = array(); - foreach ($paths as $path) { - $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); - $full_paths = array($path); - - $change = $this->getChange($path); - $type = $change->getType(); - if (ArcanistDiffChangeType::isOldLocationChangeType($type)) { - $full_paths = $change->getAwayPaths(); - } else if (ArcanistDiffChangeType::isNewLocationChangeType($type)) { - continue; - } else if (ArcanistDiffChangeType::isDeleteChangeType($type)) { - continue; - } - - foreach ($full_paths as $full_path) { - $all_paths[$svn_root.'/'.$full_path] = $path; - } - } - - $lint_future = $conduit->callMethod('diffusion.getlintmessages', array( - 'repositoryPHID' => idx($this->loadProjectRepository(), 'phid'), - 'branch' => '', // TODO: Tracking branch. - 'commit' => $api->getBaseCommit(), - 'files' => array_keys($all_paths), - )); - } - $failed = null; try { $engine->run(); } catch (Exception $ex) { $failed = $ex; } $results = $engine->getResults(); - if ($this->getArgument('only-new')) { - $total = 0; - foreach ($results as $result) { - $total += count($result->getMessages()); - } - - // Don't wait for response with default value of --only-new. - $timeout = null; - if ($this->getArgument('only-new') === null || !$total) { - $timeout = 0; - } - - $raw_messages = $this->resolveCall($lint_future, $timeout); - if ($raw_messages && $total) { - $old_messages = array(); - $line_maps = array(); - foreach ($raw_messages as $message) { - $path = $all_paths[$message['path']]; - $line = $message['line']; - $code = $message['code']; - - if (!isset($line_maps[$path])) { - $line_maps[$path] = $this->getChange($path)->buildLineMap(); - } - - $new_lines = idx($line_maps[$path], $line); - if (!$new_lines) { // Unmodified lines after last hunk. - $last_old = ($line_maps[$path] ? last_key($line_maps[$path]) : 0); - $news = array_filter($line_maps[$path]); - $last_new = ($news ? last(end($news)) : 0); - $new_lines = array($line + $last_new - $last_old); - } - - $error = array($code => array(true)); - foreach ($new_lines as $new) { - if (isset($old_messages[$path][$new])) { - $old_messages[$path][$new][$code][] = true; - break; - } - $old_messages[$path][$new] = &$error; - } - unset($error); - } - - foreach ($results as $result) { - foreach ($result->getMessages() as $message) { - $path = str_replace(DIRECTORY_SEPARATOR, '/', $message->getPath()); - $line = $message->getLine(); - $code = $message->getCode(); - if (!empty($old_messages[$path][$line][$code])) { - $message->setObsolete(true); - array_pop($old_messages[$path][$line][$code]); - } - } - $result->sortAndFilterMessages(); - } - } - } - if ($this->getArgument('never-apply-patches')) { $apply_patches = false; } else { $apply_patches = true; } if ($this->getArgument('apply-patches')) { $prompt_patches = false; } else { $prompt_patches = true; } if ($this->getArgument('amend-all')) { $this->shouldAmendChanges = true; $this->shouldAmendWithoutPrompt = true; } if ($this->getArgument('amend-autofixes')) { - $prompt_autofix_patches = false; $this->shouldAmendChanges = true; $this->shouldAmendAutofixesWithoutPrompt = true; - } else { - $prompt_autofix_patches = true; } $repository_api = $this->getRepositoryAPI(); if ($this->shouldAmendChanges) { $this->shouldAmendChanges = $repository_api->supportsAmend() && !$this->isHistoryImmutable(); } $wrote_to_disk = false; - switch ($this->getArgument('output')) { - case 'json': - $renderer = new ArcanistJSONLintRenderer(); - $prompt_patches = false; - $apply_patches = $this->getArgument('apply-patches'); - break; - case 'summary': - $renderer = new ArcanistSummaryLintRenderer(); - break; - case 'none': - $prompt_patches = false; - $apply_patches = $this->getArgument('apply-patches'); - $renderer = new ArcanistNoneLintRenderer(); - break; - case 'compiler': - $renderer = new ArcanistCompilerLintRenderer(); - $prompt_patches = false; - $apply_patches = $this->getArgument('apply-patches'); - break; - case 'xml': - $renderer = new ArcanistCheckstyleXMLLintRenderer(); - $prompt_patches = false; - $apply_patches = $this->getArgument('apply-patches'); - break; - default: - $renderer = new ArcanistConsoleLintRenderer(); - $renderer->setShowAutofixPatches($prompt_autofix_patches); - break; + $default_renderer = ArcanistConsoleLintRenderer::RENDERERKEY; + $renderer_key = $this->getArgument('output', $default_renderer); + + $renderers = ArcanistLintRenderer::getAllRenderers(); + if (!isset($renderers[$renderer_key])) { + throw new Exception( + pht( + 'Lint renderer "%s" is unknown. Supported renderers are: %s.', + $renderer_key, + implode(', ', array_keys($renderers)))); } + $renderer = $renderers[$renderer_key]; $all_autofix = true; - $tmp = null; - if ($this->getArgument('outfile') !== null) { - $tmp = id(new TempFile()) - ->setPreserveFile(true); + $out_path = $this->getArgument('outfile'); + if ($out_path !== null) { + $tmp = new TempFile(); + $renderer->setOutputPath((string)$tmp); + } else { + $tmp = null; } - $preamble = $renderer->renderPreamble(); - if ($tmp) { - Filesystem::appendFile($tmp, $preamble); - } else { - $console->writeOut('%s', $preamble); + if ($failed) { + $renderer->handleException($failed); } - foreach ($results as $result) { - $result_all_autofix = $result->isAllAutofix(); + $renderer->willRenderResults(); - if (!$result->getMessages() && !$result_all_autofix) { + $should_patch = ($apply_patches && $renderer->supportsPatching()); + foreach ($results as $result) { + if (!$result->getMessages()) { continue; } + $result_all_autofix = $result->isAllAutofix(); if (!$result_all_autofix) { $all_autofix = false; } - $lint_result = $renderer->renderLintResult($result); - if ($lint_result) { - if ($tmp) { - Filesystem::appendFile($tmp, $lint_result); - } else { - $console->writeOut('%s', $lint_result); - } - } + $renderer->renderLintResult($result); - if ($apply_patches && $result->isPatchable()) { + if ($should_patch && $result->isPatchable()) { $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result); - $old_file = $result->getFilePathOnDisk(); - if ($prompt_patches && - !($result_all_autofix && !$prompt_autofix_patches)) { + $apply = true; + if ($prompt_patches && !$result_all_autofix) { + $old_file = $result->getFilePathOnDisk(); if (!Filesystem::pathExists($old_file)) { - $old_file = '/dev/null'; + $old_file = null; } + $new_file = new TempFile(); $new = $patcher->getModifiedFileContent(); Filesystem::writeFile($new_file, $new); - // TODO: Improve the behavior here, make it more like - // difference_render(). - list(, $stdout, $stderr) = - exec_manual('diff -u %s %s', $old_file, $new_file); - $console->writeOut('%s', $stdout); - $console->writeErr('%s', $stderr); - - $prompt = pht( - 'Apply this patch to %s?', - phutil_console_format('__%s__', $result->getPath())); - if (!phutil_console_confirm($prompt, $default_no = false)) { - continue; - } + $apply = $renderer->promptForPatch($result, $old_file, $new_file); } - $patcher->writePatchToDisk(); - $wrote_to_disk = true; - $file_hashes[$old_file] = md5_file($old_file); + if ($apply) { + $patcher->writePatchToDisk(); + $wrote_to_disk = true; + } } } - $postamble = $renderer->renderPostamble(); + $renderer->didRenderResults(); + if ($tmp) { - Filesystem::appendFile($tmp, $postamble); - Filesystem::rename($tmp, $this->getArgument('outfile')); - } else { - $console->writeOut('%s', $postamble); + Filesystem::rename($tmp, $out_path); } if ($wrote_to_disk && $this->shouldAmendChanges) { if ($this->shouldAmendWithoutPrompt || ($this->shouldAmendAutofixesWithoutPrompt && $all_autofix)) { $console->writeOut( "** %s ** %s\n", pht('LINT NOTICE'), pht('Automatically amending HEAD with lint patches.')); $amend = true; } else { $amend = phutil_console_confirm(pht('Amend HEAD with lint patches?'), false); } if ($amend) { if ($repository_api instanceof ArcanistGitAPI) { // Add the changes to the index before amending $repository_api->execxLocal('add -u'); } $repository_api->amendCommit(); } else { throw new ArcanistUsageException( pht( 'Sort out the lint changes that were applied to the working '. 'copy and relint.')); } } - if ($this->getArgument('output') == 'json') { - // NOTE: Required by save_lint.php in Phabricator. - return 0; - } - - if ($failed) { - if ($failed instanceof ArcanistNoEffectException) { - if ($renderer instanceof ArcanistNoneLintRenderer) { - return 0; - } - } - throw $failed; - } - $unresolved = array(); $has_warnings = false; $has_errors = false; foreach ($results as $result) { foreach ($result->getMessages() as $message) { if (!$message->isPatchApplied()) { if ($message->isError()) { $has_errors = true; } else if ($message->isWarning()) { $has_warnings = true; } $unresolved[] = $message; } } } $this->unresolvedMessages = $unresolved; - $cache = $this->readScratchJSONFile('lint-cache.json'); - $cached = idx($cache, $this->getCacheKey(), array()); - if ($cached || $use_cache) { - $stopped = $engine->getStoppedPaths(); - foreach ($results as $result) { - $path = $result->getPath(); - if (!$use_cache) { - unset($cached[$path]); - continue; - } - $abs_path = $engine->getFilePathOnDisk($path); - if (!Filesystem::pathExists($abs_path)) { - continue; - } - $version = $result->getCacheVersion(); - $cached_path = array(); - if (isset($stopped[$path])) { - $cached_path['stopped'] = $stopped[$path]; - } - $cached_path['repository_version'] = $this->getRepositoryVersion(); - foreach ($result->getMessages() as $message) { - $granularity = $message->getGranularity(); - if ($granularity == ArcanistLinter::GRANULARITY_GLOBAL) { - continue; - } - if (!$message->isPatchApplied()) { - $cached_path[] = $message->toDictionary(); - } - } - $hash = idx($file_hashes, $abs_path); - if (!$hash) { - $hash = md5_file($abs_path); - } - $cached[$path] = array($hash => array($version => $cached_path)); - } - $cache[$this->getCacheKey()] = $cached; - // TODO: Garbage collection. - $this->writeScratchJSONFile('lint-cache.json', $cache); - } - // Take the most severe lint message severity and use that // as the result code. if ($has_errors) { $result_code = self::RESULT_ERRORS; } else if ($has_warnings) { $result_code = self::RESULT_WARNINGS; } else { $result_code = self::RESULT_OKAY; } - if (!$this->getParentWorkflow()) { - if ($result_code == self::RESULT_OKAY) { - $console->writeOut('%s', $renderer->renderOkayResult()); - } - } + $renderer->renderResultCode($result_code); return $result_code; } public function getUnresolvedMessages() { return $this->unresolvedMessages; } }