1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-22 06:42: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:
epriestley 2014-05-11 13:42:56 -07:00
parent c4985ef415
commit e13f5839d4
12 changed files with 353 additions and 43 deletions

View file

@ -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',

View file

@ -381,10 +381,6 @@ abstract class ArcanistExternalLinter extends ArcanistFutureLinter {
}
}
public function getVersion() {
return null;
}
protected function buildFutures(array $paths) {
$executable = $this->getExecutableCommand();

View file

@ -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.
}

View file

@ -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';
}

View file

@ -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';
}

View file

@ -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';
}

View file

@ -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();

View file

@ -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;
}

View file

@ -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';
}

View file

@ -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;
}
}

View file

@ -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));

View 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;
}
}