Changeset View
Changeset View
Standalone View
Standalone View
src/workflow/ArcanistLintWorkflow.php
| <?php | <?php | ||||
| /** | /** | ||||
| * Runs lint rules on changes. | * Runs lint rules on changes. | ||||
| */ | */ | ||||
| final class ArcanistLintWorkflow extends ArcanistWorkflow { | final class ArcanistLintWorkflow extends ArcanistWorkflow { | ||||
| const RESULT_OKAY = 0; | const RESULT_OKAY = 0; | ||||
| const RESULT_WARNINGS = 1; | const RESULT_WARNINGS = 1; | ||||
| const RESULT_ERRORS = 2; | const RESULT_ERRORS = 2; | ||||
| const RESULT_SKIP = 3; | const RESULT_SKIP = 3; | ||||
| const DEFAULT_SEVERITY = ArcanistLintSeverity::SEVERITY_ADVICE; | const DEFAULT_SEVERITY = ArcanistLintSeverity::SEVERITY_ADVICE; | ||||
| private $unresolvedMessages; | private $unresolvedMessages; | ||||
| private $shouldLintAll; | |||||
| private $shouldAmendChanges = false; | private $shouldAmendChanges = false; | ||||
| private $shouldAmendWithoutPrompt = false; | private $shouldAmendWithoutPrompt = false; | ||||
| private $shouldAmendAutofixesWithoutPrompt = false; | private $shouldAmendAutofixesWithoutPrompt = false; | ||||
| private $engine; | private $engine; | ||||
| public function getWorkflowName() { | public function getWorkflowName() { | ||||
| return 'lint'; | return 'lint'; | ||||
| } | } | ||||
| Show All 31 Lines | EOTEXT | ||||
| } | } | ||||
| public function getArguments() { | public function getArguments() { | ||||
| return array( | return array( | ||||
| 'lintall' => array( | 'lintall' => array( | ||||
| 'help' => pht( | 'help' => pht( | ||||
| 'Show all lint warnings, not just those on changed lines. When '. | 'Show all lint warnings, not just those on changed lines. When '. | ||||
| 'paths are specified, this is the default behavior.'), | '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( | 'rev' => array( | ||||
| 'param' => 'revision', | 'param' => 'revision', | ||||
| 'help' => pht('Lint changes since a specific revision.'), | 'help' => pht('Lint changes since a specific revision.'), | ||||
| 'supports' => array( | 'supports' => array( | ||||
| 'git', | 'git', | ||||
| 'hg', | 'hg', | ||||
| ), | ), | ||||
| 'nosupport' => array( | 'nosupport' => array( | ||||
| 'svn' => pht('Lint does not currently support %s in SVN.', '--rev'), | 'svn' => pht('Lint does not currently support %s in SVN.', '--rev'), | ||||
| ), | ), | ||||
| ), | ), | ||||
| 'output' => array( | 'output' => array( | ||||
| 'param' => 'format', | 'param' => 'format', | ||||
| 'help' => pht( | 'help' => pht('Select an output format.'), | ||||
| "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."), | |||||
| ), | ), | ||||
| 'outfile' => array( | 'outfile' => array( | ||||
| 'param' => 'path', | 'param' => 'path', | ||||
| 'help' => pht( | 'help' => pht( | ||||
| 'Output the linter results to a file. Defaults to stdout.'), | '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( | 'engine' => array( | ||||
| 'param' => 'classname', | 'param' => 'classname', | ||||
| 'help' => pht('Override configured lint engine for this project.'), | 'help' => pht('Override configured lint engine for this project.'), | ||||
| ), | ), | ||||
| 'apply-patches' => array( | 'apply-patches' => array( | ||||
| 'help' => pht( | 'help' => pht( | ||||
| 'Apply patches suggested by lint to the working copy without '. | 'Apply patches suggested by lint to the working copy without '. | ||||
| 'prompting.'), | 'prompting.'), | ||||
| Show All 17 Lines | return array( | ||||
| 'When linting git repositories, amend HEAD with autofix '. | 'When linting git repositories, amend HEAD with autofix '. | ||||
| 'patches suggested by lint without prompting.'), | 'patches suggested by lint without prompting.'), | ||||
| ), | ), | ||||
| 'everything' => array( | 'everything' => array( | ||||
| 'help' => pht( | 'help' => pht( | ||||
| 'Lint all tracked files in the working copy. Ignored files and '. | 'Lint all tracked files in the working copy. Ignored files and '. | ||||
| 'untracked files will not be linted.'), | 'untracked files will not be linted.'), | ||||
| 'conflicts' => array( | 'conflicts' => array( | ||||
| 'cache' => pht('%s lints all files', '--everything'), | |||||
| 'rev' => pht('%s lints all files', '--everything'), | 'rev' => pht('%s lints all files', '--everything'), | ||||
| ), | ), | ||||
| ), | ), | ||||
| 'severity' => array( | 'severity' => array( | ||||
| 'param' => 'string', | 'param' => 'string', | ||||
| 'help' => pht( | 'help' => pht( | ||||
| "Set minimum message severity. One of: %s. Defaults to '%s'.", | "Set minimum message severity. One of: %s. Defaults to '%s'.", | ||||
| sprintf( | sprintf( | ||||
| "'%s'", | "'%s'", | ||||
| implode( | implode( | ||||
| "', '", | "', '", | ||||
| array_keys(ArcanistLintSeverity::getLintSeverities()))), | array_keys(ArcanistLintSeverity::getLintSeverities()))), | ||||
| self::DEFAULT_SEVERITY), | 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', | '*' => 'paths', | ||||
| ); | ); | ||||
| } | } | ||||
| public function requiresAuthentication() { | public function requiresAuthentication() { | ||||
| return (bool)$this->getArgument('only-new'); | return false; | ||||
| } | } | ||||
| public function requiresWorkingCopy() { | public function requiresWorkingCopy() { | ||||
| return true; | return true; | ||||
| } | } | ||||
| public function requiresRepositoryAPI() { | public function requiresRepositoryAPI() { | ||||
| return true; | return true; | ||||
| } | } | ||||
| private function getCacheKey() { | |||||
| return implode("\n", array( | |||||
| get_class($this->engine), | |||||
| $this->getArgument('severity', self::DEFAULT_SEVERITY), | |||||
| $this->shouldLintAll, | |||||
| )); | |||||
| } | |||||
| public function run() { | public function run() { | ||||
| $console = PhutilConsole::getConsole(); | $console = PhutilConsole::getConsole(); | ||||
| $working_copy = $this->getWorkingCopy(); | $working_copy = $this->getWorkingCopy(); | ||||
| $configuration_manager = $this->getConfigurationManager(); | $configuration_manager = $this->getConfigurationManager(); | ||||
| $engine = $this->newLintEngine($this->getArgument('engine')); | $engine = $this->newLintEngine($this->getArgument('engine')); | ||||
| $rev = $this->getArgument('rev'); | $rev = $this->getArgument('rev'); | ||||
| $paths = $this->getArgument('paths'); | $paths = $this->getArgument('paths'); | ||||
| $use_cache = $this->getArgument('cache', null); | |||||
| $everything = $this->getArgument('everything'); | $everything = $this->getArgument('everything'); | ||||
| if ($everything && $paths) { | if ($everything && $paths) { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| pht( | pht( | ||||
| 'You can not specify paths with %s. The %s flag lints every '. | 'You can not specify paths with %s. The %s flag lints every '. | ||||
| 'tracked file in the working copy.', | 'tracked file in the working copy.', | ||||
| '--everything', | '--everything', | ||||
| '--everything')); | '--everything')); | ||||
| } | } | ||||
| if ($use_cache === null) { | |||||
| $use_cache = (bool)$configuration_manager->getConfigFromAnySource( | |||||
| 'arc.lint.cache', | |||||
| false); | |||||
| } | |||||
| if ($rev && $paths) { | if ($rev !== null) { | ||||
| throw new ArcanistUsageException( | $this->parseBaseCommitArgument(array($rev)); | ||||
| pht('Specify either %s or paths.', '--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. | |||||
| // 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. | |||||
| // NOTE: When the user specifies paths, we imply --lintall and show all | // You can change this behavior with various flags, including "--lintall", | ||||
| // warnings for the paths in question. This is easier to deal with for | // "--rev", and "--everything". | ||||
| // us and less confusing for users. | |||||
| $this->shouldLintAll = $paths ? true : false; | |||||
| if ($this->getArgument('lintall')) { | if ($this->getArgument('lintall')) { | ||||
| $this->shouldLintAll = true; | $lint_all = true; | ||||
| } else if ($this->getArgument('only-changed')) { | } else if ($rev !== null) { | ||||
| $this->shouldLintAll = false; | $lint_all = false; | ||||
| } else if ($paths || $everything) { | |||||
| $lint_all = true; | |||||
| } else { | |||||
| $lint_all = false; | |||||
| } | } | ||||
| if ($everything) { | if ($everything) { | ||||
| $paths = iterator_to_array($this->getRepositoryAPI()->getAllFiles()); | $paths = iterator_to_array($this->getRepositoryAPI()->getAllFiles()); | ||||
| $this->shouldLintAll = true; | |||||
| } else { | } else { | ||||
| $paths = $this->selectPathsForWorkflow($paths, $rev); | $paths = $this->selectPathsForWorkflow($paths, $rev); | ||||
| } | } | ||||
| $this->engine = $engine; | $this->engine = $engine; | ||||
| $engine->setMinimumSeverity( | $engine->setMinimumSeverity( | ||||
| $this->getArgument('severity', self::DEFAULT_SEVERITY)); | $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. | // Propagate information about which lines changed to the lint engine. | ||||
| // This is used so that the lint engine can drop warning messages | // This is used so that the lint engine can drop warning messages | ||||
| // concerning lines that weren't in the change. | // concerning lines that weren't in the change. | ||||
| $engine->setPaths($paths); | $engine->setPaths($paths); | ||||
| if (!$this->shouldLintAll) { | if (!$lint_all) { | ||||
| foreach ($paths as $path) { | foreach ($paths as $path) { | ||||
| // Note that getChangedLines() returns null to indicate that a file | // Note that getChangedLines() returns null to indicate that a file | ||||
| // is binary or a directory (i.e., changed lines are not relevant). | // is binary or a directory (i.e., changed lines are not relevant). | ||||
| $engine->setPathChangedLines( | $engine->setPathChangedLines( | ||||
| $path, | $path, | ||||
| $this->getChangedLines($path, 'new')); | $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; | $failed = null; | ||||
| try { | try { | ||||
| $engine->run(); | $engine->run(); | ||||
| } catch (Exception $ex) { | } catch (Exception $ex) { | ||||
| $failed = $ex; | $failed = $ex; | ||||
| } | } | ||||
| $results = $engine->getResults(); | $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')) { | if ($this->getArgument('never-apply-patches')) { | ||||
| $apply_patches = false; | $apply_patches = false; | ||||
| } else { | } else { | ||||
| $apply_patches = true; | $apply_patches = true; | ||||
| } | } | ||||
| if ($this->getArgument('apply-patches')) { | if ($this->getArgument('apply-patches')) { | ||||
| $prompt_patches = false; | $prompt_patches = false; | ||||
| } else { | } else { | ||||
| $prompt_patches = true; | $prompt_patches = true; | ||||
| } | } | ||||
| if ($this->getArgument('amend-all')) { | if ($this->getArgument('amend-all')) { | ||||
| $this->shouldAmendChanges = true; | $this->shouldAmendChanges = true; | ||||
| $this->shouldAmendWithoutPrompt = true; | $this->shouldAmendWithoutPrompt = true; | ||||
| } | } | ||||
| if ($this->getArgument('amend-autofixes')) { | if ($this->getArgument('amend-autofixes')) { | ||||
| $prompt_autofix_patches = false; | |||||
| $this->shouldAmendChanges = true; | $this->shouldAmendChanges = true; | ||||
| $this->shouldAmendAutofixesWithoutPrompt = true; | $this->shouldAmendAutofixesWithoutPrompt = true; | ||||
| } else { | |||||
| $prompt_autofix_patches = true; | |||||
| } | } | ||||
| $repository_api = $this->getRepositoryAPI(); | $repository_api = $this->getRepositoryAPI(); | ||||
| if ($this->shouldAmendChanges) { | if ($this->shouldAmendChanges) { | ||||
| $this->shouldAmendChanges = $repository_api->supportsAmend() && | $this->shouldAmendChanges = $repository_api->supportsAmend() && | ||||
| !$this->isHistoryImmutable(); | !$this->isHistoryImmutable(); | ||||
| } | } | ||||
| $wrote_to_disk = false; | $wrote_to_disk = false; | ||||
| switch ($this->getArgument('output')) { | $default_renderer = ArcanistConsoleLintRenderer::RENDERERKEY; | ||||
| case 'json': | $renderer_key = $this->getArgument('output', $default_renderer); | ||||
| $renderer = new ArcanistJSONLintRenderer(); | |||||
| $prompt_patches = false; | $renderers = ArcanistLintRenderer::getAllRenderers(); | ||||
| $apply_patches = $this->getArgument('apply-patches'); | if (!isset($renderers[$renderer_key])) { | ||||
| break; | throw new Exception( | ||||
| case 'summary': | pht( | ||||
| $renderer = new ArcanistSummaryLintRenderer(); | 'Lint renderer "%s" is unknown. Supported renderers are: %s.', | ||||
| break; | $renderer_key, | ||||
| case 'none': | implode(', ', array_keys($renderers)))); | ||||
| $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; | |||||
| } | } | ||||
| $renderer = $renderers[$renderer_key]; | |||||
| $all_autofix = true; | $all_autofix = true; | ||||
| $tmp = null; | |||||
| if ($this->getArgument('outfile') !== null) { | $out_path = $this->getArgument('outfile'); | ||||
| $tmp = id(new TempFile()) | if ($out_path !== null) { | ||||
| ->setPreserveFile(true); | $tmp = new TempFile(); | ||||
| $renderer->setOutputPath((string)$tmp); | |||||
| } else { | |||||
| $tmp = null; | |||||
| } | } | ||||
| $preamble = $renderer->renderPreamble(); | if ($failed) { | ||||
| if ($tmp) { | $renderer->handleException($failed); | ||||
| Filesystem::appendFile($tmp, $preamble); | |||||
| } else { | |||||
| $console->writeOut('%s', $preamble); | |||||
| } | } | ||||
| foreach ($results as $result) { | $renderer->willRenderResults(); | ||||
| $result_all_autofix = $result->isAllAutofix(); | |||||
| if (!$result->getMessages() && !$result_all_autofix) { | $should_patch = ($apply_patches && $renderer->supportsPatching()); | ||||
| foreach ($results as $result) { | |||||
| if (!$result->getMessages()) { | |||||
| continue; | continue; | ||||
| } | } | ||||
| $result_all_autofix = $result->isAllAutofix(); | |||||
| if (!$result_all_autofix) { | if (!$result_all_autofix) { | ||||
| $all_autofix = false; | $all_autofix = false; | ||||
| } | } | ||||
| $lint_result = $renderer->renderLintResult($result); | $renderer->renderLintResult($result); | ||||
| if ($lint_result) { | |||||
| if ($tmp) { | |||||
| Filesystem::appendFile($tmp, $lint_result); | |||||
| } else { | |||||
| $console->writeOut('%s', $lint_result); | |||||
| } | |||||
| } | |||||
| if ($apply_patches && $result->isPatchable()) { | if ($should_patch && $result->isPatchable()) { | ||||
| $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result); | $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result); | ||||
| $old_file = $result->getFilePathOnDisk(); | |||||
| if ($prompt_patches && | $apply = true; | ||||
| !($result_all_autofix && !$prompt_autofix_patches)) { | if ($prompt_patches && !$result_all_autofix) { | ||||
| $old_file = $result->getFilePathOnDisk(); | |||||
| if (!Filesystem::pathExists($old_file)) { | if (!Filesystem::pathExists($old_file)) { | ||||
| $old_file = '/dev/null'; | $old_file = null; | ||||
| } | } | ||||
| $new_file = new TempFile(); | $new_file = new TempFile(); | ||||
| $new = $patcher->getModifiedFileContent(); | $new = $patcher->getModifiedFileContent(); | ||||
| Filesystem::writeFile($new_file, $new); | Filesystem::writeFile($new_file, $new); | ||||
| // TODO: Improve the behavior here, make it more like | $apply = $renderer->promptForPatch($result, $old_file, $new_file); | ||||
| // 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; | |||||
| } | |||||
| } | } | ||||
| if ($apply) { | |||||
| $patcher->writePatchToDisk(); | $patcher->writePatchToDisk(); | ||||
| $wrote_to_disk = true; | $wrote_to_disk = true; | ||||
| $file_hashes[$old_file] = md5_file($old_file); | } | ||||
| } | } | ||||
| } | } | ||||
| $postamble = $renderer->renderPostamble(); | $renderer->didRenderResults(); | ||||
| if ($tmp) { | if ($tmp) { | ||||
| Filesystem::appendFile($tmp, $postamble); | Filesystem::rename($tmp, $out_path); | ||||
| Filesystem::rename($tmp, $this->getArgument('outfile')); | |||||
| } else { | |||||
| $console->writeOut('%s', $postamble); | |||||
| } | } | ||||
| if ($wrote_to_disk && $this->shouldAmendChanges) { | if ($wrote_to_disk && $this->shouldAmendChanges) { | ||||
| if ($this->shouldAmendWithoutPrompt || | if ($this->shouldAmendWithoutPrompt || | ||||
| ($this->shouldAmendAutofixesWithoutPrompt && $all_autofix)) { | ($this->shouldAmendAutofixesWithoutPrompt && $all_autofix)) { | ||||
| $console->writeOut( | $console->writeOut( | ||||
| "<bg:yellow>** %s **</bg> %s\n", | "<bg:yellow>** %s **</bg> %s\n", | ||||
| pht('LINT NOTICE'), | pht('LINT NOTICE'), | ||||
| Show All 14 Lines | if ($wrote_to_disk && $this->shouldAmendChanges) { | ||||
| } else { | } else { | ||||
| throw new ArcanistUsageException( | throw new ArcanistUsageException( | ||||
| pht( | pht( | ||||
| 'Sort out the lint changes that were applied to the working '. | 'Sort out the lint changes that were applied to the working '. | ||||
| 'copy and relint.')); | '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(); | $unresolved = array(); | ||||
| $has_warnings = false; | $has_warnings = false; | ||||
| $has_errors = false; | $has_errors = false; | ||||
| foreach ($results as $result) { | foreach ($results as $result) { | ||||
| foreach ($result->getMessages() as $message) { | foreach ($result->getMessages() as $message) { | ||||
| if (!$message->isPatchApplied()) { | if (!$message->isPatchApplied()) { | ||||
| if ($message->isError()) { | if ($message->isError()) { | ||||
| $has_errors = true; | $has_errors = true; | ||||
| } else if ($message->isWarning()) { | } else if ($message->isWarning()) { | ||||
| $has_warnings = true; | $has_warnings = true; | ||||
| } | } | ||||
| $unresolved[] = $message; | $unresolved[] = $message; | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| $this->unresolvedMessages = $unresolved; | $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 | // Take the most severe lint message severity and use that | ||||
| // as the result code. | // as the result code. | ||||
| if ($has_errors) { | if ($has_errors) { | ||||
| $result_code = self::RESULT_ERRORS; | $result_code = self::RESULT_ERRORS; | ||||
| } else if ($has_warnings) { | } else if ($has_warnings) { | ||||
| $result_code = self::RESULT_WARNINGS; | $result_code = self::RESULT_WARNINGS; | ||||
| } else { | } else { | ||||
| $result_code = self::RESULT_OKAY; | $result_code = self::RESULT_OKAY; | ||||
| } | } | ||||
| if (!$this->getParentWorkflow()) { | $renderer->renderResultCode($result_code); | ||||
| if ($result_code == self::RESULT_OKAY) { | |||||
| $console->writeOut('%s', $renderer->renderOkayResult()); | |||||
| } | |||||
| } | |||||
| return $result_code; | return $result_code; | ||||
| } | } | ||||
| public function getUnresolvedMessages() { | public function getUnresolvedMessages() { | ||||
| return $this->unresolvedMessages; | return $this->unresolvedMessages; | ||||
| } | } | ||||
| } | } | ||||