1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-10-24 17:48:50 +02:00
phorge-arcanist/src/workflow/ArcanistLintWorkflow.php

452 lines
13 KiB
PHP
Raw Normal View History

2011-01-10 00:22:25 +01:00
<?php
/**
* Runs lint rules on changes.
*
* @group workflow
*/
2011-01-10 00:22:25 +01:00
class ArcanistLintWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_WARNINGS = 1;
const RESULT_ERRORS = 2;
const RESULT_SKIP = 3;
const RESULT_POSTPONED = 4;
2011-01-10 00:22:25 +01:00
const DEFAULT_SEVERITY = ArcanistLintSeverity::SEVERITY_ADVICE;
private $unresolvedMessages;
private $shouldAmendChanges = false;
private $shouldAmendWithoutPrompt = false;
private $shouldAmendAutofixesWithoutPrompt = false;
private $engine;
private $postponedLinters;
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() {
2011-01-10 00:22:25 +01:00
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
2011-01-10 00:22:25 +01:00
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' =>
"Show all lint warnings, not just those on changed lines."
),
'rev' => array(
'param' => 'revision',
'help' => "Lint changes since a specific revision.",
'supports' => array(
'git',
'hg',
),
'nosupport' => array(
'svn' => "Lint does not currently support --rev in SVN.",
),
),
'output' => array(
'param' => 'format',
2011-01-10 00:22:25 +01:00
'help' =>
"With 'summary', show lint warnings in a more compact format. ".
"With 'json', show lint warnings in machine-readable JSON format. ".
"With 'compiler', show lint warnings in suitable for your editor."
2011-01-10 00:22:25 +01:00
),
'engine' => array(
'param' => 'classname',
'help' =>
"Override configured lint engine for this project."
),
'apply-patches' => array(
'help' =>
'Apply patches suggested by lint to the working copy without '.
'prompting.',
'conflicts' => array(
'never-apply-patches' => true,
),
),
'never-apply-patches' => array(
'help' => 'Never apply patches suggested by lint.',
'conflicts' => array(
'apply-patches' => true,
),
),
'amend-all' => array(
'help' =>
'When linting git repositories, amend HEAD with all patches '.
'suggested by lint without prompting.',
),
'amend-autofixes' => array(
'help' =>
'When linting git repositories, amend HEAD with autofix '.
'patches suggested by lint without prompting.',
),
'severity' => array(
'param' => 'string',
'help' =>
"Set minimum message severity. One of: '".
implode(
"', '",
array_keys(ArcanistLintSeverity::getLintSeverities())).
"'. Defaults to '".self::DEFAULT_SEVERITY."'.",
),
'cache' => array(
'param' => 'bool',
'help' => "0 to disable cache (default), 1 to enable.",
),
2011-01-10 00:22:25 +01:00
'*' => 'paths',
);
}
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->getArgument('lintall'),
$this->engine->getCacheVersion(),
));
}
2011-01-10 00:22:25 +01:00
public function run() {
$working_copy = $this->getWorkingCopy();
$engine = $this->getArgument('engine');
if (!$engine) {
Allow global config to load libraries and set test engines Summary: Khan Academy is looking into lint configuration, but doesn't use ".arcconfig" because they have a large number of repositories. Making configuration more flexible generally gives us more options for onboarding installs. - Currently, only project config (".arcconfig") can load libraries. Allow user config ("~/.arcrc") to load libraries as well. - Currently, only project config can set lint/unit engines. Allow user config to set default lint/unit engines. - Add some type checking to "arc set-config". - Add "arc set-config --show". Test Plan: - **load** - Ran `arc set-config load xxx`, got error about format. - Ran `arc set-config load ["apple"]`, got warning on running 'arc' commands (no such library) but was able to run 'arc set-config' again to clear it. - Ran `arc set-config load ["/path/to/a/lib/src/"]`, worked. - Ran `arc list --trace`, verified my library loaded in addition to `.arcconfig` libraries. - Ran `arc list --load-phutil-library=xxx --trace`, verified only that library loaded. - Ran `arc list --trace --load-phutil-library=apple --trace`, got hard error about bad library. - Set `.arcconfig` to point at a bad library, verified hard error. - **lint.engine** / **unit.engine** - Removed lint engine from `.arcconfig`, ran "arc lint", got a run with specified engine. - Removed unit engine from `.arcconfig`, ran "arc unit", got a run with specified engine. - **--show** - Ran `arc set-config --show`. - **misc** - Ran `arc get-config`. Reviewers: csilvers, btrahan, vrana Reviewed By: csilvers CC: aran Differential Revision: https://secure.phabricator.com/D2618
2012-05-31 20:41:39 +02:00
$engine = $working_copy->getConfigFromAnySource('lint.engine');
if (!$engine) {
throw new ArcanistNoEngineException(
"No lint engine configured for this project. Edit .arcconfig to ".
"specify a lint engine.");
}
2011-01-10 00:22:25 +01:00
}
$rev = $this->getArgument('rev');
$paths = $this->getArgument('paths');
2011-01-10 00:22:25 +01:00
if ($rev && $paths) {
throw new ArcanistUsageException("Specify either --rev or paths.");
}
2011-01-10 00:22:25 +01:00
$should_lint_all = $this->getArgument('lintall');
if ($paths) {
// 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.
$should_lint_all = true;
2011-01-10 00:22:25 +01:00
}
$paths = $this->selectPathsForWorkflow($paths, $rev);
if (!class_exists($engine) ||
!is_subclass_of($engine, 'ArcanistLintEngine')) {
throw new ArcanistUsageException(
"Configured lint engine '{$engine}' is not a subclass of ".
"'ArcanistLintEngine'.");
}
2011-01-10 00:22:25 +01:00
$engine = newv($engine, array());
$this->engine = $engine;
2011-01-10 00:22:25 +01:00
$engine->setWorkingCopy($working_copy);
$engine->setMinimumSeverity(
$this->getArgument('severity', self::DEFAULT_SEVERITY));
2011-01-10 00:22:25 +01:00
$cached = false;
if ($this->getArgument('cache')) {
$paths = array_combine($paths, $paths);
$cache = $this->readScratchJSONFile('lint-cache.json');
$cache = idx($cache, $this->getCacheKey(), array());
foreach ($cache as $path => $messages) {
$messages = idx($messages, md5_file($engine->getFilePathOnDisk($path)));
// TODO: Some linters work with the whole directory.
if ($messages !== null) {
foreach ($messages as $message) {
$engine->getResultForPath($path)->addMessage(
ArcanistLintMessage::newFromDictionary($message));
}
$cached = true;
unset($paths[$path]);
}
}
}
// 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.
2011-01-10 00:22:25 +01:00
$engine->setPaths($paths);
if (!$should_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'));
2011-01-10 00:22:25 +01:00
}
}
2012-11-02 00:21:12 +01:00
// Enable possible async linting only for 'arc diff' not 'arc lint'
if ($this->getParentWorkflow()) {
$engine->setEnableAsyncLint(true);
} else {
$engine->setEnableAsyncLint(false);
}
$failed = null;
try {
$engine->run();
} catch (Exception $ex) {
if ($ex instanceof ArcanistNoEffectException && $cached) {
// Swallow.
} else {
$failed = $ex;
}
}
$results = $engine->getResults();
2011-01-10 00:22:25 +01:00
// It'd be nice to just return a single result from the run method above
// which contains both the lint messages and the postponed linters.
// However, to maintain compatibility with existing lint subclasses, use
// a separate method call to grab the postponed linters.
$this->postponedLinters = $engine->getPostponedLinters();
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;
}
2011-01-10 00:22:25 +01:00
$wrote_to_disk = false;
switch ($this->getArgument('output')) {
case 'json':
$renderer = new ArcanistLintJSONRenderer();
$prompt_patches = false;
$apply_patches = $this->getArgument('apply-patches');
break;
case 'summary':
$renderer = new ArcanistLintSummaryRenderer();
break;
case 'compiler':
$renderer = new ArcanistLintLikeCompilerRenderer();
$prompt_patches = false;
$apply_patches = $this->getArgument('apply-patches');
break;
default:
$renderer = new ArcanistLintConsoleRenderer();
$renderer->setShowAutofixPatches($prompt_autofix_patches);
break;
2011-01-10 00:22:25 +01:00
}
$all_autofix = true;
$console = PhutilConsole::getConsole();
2011-01-10 00:22:25 +01:00
foreach ($results as $result) {
$result_all_autofix = $result->isAllAutofix();
if (!$result->getMessages() && !$result_all_autofix) {
2011-01-10 00:22:25 +01:00
continue;
}
if (!$result_all_autofix) {
$all_autofix = false;
}
$lint_result = $renderer->renderLintResult($result);
if ($lint_result) {
$console->writeOut('%s', $lint_result);
}
2011-01-10 00:22:25 +01:00
if ($apply_patches && $result->isPatchable()) {
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
if ($prompt_patches &&
!($result_all_autofix && !$prompt_autofix_patches)) {
2011-01-10 00:22:25 +01:00
$old_file = $result->getFilePathOnDisk();
if (!Filesystem::pathExists($old_file)) {
$old_file = '/dev/null';
}
2011-01-10 00:22:25 +01:00
$new_file = new TempFile();
2012-06-02 08:33:58 +02:00
$new = $patcher->getModifiedFileContent();
2011-01-10 00:22:25 +01:00
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);
2011-01-10 00:22:25 +01:00
$prompt = phutil_console_format(
"Apply this patch to __%s__?",
$result->getPath());
if (!$console->confirm($prompt, $default_no = false)) {
2011-01-10 00:22:25 +01:00
continue;
}
}
$patcher->writePatchToDisk();
$wrote_to_disk = true;
}
}
$repository_api = $this->getRepositoryAPI();
if ($wrote_to_disk &&
($repository_api instanceof ArcanistGitAPI) &&
$this->shouldAmendChanges) {
if ($this->shouldAmendWithoutPrompt ||
($this->shouldAmendAutofixesWithoutPrompt && $all_autofix)) {
$console->writeOut(
"<bg:yellow>** LINT NOTICE **</bg> Automatically amending HEAD ".
"with lint patches.\n");
$amend = true;
} else {
$amend = $console->confirm("Amend HEAD with lint patches?");
}
if ($amend) {
execx(
'(cd %s; git commit -a --amend -C HEAD)',
$repository_api->getPath());
} else {
throw new ArcanistUsageException(
"Sort out the lint changes that were applied to the working ".
"copy and relint.");
2011-01-10 00:22:25 +01:00
}
}
if ($this->getArgument('output') == 'json') {
// NOTE: Required by save_lint.php in Phabricator.
return 0;
}
if ($failed) {
throw $failed;
}
$unresolved = array();
$has_warnings = false;
$has_errors = false;
2011-01-10 00:22:25 +01:00
foreach ($results as $result) {
foreach ($result->getMessages() as $message) {
if (!$message->isPatchApplied()) {
if ($message->isError()) {
$has_errors = true;
2011-01-10 00:22:25 +01:00
} else if ($message->isWarning()) {
$has_warnings = true;
2011-01-10 00:22:25 +01:00
}
$unresolved[] = $message;
2011-01-10 00:22:25 +01:00
}
}
}
$this->unresolvedMessages = $unresolved;
2011-01-10 00:22:25 +01:00
if ($this->getArgument('cache')) {
$cached = array();
foreach ($results as $result) {
$path = $result->getPath();
$hash = md5_file($engine->getFilePathOnDisk($path));
$cached[$path] = array($hash => array());
foreach ($result->getMessages() as $message) {
if (!$message->isPatchApplied()) {
$cached[$path][$hash][] = $message->toDictionary();
}
}
}
$cache = $this->readScratchJSONFile('lint-cache.json');
$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 if (!empty($this->postponedLinters)) {
$result_code = self::RESULT_POSTPONED;
} else {
$result_code = self::RESULT_OKAY;
}
2011-01-10 00:22:25 +01:00
if (!$this->getParentWorkflow()) {
if ($result_code == self::RESULT_OKAY) {
$console->writeOut('%s', $renderer->renderOkayResult());
2011-01-10 00:22:25 +01:00
}
}
return $result_code;
}
public function getUnresolvedMessages() {
return $this->unresolvedMessages;
}
public function getPostponedLinters() {
return $this->postponedLinters;
}
2011-01-10 00:22:25 +01:00
}