Changeset View
Changeset View
Standalone View
Standalone View
src/lint/engine/ArcanistLintEngine.php
Show First 20 Lines • Show All 206 Lines • ▼ Show 20 Lines | abstract class ArcanistLintEngine { | ||||
} | } | ||||
final public function run() { | final public function run() { | ||||
$linters = $this->buildLinters(); | $linters = $this->buildLinters(); | ||||
if (!$linters) { | if (!$linters) { | ||||
throw new ArcanistNoEffectException('No linters to run.'); | throw new ArcanistNoEffectException('No linters to run.'); | ||||
} | } | ||||
foreach ($linters as $key => $linter) { | |||||
$linter->setLinterID($key); | |||||
} | |||||
$linters = msort($linters, 'getLinterPriority'); | $linters = msort($linters, 'getLinterPriority'); | ||||
foreach ($linters as $linter) { | foreach ($linters as $linter) { | ||||
$linter->setEngine($this); | $linter->setEngine($this); | ||||
} | } | ||||
$have_paths = false; | $have_paths = false; | ||||
foreach ($linters as $linter) { | foreach ($linters as $linter) { | ||||
if ($linter->getPaths()) { | if ($linter->getPaths()) { | ||||
Show All 21 Lines | foreach ($linters as $linter) { | ||||
phutil_get_library_root($symbol['library']).'/'.$symbol['where']); | phutil_get_library_root($symbol['library']).'/'.$symbol['where']); | ||||
} | } | ||||
$versions[] = $version; | $versions[] = $version; | ||||
} | } | ||||
$this->cacheVersion = crc32(implode("\n", $versions)); | $this->cacheVersion = crc32(implode("\n", $versions)); | ||||
$this->stopped = array(); | $runnable = $this->getRunnableLinters($linters); | ||||
joshuaspence: This is essentially just `mfilter($linters, 'canRun')`. | |||||
$exceptions = array(); | |||||
foreach ($linters as $linter_name => $linter) { | |||||
if (!is_string($linter_name)) { | |||||
$linter_name = get_class($linter); | |||||
} | |||||
try { | |||||
if (!$linter->canRun()) { | |||||
continue; | |||||
} | |||||
$paths = $linter->getPaths(); | |||||
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->stopped[$path])) { | |||||
unset($paths[$key]); | |||||
} | |||||
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_name) { | |||||
$this->stopped[$path] = $linter_name; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
$paths = array_values($paths); | |||||
if ($paths) { | |||||
$profiler = PhutilServiceProfiler::getInstance(); | |||||
$call_id = $profiler->beginServiceCall(array( | |||||
'type' => 'lint', | |||||
'linter' => $linter_name, | |||||
'paths' => $paths, | |||||
)); | |||||
try { | |||||
$linter->willLintPaths($paths); | |||||
foreach ($paths as $path) { | |||||
$linter->willLintPath($path); | |||||
$linter->lintPath($path); | |||||
if ($linter->didStopAllLinters()) { | |||||
$this->stopped[$path] = $linter_name; | |||||
} | |||||
} | |||||
} catch (Exception $ex) { | |||||
$profiler->endServiceCall($call_id, array()); | |||||
throw $ex; | |||||
} | |||||
$profiler->endServiceCall($call_id, array()); | |||||
} | |||||
} catch (Exception $ex) { | $this->stopped = array(); | ||||
$exceptions[$linter_name] = $ex; | |||||
} | |||||
} | |||||
$exceptions += $this->didRunLinters($linters); | $exceptions = $this->executeLinters($runnable); | ||||
foreach ($linters as $linter) { | foreach ($runnable as $linter) { | ||||
foreach ($linter->getLintMessages() as $message) { | foreach ($linter->getLintMessages() as $message) { | ||||
if (!$this->isSeverityEnabled($message->getSeverity())) { | if (!$this->isSeverityEnabled($message->getSeverity())) { | ||||
continue; | continue; | ||||
} | } | ||||
if (!$this->isRelevantMessage($message)) { | if (!$this->isRelevantMessage($message)) { | ||||
continue; | continue; | ||||
} | } | ||||
$message->setGranularity($linter->getCacheGranularity()); | $message->setGranularity($linter->getCacheGranularity()); | ||||
▲ Show 20 Lines • Show All 85 Lines • ▼ Show 20 Lines | abstract class ArcanistLintEngine { | ||||
} | } | ||||
final public function getStoppedPaths() { | final public function getStoppedPaths() { | ||||
return $this->stopped; | return $this->stopped; | ||||
} | } | ||||
abstract public function buildLinters(); | abstract public function buildLinters(); | ||||
final protected function didRunLinters(array $linters) { | |||||
assert_instances_of($linters, 'ArcanistLinter'); | |||||
$exceptions = array(); | |||||
$profiler = PhutilServiceProfiler::getInstance(); | |||||
foreach ($linters as $linter_name => $linter) { | |||||
if (!is_string($linter_name)) { | |||||
$linter_name = get_class($linter); | |||||
} | |||||
$call_id = $profiler->beginServiceCall(array( | |||||
'type' => 'lint', | |||||
'linter' => $linter_name, | |||||
)); | |||||
try { | |||||
$linter->didRunLinters(); | |||||
} catch (Exception $ex) { | |||||
$exceptions[$linter_name] = $ex; | |||||
} | |||||
$profiler->endServiceCall($call_id, array()); | |||||
} | |||||
return $exceptions; | |||||
} | |||||
final public function setRepositoryVersion($version) { | final public function setRepositoryVersion($version) { | ||||
$this->repositoryVersion = $version; | $this->repositoryVersion = $version; | ||||
return $this; | return $this; | ||||
} | } | ||||
final private function isRelevantMessage(ArcanistLintMessage $message) { | final private function isRelevantMessage(ArcanistLintMessage $message) { | ||||
// When a user runs "arc lint", we default to raising only warnings on | // When a user runs "arc lint", we default to raising only warnings on | ||||
// lines they have changed (errors are still raised anywhere in the | // lines they have changed (errors are still raised anywhere in the | ||||
▲ Show 20 Lines • Show All 117 Lines • ▼ Show 20 Lines | abstract class ArcanistLintEngine { | ||||
* @param wild Resource. | * @param wild Resource. | ||||
* @return this | * @return this | ||||
*/ | */ | ||||
public function setLinterResource($key, $value) { | public function setLinterResource($key, $value) { | ||||
$this->linterResources[$key] = $value; | $this->linterResources[$key] = $value; | ||||
return $this; | 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) { | |||||
Not Done Inline ActionsMaybe add assert_instances_of($runnable, 'ArcanistLinter') joshuaspence: Maybe add `assert_instances_of($runnable, 'ArcanistLinter')` | |||||
$all_paths = $this->getPaths(); | |||||
$path_chunks = array_chunk($all_paths, 32, $preserve_keys = true); | |||||
Not Done Inline ActionsWe can worry about this later, but it might be nice to expose the chunk size as a parameter. Particularly for repository with very large files, or machines with very limited memory. joshuaspence: We can worry about this later, but it might be nice to expose the chunk size as a parameter. | |||||
Not Done Inline ActionsYeah, I think at some point we could add some getChunkSizeLimit() method to Linter and use the minimum value from all of the runnable linters, with the default returning null to mean "use whatever the default behavior is". epriestley: Yeah, I think at some point we could add some `getChunkSizeLimit()` method to `Linter` and use… | |||||
$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(), | |||||
Not Done Inline ActionsThere could be multiple linters of the same class configured, so maybe it would be better to use $linter->getID()? joshuaspence: There could be multiple linters of the same class configured, so maybe it would be better to… | |||||
Not Done Inline ActionsLinter IDs aren't always meaningful and --trace is primarily consumed by humans, but it's possible that there are reasons why that might be a better choice. epriestley: Linter IDs aren't always meaningful and `--trace` is primarily consumed by humans, but it's… | |||||
'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); | |||||
} | |||||
} | } |
This is essentially just mfilter($linters, 'canRun').