mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-26 00:32:41 +01:00
Add 'arc linters' to list available linters and status
Summary: Ref T2039. We're starting to get kind of a lot of linters; provide `arc linters` to help users review and understand them and construct `.arclint` files. Test Plan: {F152205} Reviewers: btrahan, joshuaspence Reviewed By: btrahan, joshuaspence Subscribers: epriestley Maniphest Tasks: T2039 Differential Revision: https://secure.phabricator.com/D9041
This commit is contained in:
parent
c4985ef415
commit
e13f5839d4
12 changed files with 353 additions and 43 deletions
|
@ -104,6 +104,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistLintWorkflow' => 'workflow/ArcanistLintWorkflow.php',
|
||||
'ArcanistLinter' => 'lint/linter/ArcanistLinter.php',
|
||||
'ArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistLinterTestCase.php',
|
||||
'ArcanistLintersWorkflow' => 'workflow/ArcanistLintersWorkflow.php',
|
||||
'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php',
|
||||
'ArcanistMarkCommittedWorkflow' => 'workflow/ArcanistMarkCommittedWorkflow.php',
|
||||
'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php',
|
||||
|
@ -266,6 +267,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistLintSummaryRenderer' => 'ArcanistLintRenderer',
|
||||
'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
|
||||
'ArcanistLintersWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
|
||||
|
|
|
@ -381,10 +381,6 @@ abstract class ArcanistExternalLinter extends ArcanistFutureLinter {
|
|||
}
|
||||
}
|
||||
|
||||
public function getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function buildFutures(array $paths) {
|
||||
$executable = $this->getExecutableCommand();
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
/**
|
||||
* Implements lint rules, like syntax checks for a specific language.
|
||||
*
|
||||
* @group linter
|
||||
* @task info Human Readable Information
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
abstract class ArcanistLinter {
|
||||
|
@ -25,6 +26,54 @@ abstract class ArcanistLinter {
|
|||
private $customSeverityRules = array();
|
||||
private $config = array();
|
||||
|
||||
|
||||
/* -( Human Readable Information )---------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Return an optional informative URI where humans can learn more about this
|
||||
* linter.
|
||||
*
|
||||
* For most linters, this should return a link to the project home page. This
|
||||
* is shown on `arc linters`.
|
||||
*
|
||||
* @return string|null Optionally, return an informative URI.
|
||||
* @task info
|
||||
*/
|
||||
public function getInfoURI() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a brief human-readable description of the linter.
|
||||
*
|
||||
* These should be a line or two, and are shown on `arc linters`.
|
||||
*
|
||||
* @return string|null Optionally, return a brief human-readable description.
|
||||
* @task info
|
||||
*/
|
||||
public function getInfoDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a human-readable linter name.
|
||||
*
|
||||
* These are used by `arc linters`, and can let you give a linter a more
|
||||
* presentable name.
|
||||
*
|
||||
* @return string Human-readable linter name.
|
||||
* @task info
|
||||
*/
|
||||
public function getInfoName() {
|
||||
return nonempty(
|
||||
$this->getLinterName(),
|
||||
$this->getLinterConfigurationName(),
|
||||
get_class($this));
|
||||
}
|
||||
|
||||
public function getLinterPriority() {
|
||||
return 1.0;
|
||||
}
|
||||
|
@ -268,6 +317,10 @@ abstract class ArcanistLinter {
|
|||
abstract public function lintPath($path);
|
||||
abstract public function getLinterName();
|
||||
|
||||
public function getVersion() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function didRunLinters() {
|
||||
// This is a hook.
|
||||
}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Stops other linters from running on code marked with
|
||||
* a nolint annotation.
|
||||
*
|
||||
* @group linter
|
||||
* Stops other linters from running on code marked with a nolint annotation.
|
||||
*/
|
||||
final class ArcanistNoLintLinter extends ArcanistLinter {
|
||||
|
||||
public function getInfoName() {
|
||||
return pht('Lint Disabler');
|
||||
}
|
||||
|
||||
public function getInfoDescription() {
|
||||
return pht(
|
||||
'Allows you to disable all lint messages for a file by putting "%s" in '.
|
||||
'the file body.',
|
||||
'@'.'nolint');
|
||||
}
|
||||
|
||||
public function getLinterName() {
|
||||
return 'NOLINT';
|
||||
}
|
||||
|
|
|
@ -2,11 +2,23 @@
|
|||
|
||||
/**
|
||||
* Uses "pep8.py" to enforce PEP8 rules for Python.
|
||||
*
|
||||
* @group linter
|
||||
*/
|
||||
final class ArcanistPEP8Linter extends ArcanistExternalLinter {
|
||||
|
||||
public function getInfoName() {
|
||||
return 'pep8';
|
||||
}
|
||||
|
||||
public function getInfoURI() {
|
||||
return 'https://pypi.python.org/pypi/pep8';
|
||||
}
|
||||
|
||||
public function getInfoDescription() {
|
||||
return pht(
|
||||
'pep8 is a tool to check your Python code against some of the '.
|
||||
'style conventions in PEP 8.');
|
||||
}
|
||||
|
||||
public function getLinterName() {
|
||||
return 'PEP8';
|
||||
}
|
||||
|
|
|
@ -17,6 +17,20 @@ final class ArcanistPhpcsLinter extends ArcanistExternalLinter {
|
|||
|
||||
private $reports;
|
||||
|
||||
public function getInfoName() {
|
||||
return 'PHP_CodeSniffer';
|
||||
}
|
||||
|
||||
public function getInfoURI() {
|
||||
return 'http://pear.php.net/package/PHP_CodeSniffer/';
|
||||
}
|
||||
|
||||
public function getInfoDescription() {
|
||||
return pht(
|
||||
'PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and '.
|
||||
'detects violations of a defined set of coding standards.');
|
||||
}
|
||||
|
||||
public function getLinterName() {
|
||||
return 'PHPCS';
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Enforces basic spelling. Spelling inside code is actually pretty hard to
|
||||
* get right without false positives. I take a conservative approach and
|
||||
* Enforces basic spelling. Spelling inside code is actually pretty hard to
|
||||
* get right without false positives. I take a conservative approach and
|
||||
* just use a blacklisted set of words that are commonly spelled
|
||||
* incorrectly.
|
||||
*
|
||||
* @group linter
|
||||
*/
|
||||
final class ArcanistSpellingLinter extends ArcanistLinter {
|
||||
|
||||
|
@ -17,6 +15,14 @@ final class ArcanistSpellingLinter extends ArcanistLinter {
|
|||
private $wholeWordRules;
|
||||
private $severity;
|
||||
|
||||
public function getInfoName() {
|
||||
return pht('Spellchecker');
|
||||
}
|
||||
|
||||
public function getInfoDescription() {
|
||||
return pht('Detects common misspellings of English words.');
|
||||
}
|
||||
|
||||
public function __construct($severity = self::LINT_SPELLING_PICKY) {
|
||||
$this->severity = $severity;
|
||||
$this->wholeWordRules = ArcanistSpellingDefaultData::getFullWordRules();
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
/**
|
||||
* Enforces basic text file rules.
|
||||
*
|
||||
* @group linter
|
||||
*/
|
||||
final class ArcanistTextLinter extends ArcanistLinter {
|
||||
|
||||
|
@ -19,6 +17,16 @@ final class ArcanistTextLinter extends ArcanistLinter {
|
|||
|
||||
private $maxLineLength = 80;
|
||||
|
||||
public function getInfoName() {
|
||||
return pht('Basic Text Linter');
|
||||
}
|
||||
|
||||
public function getInfoDescription() {
|
||||
return pht(
|
||||
'Enforces basic text rules like line length, character encoding, '.
|
||||
'and trailing whitespace.');
|
||||
}
|
||||
|
||||
public function getLinterPriority() {
|
||||
return 0.5;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,15 @@
|
|||
* errors and potential problems in XML files.
|
||||
*/
|
||||
final class ArcanistXMLLinter extends ArcanistLinter {
|
||||
|
||||
public function getInfoName() {
|
||||
return pht('SimpleXML Linter');
|
||||
}
|
||||
|
||||
public function getInfoDescription() {
|
||||
return pht('Uses SimpleXML to detect formatting errors in XML files.');
|
||||
}
|
||||
|
||||
public function getLinterName() {
|
||||
return 'XML';
|
||||
}
|
||||
|
|
|
@ -1741,4 +1741,54 @@ abstract class ArcanistBaseWorkflow extends Phobject {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build a new lint engine for the current working copy.
|
||||
*
|
||||
* Optionally, you can pass an explicit engine class name to build an engine
|
||||
* of a particular class. Normally this is used to implement an `--engine`
|
||||
* flag from the CLI.
|
||||
*
|
||||
* @param string Optional explicit engine class name.
|
||||
* @return ArcanistLintEngine Constructed engine.
|
||||
*/
|
||||
protected function newLintEngine($engine_class = null) {
|
||||
$working_copy = $this->getWorkingCopy();
|
||||
$config = $this->getConfigurationManager();
|
||||
|
||||
if (!$engine_class) {
|
||||
$engine_class = $config->getConfigFromAnySource('lint.engine');
|
||||
}
|
||||
|
||||
if (!$engine_class) {
|
||||
if (Filesystem::pathExists($working_copy->getProjectPath('.arclint'))) {
|
||||
$engine_class = 'ArcanistConfigurationDrivenLintEngine';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$engine_class) {
|
||||
throw new ArcanistNoEngineException(
|
||||
pht(
|
||||
"No lint engine is configured for this project. ".
|
||||
"Create an '.arclint' file, or configure an advanced engine ".
|
||||
"with 'lint.engine' in '.arcconfig'."));
|
||||
}
|
||||
|
||||
$base_class = 'ArcanistLintEngine';
|
||||
if (!class_exists($engine_class) ||
|
||||
!is_subclass_of($engine_class, $base_class)) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'Configured lint engine "%s" is not a subclass of "%s", but must '.
|
||||
'be.',
|
||||
$engine_class,
|
||||
$base_class));
|
||||
}
|
||||
|
||||
$engine = newv($engine_class, array())
|
||||
->setWorkingCopy($working_copy)
|
||||
->setConfigurationManager($config);
|
||||
|
||||
return $engine;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -184,22 +184,7 @@ EOTEXT
|
|||
$working_copy = $this->getWorkingCopy();
|
||||
$configuration_manager = $this->getConfigurationManager();
|
||||
|
||||
$engine = $this->getArgument('engine');
|
||||
if (!$engine) {
|
||||
$engine = $configuration_manager->getConfigFromAnySource('lint.engine');
|
||||
}
|
||||
|
||||
if (!$engine) {
|
||||
if (Filesystem::pathExists($working_copy->getProjectPath('.arclint'))) {
|
||||
$engine = 'ArcanistConfigurationDrivenLintEngine';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$engine) {
|
||||
throw new ArcanistNoEngineException(
|
||||
"No lint engine configured for this project. Edit '.arcconfig' to ".
|
||||
"specify a lint engine, or create an '.arclint' file.");
|
||||
}
|
||||
$engine = $this->newLintEngine($this->getArgument('engine'));
|
||||
|
||||
$rev = $this->getArgument('rev');
|
||||
$paths = $this->getArgument('paths');
|
||||
|
@ -252,17 +237,8 @@ EOTEXT
|
|||
$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->setConfigurationManager($configuration_manager);
|
||||
|
||||
$engine->setMinimumSeverity(
|
||||
$this->getArgument('severity', self::DEFAULT_SEVERITY));
|
||||
|
||||
|
|
176
src/workflow/ArcanistLintersWorkflow.php
Normal file
176
src/workflow/ArcanistLintersWorkflow.php
Normal file
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* List available linters.
|
||||
*/
|
||||
final class ArcanistLintersWorkflow extends ArcanistBaseWorkflow {
|
||||
|
||||
public function getWorkflowName() {
|
||||
return 'linters';
|
||||
}
|
||||
|
||||
public function getCommandSynopses() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
**linters** [__options__]
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
||||
public function getCommandHelp() {
|
||||
return phutil_console_format(pht(<<<EOTEXT
|
||||
Supports: cli
|
||||
List the available and configured linters, with information about
|
||||
what they do and which versions are installed.
|
||||
EOTEXT
|
||||
));
|
||||
}
|
||||
|
||||
public function getArguments() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function run() {
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
$linters = id(new PhutilSymbolLoader())
|
||||
->setAncestorClass('ArcanistLinter')
|
||||
->loadObjects();
|
||||
|
||||
try {
|
||||
$built = $this->newLintEngine()->buildLinters();
|
||||
} catch (ArcanistNoEngineException $ex) {
|
||||
$built = $engine->buildLinters();
|
||||
}
|
||||
|
||||
// Note that an engine can emit multiple linters of the same class to run
|
||||
// different rulesets on different groups of files, so these linters do not
|
||||
// necessarily have unique classes or types.
|
||||
$groups = array();
|
||||
foreach ($built as $linter) {
|
||||
$groups[get_class($linter)][] = $linter;
|
||||
}
|
||||
|
||||
$linter_info = array();
|
||||
foreach ($linters as $key => $linter) {
|
||||
$installed = idx($groups, $key, array());
|
||||
$exception = null;
|
||||
|
||||
if ($installed) {
|
||||
$status = 'configured';
|
||||
try {
|
||||
$version = head($installed)->getVersion();
|
||||
} catch (Exception $ex) {
|
||||
$status = 'error';
|
||||
$exception = $ex;
|
||||
}
|
||||
} else {
|
||||
$status = 'available';
|
||||
$version = null;
|
||||
}
|
||||
|
||||
$linter_info[$key] = array(
|
||||
'short' => $linter->getLinterConfigurationName(),
|
||||
'class' => get_class($linter),
|
||||
'status' => $status,
|
||||
'version' => $version,
|
||||
'name' => $linter->getInfoName(),
|
||||
'uri' => $linter->getInfoURI(),
|
||||
'description' => $linter->getInfoDescription(),
|
||||
'exception' => $exception,
|
||||
);
|
||||
}
|
||||
|
||||
$linter_info = isort($linter_info, 'short');
|
||||
|
||||
$status_map = $this->getStatusMap();
|
||||
$pad = ' ';
|
||||
|
||||
$color_map = array(
|
||||
'configured' => 'green',
|
||||
'available' => 'yellow',
|
||||
'error' => 'red',
|
||||
);
|
||||
|
||||
foreach ($linter_info as $key => $linter) {
|
||||
$status = $linter['status'];
|
||||
$color = $color_map[$status];
|
||||
$text = $status_map[$status];
|
||||
$print_tail = false;
|
||||
|
||||
$console->writeOut(
|
||||
"<bg:".$color.">** %s **</bg> **%s** (%s)\n",
|
||||
$text,
|
||||
nonempty($linter['short'], '-'),
|
||||
$linter['name']);
|
||||
|
||||
if ($linter['exception']) {
|
||||
$console->writeOut(
|
||||
"\n%s**%s**\n%s\n",
|
||||
$pad,
|
||||
get_class($linter['exception']),
|
||||
phutil_console_wrap(
|
||||
$linter['exception']->getMessage(),
|
||||
strlen($pad)));
|
||||
$print_tail = true;
|
||||
}
|
||||
|
||||
$version = $linter['version'];
|
||||
$uri = $linter['uri'];
|
||||
if ($version || $uri) {
|
||||
$console->writeOut("\n");
|
||||
$print_tail = true;
|
||||
}
|
||||
|
||||
if ($version) {
|
||||
$console->writeOut("%s%s **%s**\n", $pad, pht('Version'), $version);
|
||||
}
|
||||
|
||||
if ($uri) {
|
||||
$console->writeOut("%s__%s__\n", $pad, $linter['uri']);
|
||||
}
|
||||
|
||||
$description = $linter['description'];
|
||||
if ($description) {
|
||||
$console->writeOut(
|
||||
"\n%s\n",
|
||||
phutil_console_wrap($linter['description'], strlen($pad)));
|
||||
$print_tail = true;
|
||||
}
|
||||
|
||||
if ($print_tail) {
|
||||
$console->writeOut("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get human-readable linter statuses, padded to fixed width.
|
||||
*
|
||||
* @return map<string, string> Human-readable linter status names.
|
||||
*/
|
||||
private function getStatusMap() {
|
||||
$text_map = array(
|
||||
'configured' => pht('CONFIGURED'),
|
||||
'available' => pht('AVAILABLE'),
|
||||
'error' => pht('ERROR'),
|
||||
);
|
||||
|
||||
$sizes = array();
|
||||
foreach ($text_map as $key => $string) {
|
||||
$sizes[$key] = phutil_utf8_console_strlen($string);
|
||||
}
|
||||
|
||||
$longest = max($sizes);
|
||||
foreach ($text_map as $key => $string) {
|
||||
if ($sizes[$key] < $longest) {
|
||||
$text_map[$key] .= str_repeat(' ', $longest - $sizes[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$text_map['padding'] = str_repeat(' ', $longest);
|
||||
|
||||
return $text_map;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue