2011-01-10 00:22:25 +01:00
|
|
|
<?php
|
|
|
|
|
2011-02-19 20:36:08 +01:00
|
|
|
/**
|
2011-06-30 04:32:03 +02:00
|
|
|
* 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
|
2012-11-21 00:37:31 +01:00
|
|
|
* key "lint.engine". The engine must extend this class. For example:
|
2011-06-30 04:32:03 +02:00
|
|
|
*
|
2013-11-14 19:19:08 +01:00
|
|
|
* lang=js
|
|
|
|
* {
|
|
|
|
* // ...
|
|
|
|
* "lint.engine" : "ExampleLintEngine",
|
|
|
|
* // ...
|
|
|
|
* }
|
2011-06-30 04:32:03 +02:00
|
|
|
*
|
|
|
|
* 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.
|
2011-02-19 20:36:08 +01:00
|
|
|
*
|
|
|
|
* @group lint
|
2012-01-31 21:07:05 +01:00
|
|
|
* @stable
|
2011-02-19 20:36:08 +01:00
|
|
|
*/
|
2011-01-10 00:22:25 +01:00
|
|
|
abstract class ArcanistLintEngine {
|
|
|
|
|
|
|
|
protected $workingCopy;
|
2012-11-02 20:35:09 +01:00
|
|
|
protected $paths = array();
|
2011-01-10 00:22:25 +01:00
|
|
|
protected $fileData = array();
|
|
|
|
|
|
|
|
protected $charToLine = array();
|
|
|
|
protected $lineToFirstChar = array();
|
2012-11-21 23:52:50 +01:00
|
|
|
private $cachedResults;
|
|
|
|
private $cacheVersion;
|
2013-01-23 22:06:45 +01:00
|
|
|
private $repositoryVersion;
|
2011-01-10 00:22:25 +01:00
|
|
|
private $results = array();
|
2013-01-15 22:47:11 +01:00
|
|
|
private $stopped = array();
|
2011-07-14 01:33:18 +02:00
|
|
|
private $minimumSeverity = ArcanistLintSeverity::SEVERITY_DISABLED;
|
2011-01-10 00:22:25 +01:00
|
|
|
|
|
|
|
private $changedLines = array();
|
2011-02-15 23:57:24 +01:00
|
|
|
private $commitHookMode = false;
|
[arc svn-hook-pre-commit] Access working copy
Summary:
Creates a new hook API that can be used to interface with
SVN/Git/Mercurial in the context of a commit hook. Currently only adds a
function to read the modified file data in a Subversion commit hook.
An object of this API is created in the SvnHookPreCommitWorkflow and
passed on the Lint Engine which then uses it to access current file
data, of the way the APIs seem to be structured); linters use the
getData function which is essentially a wrapper around the engine's
call, with another layer of caching.
Task ID: #770556
Blame Rev:
Test Plan:
- Create a local svn repository and add a minimal hook to run the local
version of arc to test commits
(http://phabricator.com/docs/arcanist/article/Installing_Arcanist_SVN_Hooks.html)
- Create a temporary repository that can trigger any of the linters
available, and test against a temporary linter by committing against
the test repository: the linter should be able to access all required
files by using loadData/getData in the LintEngine and Linter.
Revert Plan:
Tags: lint, svn-hook-pre-commit
Reviewers: jungejason, asukhachev, epriestley, aran
Reviewed By: epriestley
CC: aran, jungejason, epriestley, kunalb, asukhachev
Differential Revision: https://secure.phabricator.com/D1256
2011-12-21 05:26:05 +01:00
|
|
|
private $hookAPI;
|
2011-01-10 00:22:25 +01:00
|
|
|
|
2012-07-03 00:53:22 +02:00
|
|
|
private $enableAsyncLint = false;
|
|
|
|
private $postponedLinters = array();
|
2013-10-22 01:57:22 +02:00
|
|
|
private $configurationManager;
|
2012-07-03 00:53:22 +02:00
|
|
|
|
2014-05-12 04:28:46 +02:00
|
|
|
private $linterResources = array();
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
public function __construct() {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setConfigurationManager(
|
2013-10-22 01:57:22 +02:00
|
|
|
ArcanistConfigurationManager $configuration_manager) {
|
|
|
|
$this->configurationManager = $configuration_manager;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getConfigurationManager() {
|
2013-10-22 01:57:22 +02:00
|
|
|
return $this->configurationManager;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setWorkingCopy(
|
|
|
|
ArcanistWorkingCopyIdentity $working_copy) {
|
2011-01-10 00:22:25 +01:00
|
|
|
$this->workingCopy = $working_copy;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getWorkingCopy() {
|
2011-01-10 00:22:25 +01:00
|
|
|
return $this->workingCopy;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setPaths($paths) {
|
2011-01-10 00:22:25 +01:00
|
|
|
$this->paths = $paths;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPaths() {
|
|
|
|
return $this->paths;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setPathChangedLines($path, $changed) {
|
2011-11-03 21:42:19 +01:00
|
|
|
if ($changed === null) {
|
|
|
|
$this->changedLines[$path] = null;
|
|
|
|
} else {
|
|
|
|
$this->changedLines[$path] = array_fill_keys($changed, true);
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getPathChangedLines($path) {
|
2011-01-10 00:22:25 +01:00
|
|
|
return idx($this->changedLines, $path);
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setFileData($data) {
|
2011-02-15 23:57:24 +01:00
|
|
|
$this->fileData = $data + $this->fileData;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setCommitHookMode($mode) {
|
2011-02-15 23:57:24 +01:00
|
|
|
$this->commitHookMode = $mode;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setHookAPI(ArcanistHookAPI $hook_api) {
|
[arc svn-hook-pre-commit] Access working copy
Summary:
Creates a new hook API that can be used to interface with
SVN/Git/Mercurial in the context of a commit hook. Currently only adds a
function to read the modified file data in a Subversion commit hook.
An object of this API is created in the SvnHookPreCommitWorkflow and
passed on the Lint Engine which then uses it to access current file
data, of the way the APIs seem to be structured); linters use the
getData function which is essentially a wrapper around the engine's
call, with another layer of caching.
Task ID: #770556
Blame Rev:
Test Plan:
- Create a local svn repository and add a minimal hook to run the local
version of arc to test commits
(http://phabricator.com/docs/arcanist/article/Installing_Arcanist_SVN_Hooks.html)
- Create a temporary repository that can trigger any of the linters
available, and test against a temporary linter by committing against
the test repository: the linter should be able to access all required
files by using loadData/getData in the LintEngine and Linter.
Revert Plan:
Tags: lint, svn-hook-pre-commit
Reviewers: jungejason, asukhachev, epriestley, aran
Reviewed By: epriestley
CC: aran, jungejason, epriestley, kunalb, asukhachev
Differential Revision: https://secure.phabricator.com/D1256
2011-12-21 05:26:05 +01:00
|
|
|
$this->hookAPI = $hook_api;
|
2012-04-03 03:35:19 +02:00
|
|
|
return $this;
|
[arc svn-hook-pre-commit] Access working copy
Summary:
Creates a new hook API that can be used to interface with
SVN/Git/Mercurial in the context of a commit hook. Currently only adds a
function to read the modified file data in a Subversion commit hook.
An object of this API is created in the SvnHookPreCommitWorkflow and
passed on the Lint Engine which then uses it to access current file
data, of the way the APIs seem to be structured); linters use the
getData function which is essentially a wrapper around the engine's
call, with another layer of caching.
Task ID: #770556
Blame Rev:
Test Plan:
- Create a local svn repository and add a minimal hook to run the local
version of arc to test commits
(http://phabricator.com/docs/arcanist/article/Installing_Arcanist_SVN_Hooks.html)
- Create a temporary repository that can trigger any of the linters
available, and test against a temporary linter by committing against
the test repository: the linter should be able to access all required
files by using loadData/getData in the LintEngine and Linter.
Revert Plan:
Tags: lint, svn-hook-pre-commit
Reviewers: jungejason, asukhachev, epriestley, aran
Reviewed By: epriestley
CC: aran, jungejason, epriestley, kunalb, asukhachev
Differential Revision: https://secure.phabricator.com/D1256
2011-12-21 05:26:05 +01:00
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getHookAPI() {
|
[arc svn-hook-pre-commit] Access working copy
Summary:
Creates a new hook API that can be used to interface with
SVN/Git/Mercurial in the context of a commit hook. Currently only adds a
function to read the modified file data in a Subversion commit hook.
An object of this API is created in the SvnHookPreCommitWorkflow and
passed on the Lint Engine which then uses it to access current file
data, of the way the APIs seem to be structured); linters use the
getData function which is essentially a wrapper around the engine's
call, with another layer of caching.
Task ID: #770556
Blame Rev:
Test Plan:
- Create a local svn repository and add a minimal hook to run the local
version of arc to test commits
(http://phabricator.com/docs/arcanist/article/Installing_Arcanist_SVN_Hooks.html)
- Create a temporary repository that can trigger any of the linters
available, and test against a temporary linter by committing against
the test repository: the linter should be able to access all required
files by using loadData/getData in the LintEngine and Linter.
Revert Plan:
Tags: lint, svn-hook-pre-commit
Reviewers: jungejason, asukhachev, epriestley, aran
Reviewed By: epriestley
CC: aran, jungejason, epriestley, kunalb, asukhachev
Differential Revision: https://secure.phabricator.com/D1256
2011-12-21 05:26:05 +01:00
|
|
|
return $this->hookAPI;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setEnableAsyncLint($enable_async_lint) {
|
2012-07-03 00:53:22 +02:00
|
|
|
$this->enableAsyncLint = $enable_async_lint;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getEnableAsyncLint() {
|
2012-07-03 00:53:22 +02:00
|
|
|
return $this->enableAsyncLint;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function loadData($path) {
|
2011-01-10 00:22:25 +01:00
|
|
|
if (!isset($this->fileData[$path])) {
|
[arc svn-hook-pre-commit] Access working copy
Summary:
Creates a new hook API that can be used to interface with
SVN/Git/Mercurial in the context of a commit hook. Currently only adds a
function to read the modified file data in a Subversion commit hook.
An object of this API is created in the SvnHookPreCommitWorkflow and
passed on the Lint Engine which then uses it to access current file
data, of the way the APIs seem to be structured); linters use the
getData function which is essentially a wrapper around the engine's
call, with another layer of caching.
Task ID: #770556
Blame Rev:
Test Plan:
- Create a local svn repository and add a minimal hook to run the local
version of arc to test commits
(http://phabricator.com/docs/arcanist/article/Installing_Arcanist_SVN_Hooks.html)
- Create a temporary repository that can trigger any of the linters
available, and test against a temporary linter by committing against
the test repository: the linter should be able to access all required
files by using loadData/getData in the LintEngine and Linter.
Revert Plan:
Tags: lint, svn-hook-pre-commit
Reviewers: jungejason, asukhachev, epriestley, aran
Reviewed By: epriestley
CC: aran, jungejason, epriestley, kunalb, asukhachev
Differential Revision: https://secure.phabricator.com/D1256
2011-12-21 05:26:05 +01:00
|
|
|
if ($this->getCommitHookMode()) {
|
|
|
|
$this->fileData[$path] = $this->getHookAPI()
|
|
|
|
->getCurrentFileData($path);
|
|
|
|
} else {
|
|
|
|
$disk_path = $this->getFilePathOnDisk($path);
|
|
|
|
$this->fileData[$path] = Filesystem::readFile($disk_path);
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
return $this->fileData[$path];
|
|
|
|
}
|
|
|
|
|
2011-02-15 23:57:24 +01:00
|
|
|
public function pathExists($path) {
|
|
|
|
if ($this->getCommitHookMode()) {
|
2012-05-24 01:26:20 +02:00
|
|
|
$file_data = $this->loadData($path);
|
|
|
|
return ($file_data !== null);
|
2011-02-15 23:57:24 +01:00
|
|
|
} else {
|
|
|
|
$disk_path = $this->getFilePathOnDisk($path);
|
|
|
|
return Filesystem::pathExists($disk_path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function isDirectory($path) {
|
Support PHPCS as a `.arclint` linter
Summary:
Ref T3186. Ref T2039. Ref T3771. A few effects here:
# Expose PHPCS as a `.arclint` linter.
# Turn PHPCS into an ArcanistExternalLinter linter.
# Add test coverage for PHPCS.
# Add a `severity.rules` option to `.arclint`. Some linters have very explicit builtin severities ("error", "warning") but their meanings are different from how arc interprets these terms. For example, PHPCS raises "wrong indentation level" as an "error". You can already use the "severity" map to adjust individual rules, but if you want to adjust an entire linter it's currently difficult. This rule map makes it easy. There's substantial precedent for this in other linters, notably all the Python linters.
For `severity.rules`, for example, this will turn all PHPCS "errors" into warnings, and all of its warnings into advice:
"severity.rules" : {
"(^PHPCS\\.E\\.)" : "warning",
"(^PHPCS\\.W\\.)" : "advice"
}
The user can use `severity` (or more rules) to get additional granularity adjustments if they desire.
Test Plan: https://github.com/epriestley/arclint-examples/commit/5bb919bc3a684d6654dcc28622de9a29ec08307d
Reviewers: btrahan
Reviewed By: btrahan
CC: aran, ajtrichards
Maniphest Tasks: T2039, T3186, T3771
Differential Revision: https://secure.phabricator.com/D6830
2013-08-29 15:47:27 +02:00
|
|
|
if ($this->getCommitHookMode()) {
|
|
|
|
// TODO: This won't get the right result in every case (we need more
|
|
|
|
// metadata) but should almost always be correct.
|
|
|
|
try {
|
|
|
|
$this->loadData($path);
|
|
|
|
return false;
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$disk_path = $this->getFilePathOnDisk($path);
|
|
|
|
return is_dir($disk_path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function isBinaryFile($path) {
|
Support PHPCS as a `.arclint` linter
Summary:
Ref T3186. Ref T2039. Ref T3771. A few effects here:
# Expose PHPCS as a `.arclint` linter.
# Turn PHPCS into an ArcanistExternalLinter linter.
# Add test coverage for PHPCS.
# Add a `severity.rules` option to `.arclint`. Some linters have very explicit builtin severities ("error", "warning") but their meanings are different from how arc interprets these terms. For example, PHPCS raises "wrong indentation level" as an "error". You can already use the "severity" map to adjust individual rules, but if you want to adjust an entire linter it's currently difficult. This rule map makes it easy. There's substantial precedent for this in other linters, notably all the Python linters.
For `severity.rules`, for example, this will turn all PHPCS "errors" into warnings, and all of its warnings into advice:
"severity.rules" : {
"(^PHPCS\\.E\\.)" : "warning",
"(^PHPCS\\.W\\.)" : "advice"
}
The user can use `severity` (or more rules) to get additional granularity adjustments if they desire.
Test Plan: https://github.com/epriestley/arclint-examples/commit/5bb919bc3a684d6654dcc28622de9a29ec08307d
Reviewers: btrahan
Reviewed By: btrahan
CC: aran, ajtrichards
Maniphest Tasks: T2039, T3186, T3771
Differential Revision: https://secure.phabricator.com/D6830
2013-08-29 15:47:27 +02:00
|
|
|
try {
|
|
|
|
$data = $this->loadData($path);
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ArcanistDiffUtils::isHeuristicBinaryFile($data);
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getFilePathOnDisk($path) {
|
2011-01-10 05:40:13 +01:00
|
|
|
return Filesystem::resolvePath(
|
|
|
|
$path,
|
|
|
|
$this->getWorkingCopy()->getProjectRoot());
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setMinimumSeverity($severity) {
|
2011-01-10 00:22:25 +01:00
|
|
|
$this->minimumSeverity = $severity;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getCommitHookMode() {
|
2011-02-15 23:57:24 +01:00
|
|
|
return $this->commitHookMode;
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function run() {
|
2011-01-10 00:22:25 +01:00
|
|
|
$linters = $this->buildLinters();
|
2012-11-21 23:52:50 +01:00
|
|
|
if (!$linters) {
|
|
|
|
throw new ArcanistNoEffectException("No linters to run.");
|
|
|
|
}
|
|
|
|
|
Ready more linters and linter functions for .arclint
Summary:
Ref T3186. Ref T2039. Continues work on readying linters for `.arclint`.
- **Ruby**: Make this an ExternalLinter.
- **Priority**: Currently, linters have an implicit "correct" order (notably, the "NoLint" linter needs to run before other linters). Make this explicit by introducing `getLinterPriority()`.
- **Binaries**: Currently, linters manually reject binary files. Instead, reject binary files by default (linters can override this if they do want to lint binary files).
- **Deleted Files**: Currently, linters manually reject deleted files (usually in engines). Instead, reject deleted files by default (linters can override this).
- **Severity**: Move this `.arclint` config option up to top level.
- **willLintPaths()**: This method is abstract, but almost all linters provide a trivial implementation. Provide a trivial implementation in the base class.
- **getLintSeverityMap()/getLintNameMap()**: A bunch of linters have empty implementations; these are redundant. Remove them.
- **Spelling**: clean up some dead / test-only / unconventional code.
- **`.arclint`**: Allow the filename, generated, nolint, text, spelling and ruby linters to be configured via `.arclint`.
Test Plan:
https://github.com/epriestley/arclint-examples/commit/458beca3d65b64d52ed612904ae66eb837118b94
Ran unit tests.
Reviewers: btrahan
Reviewed By: btrahan
CC: Firehed, aran
Maniphest Tasks: T2039, T3186
Differential Revision: https://secure.phabricator.com/D6805
2013-08-26 14:37:10 +02:00
|
|
|
$linters = msort($linters, 'getLinterPriority');
|
|
|
|
foreach ($linters as $linter) {
|
|
|
|
$linter->setEngine($this);
|
|
|
|
}
|
|
|
|
|
2012-11-21 23:52:50 +01:00
|
|
|
$have_paths = false;
|
|
|
|
foreach ($linters as $linter) {
|
|
|
|
if ($linter->getPaths()) {
|
|
|
|
$have_paths = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$have_paths) {
|
|
|
|
throw new ArcanistNoEffectException("No paths are lintable.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$versions = array($this->getCacheVersion());
|
2013-02-27 00:20:13 +01:00
|
|
|
|
2012-11-21 23:52:50 +01:00
|
|
|
foreach ($linters as $linter) {
|
2013-02-27 00:20:13 +01:00
|
|
|
$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;
|
2012-11-21 23:52:50 +01:00
|
|
|
}
|
2013-02-27 00:20:13 +01:00
|
|
|
|
2012-11-21 23:52:50 +01:00
|
|
|
$this->cacheVersion = crc32(implode("\n", $versions));
|
|
|
|
|
2013-01-15 22:47:11 +01:00
|
|
|
$this->stopped = array();
|
2013-01-09 22:00:45 +01:00
|
|
|
$exceptions = array();
|
|
|
|
foreach ($linters as $linter_name => $linter) {
|
2013-01-15 22:47:11 +01:00
|
|
|
if (!is_string($linter_name)) {
|
|
|
|
$linter_name = get_class($linter);
|
|
|
|
}
|
2013-01-09 22:00:45 +01:00
|
|
|
try {
|
2013-01-11 23:11:38 +01:00
|
|
|
if (!$linter->canRun()) {
|
|
|
|
continue;
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
2013-01-11 23:11:38 +01:00
|
|
|
$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);
|
2013-01-15 22:47:11 +01:00
|
|
|
if (isset($this->stopped[$path])) {
|
2013-01-11 23:11:38 +01:00
|
|
|
unset($paths[$key]);
|
2012-10-20 13:42:53 +02:00
|
|
|
}
|
2013-01-11 23:11:38 +01:00
|
|
|
if (isset($this->cachedResults[$path][$this->cacheVersion])) {
|
2013-01-23 22:06:45 +01:00
|
|
|
$cached_result = $this->cachedResults[$path][$this->cacheVersion];
|
|
|
|
|
2013-02-07 00:27:31 +01:00
|
|
|
$use_cache = $this->shouldUseCache(
|
|
|
|
$linter->getCacheGranularity(),
|
|
|
|
idx($cached_result, 'repository_version'));
|
2013-01-23 22:06:45 +01:00
|
|
|
|
|
|
|
if ($use_cache) {
|
2013-01-11 23:11:38 +01:00
|
|
|
unset($paths[$key]);
|
2013-01-15 22:47:11 +01:00
|
|
|
|
2013-01-23 22:06:45 +01:00
|
|
|
if (idx($cached_result, 'stopped') == $linter_name) {
|
2013-01-15 22:47:11 +01:00
|
|
|
$this->stopped[$path] = $linter_name;
|
|
|
|
}
|
2013-01-11 23:11:38 +01:00
|
|
|
}
|
2012-10-20 13:42:53 +02:00
|
|
|
}
|
2013-01-11 23:11:38 +01:00
|
|
|
}
|
|
|
|
$paths = array_values($paths);
|
|
|
|
|
|
|
|
if ($paths) {
|
2013-02-05 02:25:37 +01:00
|
|
|
$profiler = PhutilServiceProfiler::getInstance();
|
2013-02-22 18:21:07 +01:00
|
|
|
$call_id = $profiler->beginServiceCall(array(
|
|
|
|
'type' => 'lint',
|
2013-02-26 20:35:58 +01:00
|
|
|
'linter' => $linter_name,
|
2013-02-22 18:21:07 +01:00
|
|
|
'paths' => $paths,
|
|
|
|
));
|
|
|
|
|
|
|
|
try {
|
|
|
|
$linter->willLintPaths($paths);
|
|
|
|
foreach ($paths as $path) {
|
|
|
|
$linter->willLintPath($path);
|
2013-02-05 02:25:37 +01:00
|
|
|
$linter->lintPath($path);
|
2013-02-22 18:21:07 +01:00
|
|
|
if ($linter->didStopAllLinters()) {
|
|
|
|
$this->stopped[$path] = $linter_name;
|
|
|
|
}
|
2013-02-05 02:25:37 +01:00
|
|
|
}
|
2013-02-22 18:21:07 +01:00
|
|
|
} catch (Exception $ex) {
|
2013-02-05 02:25:37 +01:00
|
|
|
$profiler->endServiceCall($call_id, array());
|
2013-02-22 18:21:07 +01:00
|
|
|
throw $ex;
|
2012-11-22 03:38:24 +01:00
|
|
|
}
|
2013-02-22 18:21:07 +01:00
|
|
|
$profiler->endServiceCall($call_id, array());
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
2013-01-11 23:11:38 +01:00
|
|
|
|
2012-10-20 13:42:53 +02:00
|
|
|
} catch (Exception $ex) {
|
2012-10-20 16:11:02 +02:00
|
|
|
$exceptions[$linter_name] = $ex;
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-02-26 20:35:58 +01:00
|
|
|
$exceptions += $this->didRunLinters($linters);
|
2013-01-11 23:11:38 +01:00
|
|
|
|
|
|
|
foreach ($linters as $linter) {
|
|
|
|
foreach ($linter->getLintMessages() as $message) {
|
2013-02-15 00:18:39 +01:00
|
|
|
if (!$this->isSeverityEnabled($message->getSeverity())) {
|
2013-01-11 23:11:38 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!$this->isRelevantMessage($message)) {
|
|
|
|
continue;
|
|
|
|
}
|
2013-02-07 00:27:31 +01:00
|
|
|
$message->setGranularity($linter->getCacheGranularity());
|
2013-01-11 23:11:38 +01:00
|
|
|
$result = $this->getResultForPath($message->getPath());
|
|
|
|
$result->addMessage($message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-11-21 23:52:50 +01:00
|
|
|
if ($this->cachedResults) {
|
|
|
|
foreach ($this->cachedResults as $path => $messages) {
|
2013-01-15 22:47:11 +01:00
|
|
|
$messages = idx($messages, $this->cacheVersion, array());
|
2013-02-07 00:27:31 +01:00
|
|
|
$repository_version = idx($messages, 'repository_version');
|
2013-01-15 22:47:11 +01:00
|
|
|
unset($messages['stopped']);
|
2013-01-23 22:06:45 +01:00
|
|
|
unset($messages['repository_version']);
|
2013-01-15 22:47:11 +01:00
|
|
|
foreach ($messages as $message) {
|
2013-02-07 00:27:31 +01:00
|
|
|
$use_cache = $this->shouldUseCache(
|
|
|
|
idx($message, 'granularity'),
|
|
|
|
$repository_version);
|
|
|
|
if ($use_cache) {
|
|
|
|
$this->getResultForPath($path)->addMessage(
|
|
|
|
ArcanistLintMessage::newFromDictionary($message));
|
|
|
|
}
|
2012-11-21 23:52:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
foreach ($this->results as $path => $result) {
|
2011-04-11 01:05:44 +02:00
|
|
|
$disk_path = $this->getFilePathOnDisk($path);
|
|
|
|
$result->setFilePathOnDisk($disk_path);
|
2011-01-10 00:22:25 +01:00
|
|
|
if (isset($this->fileData[$path])) {
|
2011-04-11 01:05:44 +02:00
|
|
|
$result->setData($this->fileData[$path]);
|
2011-04-11 23:53:48 +02:00
|
|
|
} else if ($disk_path && Filesystem::pathExists($disk_path)) {
|
2011-04-11 01:05:44 +02:00
|
|
|
// 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.
|
2011-04-12 01:13:22 +02:00
|
|
|
try {
|
'arc liberate', convenience wrapper for various libphutil operations
Summary:
The story for creating and maintaining libphutil libraries and modules
is pretty terrible right now: you need to know a bunch of secret scripts and
dark magic. Provide 'arc liberate' which endeavors to always do the right thing
and put a library in the correct state.
Test Plan:
Ran liberate on libphutil, arcanist, phabricator; created new
libphutil libraries, added classes to them, liberated everything, introduced
errors etc and liberated that stuff, nothing was obviously broken in a terrible
way..?
Reviewed By: aran
Reviewers: jungejason, tuomaspelkonen, aran
CC: aran, epriestley
Differential Revision: 269
2011-05-12 01:30:22 +02:00
|
|
|
$this->fileData[$path] = Filesystem::readFile($disk_path);
|
2011-04-12 01:13:22 +02:00
|
|
|
$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.
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-10-20 13:42:53 +02:00
|
|
|
if ($exceptions) {
|
|
|
|
throw new PhutilAggregateException('Some linters failed:', $exceptions);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->results;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function isSeverityEnabled($severity) {
|
2013-02-15 00:18:39 +01:00
|
|
|
$minimum = $this->minimumSeverity;
|
|
|
|
return ArcanistLintSeverity::isAtLeastAsSevere($severity, $minimum);
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final private function shouldUseCache($cache_granularity,
|
|
|
|
$repository_version) {
|
2013-04-25 00:10:27 +02:00
|
|
|
if ($this->commitHookMode) {
|
|
|
|
return false;
|
|
|
|
}
|
2013-02-07 00:27:31 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-11-21 23:52:50 +01:00
|
|
|
/**
|
2013-01-15 22:47:11 +01:00
|
|
|
* @param dict<string path, dict<string version, list<dict message>>>
|
2012-11-21 23:52:50 +01:00
|
|
|
* @return this
|
|
|
|
*/
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setCachedResults(array $results) {
|
2012-11-21 23:52:50 +01:00
|
|
|
$this->cachedResults = $results;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getResults() {
|
2011-01-10 00:22:25 +01:00
|
|
|
return $this->results;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getStoppedPaths() {
|
2013-01-15 22:47:11 +01:00
|
|
|
return $this->stopped;
|
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
abstract protected function buildLinters();
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final protected function didRunLinters(array $linters) {
|
2013-01-11 23:11:38 +01:00
|
|
|
assert_instances_of($linters, 'ArcanistLinter');
|
2013-02-22 18:21:07 +01:00
|
|
|
|
2013-02-26 20:35:58 +01:00
|
|
|
$exceptions = array();
|
2013-02-22 18:21:07 +01:00
|
|
|
$profiler = PhutilServiceProfiler::getInstance();
|
2013-02-26 20:35:58 +01:00
|
|
|
|
|
|
|
foreach ($linters as $linter_name => $linter) {
|
|
|
|
if (!is_string($linter_name)) {
|
|
|
|
$linter_name = get_class($linter);
|
|
|
|
}
|
|
|
|
|
2013-02-22 18:21:07 +01:00
|
|
|
$call_id = $profiler->beginServiceCall(array(
|
|
|
|
'type' => 'lint',
|
2013-02-26 20:35:58 +01:00
|
|
|
'linter' => $linter_name,
|
2013-02-22 18:21:07 +01:00
|
|
|
));
|
2013-02-26 20:35:58 +01:00
|
|
|
|
|
|
|
try {
|
|
|
|
$linter->didRunLinters();
|
|
|
|
} catch (Exception $ex) {
|
|
|
|
$exceptions[$linter_name] = $ex;
|
|
|
|
}
|
2013-02-22 18:21:07 +01:00
|
|
|
$profiler->endServiceCall($call_id, array());
|
2013-01-11 23:11:38 +01:00
|
|
|
}
|
2013-02-26 20:35:58 +01:00
|
|
|
|
|
|
|
return $exceptions;
|
2013-01-11 23:11:38 +01:00
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setRepositoryVersion($version) {
|
2013-01-23 22:06:45 +01:00
|
|
|
$this->repositoryVersion = $version;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final private function isRelevantMessage(ArcanistLintMessage $message) {
|
2012-07-22 21:52:10 +02:00
|
|
|
// 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.
|
|
|
|
|
2013-06-09 17:37:40 +02:00
|
|
|
if (!$this->changedLines ||
|
|
|
|
$message->isError() ||
|
|
|
|
$message->shouldBypassChangedLineFiltering()) {
|
2012-07-22 21:52:10 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2013-01-11 00:03:33 +01:00
|
|
|
$locations = $message->getOtherLocations();
|
|
|
|
$locations[] = $message->toDictionary();
|
|
|
|
|
|
|
|
foreach ($locations as $location) {
|
|
|
|
$path = idx($location, 'path', $message->getPath());
|
|
|
|
|
|
|
|
if (!array_key_exists($path, $this->changedLines)) {
|
|
|
|
continue;
|
|
|
|
}
|
2012-07-22 21:52:10 +02:00
|
|
|
|
2013-01-11 00:03:33 +01:00
|
|
|
$changed = $this->getPathChangedLines($path);
|
|
|
|
|
|
|
|
if ($changed === null || !$location['line']) {
|
2012-07-22 21:52:10 +02:00
|
|
|
return true;
|
|
|
|
}
|
2013-01-11 00:03:33 +01:00
|
|
|
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
}
|
2012-07-22 21:52:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final protected function getResultForPath($path) {
|
2011-01-10 00:22:25 +01:00
|
|
|
if (empty($this->results[$path])) {
|
|
|
|
$result = new ArcanistLintResult();
|
|
|
|
$result->setPath($path);
|
2012-11-21 23:52:50 +01:00
|
|
|
$result->setCacheVersion($this->cacheVersion);
|
2011-01-10 00:22:25 +01:00
|
|
|
$this->results[$path] = $result;
|
|
|
|
}
|
|
|
|
return $this->results[$path];
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getLineAndCharFromOffset($path, $offset) {
|
2011-01-10 00:22:25 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function getPostponedLinters() {
|
2012-07-03 00:53:22 +02:00
|
|
|
return $this->postponedLinters;
|
|
|
|
}
|
|
|
|
|
2014-05-18 20:08:29 +02:00
|
|
|
final public function setPostponedLinters(array $linters) {
|
2012-07-03 00:53:22 +02:00
|
|
|
$this->postponedLinters = $linters;
|
|
|
|
return $this;
|
|
|
|
}
|
2011-01-10 00:22:25 +01:00
|
|
|
|
2012-11-21 23:52:50 +01:00
|
|
|
protected function getCacheVersion() {
|
2013-01-15 22:47:11 +01:00
|
|
|
return 1;
|
2012-11-21 01:43:10 +01:00
|
|
|
}
|
|
|
|
|
Ready more linters and linter functions for .arclint
Summary:
Ref T3186. Ref T2039. Continues work on readying linters for `.arclint`.
- **Ruby**: Make this an ExternalLinter.
- **Priority**: Currently, linters have an implicit "correct" order (notably, the "NoLint" linter needs to run before other linters). Make this explicit by introducing `getLinterPriority()`.
- **Binaries**: Currently, linters manually reject binary files. Instead, reject binary files by default (linters can override this if they do want to lint binary files).
- **Deleted Files**: Currently, linters manually reject deleted files (usually in engines). Instead, reject deleted files by default (linters can override this).
- **Severity**: Move this `.arclint` config option up to top level.
- **willLintPaths()**: This method is abstract, but almost all linters provide a trivial implementation. Provide a trivial implementation in the base class.
- **getLintSeverityMap()/getLintNameMap()**: A bunch of linters have empty implementations; these are redundant. Remove them.
- **Spelling**: clean up some dead / test-only / unconventional code.
- **`.arclint`**: Allow the filename, generated, nolint, text, spelling and ruby linters to be configured via `.arclint`.
Test Plan:
https://github.com/epriestley/arclint-examples/commit/458beca3d65b64d52ed612904ae66eb837118b94
Ran unit tests.
Reviewers: btrahan
Reviewed By: btrahan
CC: Firehed, aran
Maniphest Tasks: T2039, T3186
Differential Revision: https://secure.phabricator.com/D6805
2013-08-26 14:37:10 +02:00
|
|
|
|
2014-05-12 04:28:46 +02:00
|
|
|
/**
|
|
|
|
* 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 accesss.
|
|
|
|
*
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|