Page MenuHomeSealhub

No OneTemporary

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 @@
<?php
/**
* Manages lint execution. When you run 'arc lint' or 'arc diff', Arcanist
* attempts to run lint rules using a lint engine.
*
* Lint engines are high-level strategic classes which do not contain any
* actual linting rules. Linting rules live in `Linter` classes. The lint
* engine builds and configures linters.
*
* Most modern linters can be configured with an `.arclint` file, which is
* managed by the builtin @{class:ArcanistConfigurationDrivenLintEngine}.
* Consult the documentation for more information on these files.
*
* In the majority of cases, you do not need to write a custom lint engine.
* For example, to add new rules for a new language, write a linter instead.
* However, if you have a very advanced or specialized use case, you can write
* a custom lint engine by extending this class; custom lint engines are more
* powerful but much more complex than the builtin engines.
*
* The lint engine is given a list of paths (generally, the paths that you
* modified in your change) and determines which linters to run on them. The
* linters themselves are responsible for actually analyzing file text and
* finding warnings and errors. For example, if the modified paths include some
* JS files and some Python files, you might want to run JSLint on the JS files
* and PyLint on the Python files.
*
* You can also run multiple linters on a single file. For instance, you might
* run one linter on all text files to make sure they don't have trailing
* whitespace, or enforce tab vs space rules, or make sure there are enough
* curse words in them.
*
* You can test an engine like this:
*
* arc lint --engine YourLintEngineClassName --lintall some_file.py
*
* ...which will show you all the lint issues raised in the file.
*
* See @{article@phabricator:Arcanist User Guide: Customizing Lint, Unit Tests
* and Workflows} for more information about configuring lint engines.
*/
abstract class ArcanistLintEngine extends Phobject {
protected $workingCopy;
protected $paths = array();
protected $fileData = array();
protected $charToLine = array();
protected $lineToFirstChar = array();
private $cachedResults;
private $cacheVersion;
private $repositoryVersion;
private $results = array();
private $stopped = array();
private $minimumSeverity = ArcanistLintSeverity::SEVERITY_DISABLED;
private $changedLines = array();
- private $enableAsyncLint = false;
private $configurationManager;
private $linterResources = array();
public function __construct() {}
final public function setConfigurationManager(
ArcanistConfigurationManager $configuration_manager) {
$this->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<string path, dict<string version, list<dict message>>>
* @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 @@
<?php
-/**
- * Shows lint messages to the user.
- */
final class ArcanistCheckstyleXMLLintRenderer extends ArcanistLintRenderer {
+ const RENDERERKEY = 'xml';
+
private $writer;
public function __construct() {
$this->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 @@
<?php
-/**
- * Shows lint messages to the user.
- */
final class ArcanistCompilerLintRenderer extends ArcanistLintRenderer {
+ const RENDERERKEY = 'compiler';
+
public function renderLintResult(ArcanistLintResult $result) {
$lines = array();
$messages = $result->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 @@
<?php
-/**
- * Shows lint messages to the user.
- */
final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer {
- private $showAutofixPatches = false;
- private $testableMode;
+ const RENDERERKEY = 'console';
- public function setShowAutofixPatches($show_autofix_patches) {
- $this->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(
" **<bg:{$color}> %s </bg>** (%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(
- "<bg:green>** %s **</bg> %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 @@
<?php
-/**
- * Shows lint messages to the user.
- */
final class ArcanistJSONLintRenderer extends ArcanistLintRenderer {
+ const RENDERERKEY = 'json';
+
const LINES_OF_CONTEXT = 3;
public function renderLintResult(ArcanistLintResult $result) {
$messages = $result->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 @@
<?php
-/**
- * Shows lint messages to the user.
- */
abstract class ArcanistLintRenderer extends Phobject {
- public function renderPreamble() {
- return '';
+ private $output;
+
+ final public function getRendererKey() {
+ return $this->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 @@
<?php
final class ArcanistNoneLintRenderer extends ArcanistLintRenderer {
- public function renderLintResult(ArcanistLintResult $result) {
- return '';
- }
+ const RENDERERKEY = 'none';
- public function renderOkayResult() {
- return '';
+ public function renderLintResult(ArcanistLintResult $result) {
+ return null;
}
}
diff --git a/src/lint/renderer/ArcanistSummaryLintRenderer.php b/src/lint/renderer/ArcanistSummaryLintRenderer.php
index 92eed3a4..22c6aba0 100644
--- a/src/lint/renderer/ArcanistSummaryLintRenderer.php
+++ b/src/lint/renderer/ArcanistSummaryLintRenderer.php
@@ -1,32 +1,33 @@
<?php
-/**
- * Shows lint messages to the user.
- */
final class ArcanistSummaryLintRenderer extends ArcanistLintRenderer {
+ const RENDERERKEY = 'summary';
+
public function renderLintResult(ArcanistLintResult $result) {
$messages = $result->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(
- "<bg:green>** %s **</bg> %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 @@
<?php
final class ArcanistConsoleLintRendererTestCase
extends PhutilTestCase {
public function testRendering() {
$midline_original = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$midline_replacement = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$remline_original = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$remline_replacement = <<<EOTEXT
import apple;
import banana;
import cat;
import dog;
EOTEXT;
$map = array(
'simple' => 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 @@
<?php
/**
* Runs lint rules on changes.
*/
final class ArcanistLintWorkflow extends ArcanistWorkflow {
const RESULT_OKAY = 0;
const RESULT_WARNINGS = 1;
const RESULT_ERRORS = 2;
const RESULT_SKIP = 3;
const DEFAULT_SEVERITY = ArcanistLintSeverity::SEVERITY_ADVICE;
private $unresolvedMessages;
- private $shouldLintAll;
private $shouldAmendChanges = false;
private $shouldAmendWithoutPrompt = false;
private $shouldAmendAutofixesWithoutPrompt = false;
private $engine;
public function getWorkflowName() {
return 'lint';
}
public function setShouldAmendChanges($should_amend) {
$this->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(<<<EOTEXT
**lint** [__options__] [__paths__]
**lint** [__options__] --rev [__rev__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
Run static analysis on changes to check for mistakes. If no files
are specified, lint will be run on all files which have been modified.
EOTEXT
);
}
public function getArguments() {
return array(
'lintall' => 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 <file>", 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(
"<bg:yellow>** %s **</bg> %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;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 8, 06:35 (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034191
Default Alt Text
(67 KB)

Event Timeline