Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F10360415
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
67 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment