Page MenuHomePhorge

No OneTemporary

diff --git a/src/lint/engine/ArcanistLintEngine.php b/src/lint/engine/ArcanistLintEngine.php
index 59a05d2c..8ea0755b 100644
--- a/src/lint/engine/ArcanistLintEngine.php
+++ b/src/lint/engine/ArcanistLintEngine.php
@@ -1,338 +1,351 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Manages lint execution. When you run 'arc lint' or 'arc diff', Arcanist
* checks your .arcconfig to see if you have specified a lint engine in the
* key "lint_engine". The engine must extend this class. For example:
*
* lang=js
* {
* // ...
* "lint_engine" : "ExampleLintEngine",
* // ...
* }
*
* 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.
*
* Because lint engines are pretty custom to the rules of a project, you will
* generally need to build your own. Fortunately, it's pretty easy (and you
* can use the prebuilt //linters//, you just need to write a little glue code
* to tell Arcanist which linters to run). For a simple example of how to build
* a lint engine, see @{class:ExampleLintEngine}.
*
* You can test an engine like this:
*
* arc lint --engine ExampleLintEngine --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.
*
* @group lint
* @stable
*/
abstract class ArcanistLintEngine {
protected $workingCopy;
protected $fileData = array();
protected $charToLine = array();
protected $lineToFirstChar = array();
private $results = array();
private $minimumSeverity = ArcanistLintSeverity::SEVERITY_DISABLED;
private $changedLines = array();
private $commitHookMode = false;
private $hookAPI;
private $enableAsyncLint = false;
private $postponedLinters = array();
public function __construct() {
}
public function setWorkingCopy(ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
public function getWorkingCopy() {
return $this->workingCopy;
}
public function setPaths($paths) {
$this->paths = $paths;
return $this;
}
public function getPaths() {
return $this->paths;
}
public function setPathChangedLines($path, $changed) {
if ($changed === null) {
$this->changedLines[$path] = null;
} else {
$this->changedLines[$path] = array_fill_keys($changed, true);
}
return $this;
}
public function getPathChangedLines($path) {
return idx($this->changedLines, $path);
}
public function setFileData($data) {
$this->fileData = $data + $this->fileData;
return $this;
}
public function setCommitHookMode($mode) {
$this->commitHookMode = $mode;
return $this;
}
public function setHookAPI(ArcanistHookAPI $hook_api) {
$this->hookAPI = $hook_api;
return $this;
}
public function getHookAPI() {
return $this->hookAPI;
}
public function setEnableAsyncLint($enable_async_lint) {
$this->enableAsyncLint = $enable_async_lint;
return $this;
}
public function getEnableAsyncLint() {
return $this->enableAsyncLint;
}
public function loadData($path) {
if (!isset($this->fileData[$path])) {
if ($this->getCommitHookMode()) {
$this->fileData[$path] = $this->getHookAPI()
->getCurrentFileData($path);
} else {
$disk_path = $this->getFilePathOnDisk($path);
$this->fileData[$path] = Filesystem::readFile($disk_path);
}
}
return $this->fileData[$path];
}
public function pathExists($path) {
if ($this->getCommitHookMode()) {
$file_data = $this->loadData($path);
return ($file_data !== null);
} else {
$disk_path = $this->getFilePathOnDisk($path);
return Filesystem::pathExists($disk_path);
}
}
public function getFilePathOnDisk($path) {
return Filesystem::resolvePath(
$path,
$this->getWorkingCopy()->getProjectRoot());
}
public function setMinimumSeverity($severity) {
$this->minimumSeverity = $severity;
return $this;
}
public function getCommitHookMode() {
return $this->commitHookMode;
}
public function run() {
$stopped = array();
$linters = $this->buildLinters();
if (!$linters) {
throw new ArcanistNoEffectException("No linters to run.");
}
$have_paths = false;
foreach ($linters as $linter) {
if ($linter->getPaths()) {
$have_paths = true;
break;
}
}
if (!$have_paths) {
throw new ArcanistNoEffectException("No paths are lintable.");
}
+ $exceptions = array();
foreach ($linters as $linter) {
- $linter->setEngine($this);
- if (!$linter->canRun()) {
- continue;
- }
- $paths = $linter->getPaths();
-
- foreach ($paths as $key => $path) {
- // Make sure each path has a result generated, even if it is empty
- // (i.e., the file has no lint messages).
- $result = $this->getResultForPath($path);
- if (isset($stopped[$path])) {
- unset($paths[$key]);
+ try {
+ $linter->setEngine($this);
+ if (!$linter->canRun()) {
+ continue;
}
- }
- $paths = array_values($paths);
-
- if ($paths) {
- $linter->willLintPaths($paths);
- foreach ($paths as $path) {
- $linter->willLintPath($path);
- $linter->lintPath($path);
- if ($linter->didStopAllLinters()) {
- $stopped[$path] = true;
+ $paths = $linter->getPaths();
+
+ foreach ($paths as $key => $path) {
+ // Make sure each path has a result generated, even if it is empty
+ // (i.e., the file has no lint messages).
+ $result = $this->getResultForPath($path);
+ if (isset($stopped[$path])) {
+ unset($paths[$key]);
}
}
- }
-
- $minimum = $this->minimumSeverity;
- foreach ($linter->getLintMessages() as $message) {
- if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) {
- continue;
+ $paths = array_values($paths);
+
+ if ($paths) {
+ $linter->willLintPaths($paths);
+ foreach ($paths as $path) {
+ $linter->willLintPath($path);
+ $linter->lintPath($path);
+ if ($linter->didStopAllLinters()) {
+ $stopped[$path] = true;
+ }
+ }
}
- if (!$this->isRelevantMessage($message)) {
- continue;
+
+ $minimum = $this->minimumSeverity;
+ foreach ($linter->getLintMessages() as $message) {
+ if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) {
+ continue;
+ }
+ if (!$this->isRelevantMessage($message)) {
+ continue;
+ }
+ $result = $this->getResultForPath($message->getPath());
+ $result->addMessage($message);
}
- $result = $this->getResultForPath($message->getPath());
- $result->addMessage($message);
+ } catch (Exception $ex) {
+ $exceptions[] = $ex;
}
}
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('Some linters failed:', $exceptions);
+ }
+
+ return $this->results;
+ }
+
+ public function getResults() {
return $this->results;
}
abstract protected function buildLinters();
private function isRelevantMessage($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.
$changed = $this->getPathChangedLines($message->getPath());
if ($changed === null || $message->isError() || !$message->getLine()) {
return true;
}
$last_line = $message->getLine();
if ($message->getOriginalText()) {
$last_line += substr_count($message->getOriginalText(), "\n");
}
for ($l = $message->getLine(); $l <= $last_line; $l++) {
if (!empty($changed[$l])) {
return true;
}
}
return false;
}
private function getResultForPath($path) {
if (empty($this->results[$path])) {
$result = new ArcanistLintResult();
$result->setPath($path);
$this->results[$path] = $result;
}
return $this->results[$path];
}
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);
}
public function getPostponedLinters() {
return $this->postponedLinters;
}
public function setPostponedLinters(array $linters) {
$this->postponedLinters = $linters;
return $this;
}
}
diff --git a/src/workflow/ArcanistLintWorkflow.php b/src/workflow/ArcanistLintWorkflow.php
index 06e284a0..4c9c11a2 100644
--- a/src/workflow/ArcanistLintWorkflow.php
+++ b/src/workflow/ArcanistLintWorkflow.php
@@ -1,385 +1,396 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Runs lint rules on changes.
*
* @group workflow
*/
class ArcanistLintWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_WARNINGS = 1;
const RESULT_ERRORS = 2;
const RESULT_SKIP = 3;
const RESULT_POSTPONED = 4;
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() {
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' =>
"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',
'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."
),
'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.',
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function run() {
$working_copy = $this->getWorkingCopy();
$engine = $this->getArgument('engine');
if (!$engine) {
$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.");
}
}
$rev = $this->getArgument('rev');
$paths = $this->getArgument('paths');
if ($rev && $paths) {
throw new ArcanistUsageException("Specify either --rev or paths.");
}
$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;
}
$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'.");
}
$engine = newv($engine, array());
$this->engine = $engine;
$engine->setWorkingCopy($working_copy);
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ADVICE);
// 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 (!$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'));
}
}
// Enable possible async linting only for 'arc diff' not 'arc unit'
if ($this->getParentWorkflow()) {
$engine->setEnableAsyncLint(true);
} else {
$engine->setEnableAsyncLint(false);
}
- $results = $engine->run();
+ $failed = null;
+ try {
+ $engine->run();
+ } catch (Exception $ex) {
+ $failed = $ex;
+ }
+
+ $results = $engine->getResults();
// 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;
}
$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;
}
$all_autofix = true;
$console = PhutilConsole::getConsole();
foreach ($results as $result) {
$result_all_autofix = $result->isAllAutofix();
if (!$result->getMessages() && !$result_all_autofix) {
continue;
}
if (!$result_all_autofix) {
$all_autofix = false;
}
$lint_result = $renderer->renderLintResult($result);
if ($lint_result) {
$console->writeOut('%s', $lint_result);
}
if ($apply_patches && $result->isPatchable()) {
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
if ($prompt_patches &&
!($result_all_autofix && !$prompt_autofix_patches)) {
$old_file = $result->getFilePathOnDisk();
if (!Filesystem::pathExists($old_file)) {
$old_file = '/dev/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 = phutil_console_format(
"Apply this patch to __%s__?",
$result->getPath());
if (!$console->confirm($prompt, $default_no = false)) {
continue;
}
}
$patcher->writePatchToDisk();
$wrote_to_disk = true;
}
}
+ if ($failed) {
+ throw $failed;
+ }
+
$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.");
}
}
$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;
// 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;
}
if (!$this->getParentWorkflow()) {
if ($result_code == self::RESULT_OKAY) {
$console->writeOut('%s', $renderer->renderOkayResult());
}
}
return $result_code;
}
public function getUnresolvedMessages() {
return $this->unresolvedMessages;
}
public function getPostponedLinters() {
return $this->postponedLinters;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 12:19 (3 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1124491
Default Alt Text
(23 KB)

Event Timeline