diff --git a/src/lint/engine/ArcanistLintEngine.php b/src/lint/engine/ArcanistLintEngine.php index 9025b839..fd5816eb 100644 --- a/src/lint/engine/ArcanistLintEngine.php +++ b/src/lint/engine/ArcanistLintEngine.php @@ -1,409 +1,416 @@ workingCopy = $working_copy; return $this; } public function getWorkingCopy() { return $this->workingCopy; } public function setPaths($paths) { $this->paths = $paths; return $this; } public function getPaths() { return $this->paths; } public function setPathChangedLines($path, $changed) { if ($changed === null) { $this->changedLines[$path] = null; } else { $this->changedLines[$path] = array_fill_keys($changed, true); } return $this; } public function getPathChangedLines($path) { return idx($this->changedLines, $path); } public function setFileData($data) { $this->fileData = $data + $this->fileData; return $this; } public function setCommitHookMode($mode) { $this->commitHookMode = $mode; return $this; } public function setHookAPI(ArcanistHookAPI $hook_api) { $this->hookAPI = $hook_api; return $this; } public function getHookAPI() { return $this->hookAPI; } public function setEnableAsyncLint($enable_async_lint) { $this->enableAsyncLint = $enable_async_lint; return $this; } public function getEnableAsyncLint() { return $this->enableAsyncLint; } public function loadData($path) { if (!isset($this->fileData[$path])) { if ($this->getCommitHookMode()) { $this->fileData[$path] = $this->getHookAPI() ->getCurrentFileData($path); } else { $disk_path = $this->getFilePathOnDisk($path); $this->fileData[$path] = Filesystem::readFile($disk_path); } } return $this->fileData[$path]; } public function pathExists($path) { if ($this->getCommitHookMode()) { $file_data = $this->loadData($path); return ($file_data !== null); } else { $disk_path = $this->getFilePathOnDisk($path); return Filesystem::pathExists($disk_path); } } public function getFilePathOnDisk($path) { return Filesystem::resolvePath( $path, $this->getWorkingCopy()->getProjectRoot()); } public function setMinimumSeverity($severity) { $this->minimumSeverity = $severity; return $this; } public function getCommitHookMode() { return $this->commitHookMode; } public function run() { $linters = $this->buildLinters(); if (!$linters) { throw new ArcanistNoEffectException("No linters to run."); } $have_paths = false; foreach ($linters as $linter) { if ($linter->getPaths()) { $have_paths = true; break; } } if (!$have_paths) { throw new ArcanistNoEffectException("No paths are lintable."); } $versions = array($this->getCacheVersion()); foreach ($linters as $linter) { $versions[] = get_class($linter).':'.$linter->getCacheVersion(); } $this->cacheVersion = crc32(implode("\n", $versions)); - $linters_paths = array(); - foreach ($linters as $linter_name => $linter) { - $linter->setEngine($this); - if (!$linter->canRun()) { - continue; - } - $paths = $linter->getPaths(); - - $cache_granularity = $linter->getCacheGranularity(); - - foreach ($paths as $key => $path) { - // 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 (isset($this->cachedResults[$path][$this->cacheVersion])) { - if ($cache_granularity == ArcanistLinter::GRANULARITY_FILE) { - unset($paths[$key]); - } - } - } - $paths = array_values($paths); - $linters_paths[$linter_name] = $paths; - - if ($paths) { - $linter->willLintPaths($paths); - } - } - $stopped = array(); $exceptions = array(); foreach ($linters as $linter_name => $linter) { try { - foreach ($linters_paths[$linter_name] as $path) { - if (isset($stopped[$path])) { - continue; - } - $linter->willLintPath($path); - $linter->lintPath($path); - if ($linter->didStopAllLinters()) { - $stopped[$path] = true; - } + $linter->setEngine($this); + if (!$linter->canRun()) { + continue; } + $paths = $linter->getPaths(); + + $cache_granularity = $linter->getCacheGranularity(); - $minimum = $this->minimumSeverity; - foreach ($linter->getLintMessages() as $message) { - if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) { - continue; + foreach ($paths as $key => $path) { + // 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 (isset($stopped[$path])) { + unset($paths[$key]); } - if (!$this->isRelevantMessage($message)) { - continue; + if (isset($this->cachedResults[$path][$this->cacheVersion])) { + if ($cache_granularity == ArcanistLinter::GRANULARITY_FILE) { + unset($paths[$key]); + } } - if ($cache_granularity != ArcanistLinter::GRANULARITY_FILE) { - $message->setUncacheable(true); + } + $paths = array_values($paths); + + if ($paths) { + $linter->willLintPaths($paths); + foreach ($paths as $path) { + $linter->willLintPath($path); + $linter->lintPath($path); + if ($linter->didStopAllLinters()) { + $stopped[$path] = true; + } } - $result = $this->getResultForPath($message->getPath()); - $result->addMessage($message); } + } catch (Exception $ex) { if (!is_string($linter_name)) { $linter_name = get_class($linter); } $exceptions[$linter_name] = $ex; } } + $this->didRunLinters($linters); + + foreach ($linters as $linter) { + $minimum = $this->minimumSeverity; + foreach ($linter->getLintMessages() as $message) { + if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) { + continue; + } + if (!$this->isRelevantMessage($message)) { + continue; + } + if ($cache_granularity != ArcanistLinter::GRANULARITY_FILE) { + $message->setUncacheable(true); + } + $result = $this->getResultForPath($message->getPath()); + $result->addMessage($message); + } + } + if ($this->cachedResults) { foreach ($this->cachedResults as $path => $messages) { foreach (idx($messages, $this->cacheVersion, array()) as $message) { $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('Some linters failed:', $exceptions); } return $this->results; } /** * @param dict>> * @return this */ public function setCachedResults(array $results) { $this->cachedResults = $results; return $this; } public function getResults() { return $this->results; } abstract protected function buildLinters(); + protected function didRunLinters(array $linters) { + assert_instances_of($linters, 'ArcanistLinter'); + foreach ($linters as $linter) { + $linter->didRunLinters(); + } + } + 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()) { 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)) { 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; } 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]; } 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); } public function getPostponedLinters() { return $this->postponedLinters; } public function setPostponedLinters(array $linters) { $this->postponedLinters = $linters; return $this; } protected function getCacheVersion() { return 0; } protected function getPEP8WithTextOptions() { // E101 is subset of TXT2 (Tab Literal). // E501 is same as TXT3 (Line Too Long). // W291 is same as TXT6 (Trailing Whitespace). // W292 is same as TXT4 (File Does Not End in Newline). // W293 is same as TXT6 (Trailing Whitespace). return '--ignore=E101,E501,W291,W292,W293'; } } diff --git a/src/lint/linter/ArcanistLinter.php b/src/lint/linter/ArcanistLinter.php index eb2b9d9d..776dded8 100644 --- a/src/lint/linter/ArcanistLinter.php +++ b/src/lint/linter/ArcanistLinter.php @@ -1,221 +1,225 @@ customSeverityMap = $map; return $this; } public function setConfig(array $config) { $this->config = $config; return $this; } protected function getConfig($key, $default = null) { return idx($this->config, $key, $default); } public function getActivePath() { return $this->activePath; } public function stopAllLinters() { $this->stopAllLinters = true; return $this; } public function didStopAllLinters() { return $this->stopAllLinters; } public function addPath($path) { $this->paths[$path] = $path; return $this; } public function setPaths(array $paths) { $this->paths = $paths; return $this; } public function getPaths() { return array_values($this->paths); } public function addData($path, $data) { $this->data[$path] = $data; return $this; } protected function getData($path) { if (!array_key_exists($path, $this->data)) { $this->data[$path] = $this->getEngine()->loadData($path); } return $this->data[$path]; } public function setEngine(ArcanistLintEngine $engine) { $this->engine = $engine; return $this; } protected function getEngine() { return $this->engine; } public function getCacheVersion() { return 0; } public function getLintMessageFullCode($short_code) { return $this->getLinterName().$short_code; } public function getLintMessageSeverity($code) { $map = $this->customSeverityMap; if (isset($map[$code])) { return $map[$code]; } $map = $this->getLintSeverityMap(); if (isset($map[$code])) { return $map[$code]; } return ArcanistLintSeverity::SEVERITY_ERROR; } public function isMessageEnabled($code) { return ($this->getLintMessageSeverity($code) !== ArcanistLintSeverity::SEVERITY_DISABLED); } public function getLintMessageName($code) { $map = $this->getLintNameMap(); if (isset($map[$code])) { return $map[$code]; } return "Unknown lint message!"; } protected function addLintMessage(ArcanistLintMessage $message) { if (!$this->getEngine()->getCommitHookMode()) { $root = $this->getEngine()->getWorkingCopy()->getProjectRoot(); $path = Filesystem::resolvePath($message->getPath(), $root); $message->setPath(Filesystem::readablePath($path, $root)); } $this->messages[] = $message; return $message; } public function getLintMessages() { return $this->messages; } protected function newLintAtLine($line, $char, $code, $desc) { return id(new ArcanistLintMessage()) ->setPath($this->getActivePath()) ->setLine($line) ->setChar($char) ->setCode($this->getLintMessageFullCode($code)) ->setSeverity($this->getLintMessageSeverity($code)) ->setName($this->getLintMessageName($code)) ->setDescription($desc); } protected function raiseLintAtLine( $line, $char, $code, $desc, $original = null, $replacement = null) { $message = $this->newLintAtLine($line, $char, $code, $desc) ->setOriginalText($original) ->setReplacementText($replacement); return $this->addLintMessage($message); } protected function raiseLintAtPath( $code, $desc) { return $this->raiseLintAtLine(null, null, $code, $desc, null, null); } protected function raiseLintAtOffset( $offset, $code, $desc, $original = null, $replacement = null) { $path = $this->getActivePath(); $engine = $this->getEngine(); if ($offset === null) { $line = null; $char = null; } else { list($line, $char) = $engine->getLineAndCharFromOffset($path, $offset); } return $this->raiseLintAtLine( $line + 1, $char + 1, $code, $desc, $original, $replacement); } public function willLintPath($path) { $this->stopAllLinters = false; $this->activePath = $path; } public function canRun() { return true; } abstract public function willLintPaths(array $paths); abstract public function lintPath($path); abstract public function getLinterName(); + public function didRunLinters() { + // This is a hook. + } + public function getLintSeverityMap() { return array(); } public function getLintNameMap() { return array(); } public function getCacheGranularity() { return self::GRANULARITY_FILE; } }