1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-15 03:12:40 +01:00

(stable) Promote 2015 Week 35

This commit is contained in:
epriestley 2015-08-29 05:27:02 -07:00
commit c94e60487a
8 changed files with 318 additions and 201 deletions

View file

@ -130,10 +130,15 @@ final class ArcanistPyLintLinter extends ArcanistExternalLinter {
continue; continue;
} }
// NOTE: PyLint sometimes returns -1 as the character offset for a
// message. If it does, treat it as 0. See T9257.
$char = (int)$matches[1];
$char = max(0, $char);
$message = id(new ArcanistLintMessage()) $message = id(new ArcanistLintMessage())
->setPath($path) ->setPath($path)
->setLine($matches[0]) ->setLine($matches[0])
->setChar($matches[1]) ->setChar($char)
->setCode($matches[2]) ->setCode($matches[2])
->setSeverity($this->getLintMessageSeverity($matches[2])) ->setSeverity($this->getLintMessageSeverity($matches[2]))
->setName(ucwords(str_replace('-', ' ', $matches[3]))) ->setName(ucwords(str_replace('-', ' ', $matches[3])))

View file

@ -0,0 +1,6 @@
"""Docstring"""
"""
Useless string """
~~~~~~~~~~
warning:4:0 See T9257.

View file

@ -1,8 +0,0 @@
<!DOCTYPE doc [
<!ELEMENT doc (#PCDATA)>
<!ATTLIST doc a1 CDATA "v1">
<!ATTLIST doc a1 CDATA "z1">
]>
<doc></doc>
~~~~~~~~~~
warning:4:28

View file

@ -1,3 +0,0 @@
<ROOT attr="XY"/>
~~~~~~~~~~
error:1:15

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<languages>
<lang>
</languages>
~~~~~~~~~~
error:4:7
error:5:1

View file

@ -327,7 +327,7 @@ final class ArcanistPHPCompatibilityXHPASTLinterRule
$ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION'); $ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION');
foreach ($ternaries as $ternary) { foreach ($ternaries as $ternary) {
$yes = $ternary->getChildByIndex(1); $yes = $ternary->getChildByIndex(2);
if ($yes->getTypeName() === 'n_EMPTY') { if ($yes->getTypeName() === 'n_EMPTY') {
$this->raiseLintAtNode( $this->raiseLintAtNode(
$ternary, $ternary,

View file

@ -51,6 +51,11 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
return 'git'; return 'git';
} }
public function getGitVersion() {
list($stdout) = $this->execxLocal('--version');
return rtrim(str_replace('git version ', '', $stdout));
}
public function getMetadataPath() { public function getMetadataPath() {
static $path = null; static $path = null;
if ($path === null) { if ($path === null) {
@ -482,35 +487,61 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
return $stdout; return $stdout;
} }
public function getBranchName() { private function getBranchNameFromRef($ref) {
// TODO: consider: $count = 0;
// $branch = preg_replace('/^refs\/heads\//', '', $ref, 1, $count);
// $ git rev-parse --abbrev-ref `git symbolic-ref HEAD` if ($count !== 1) {
// return null;
// But that may fail if you're not on a branch.
list($stdout) = $this->execxLocal('branch --no-color');
// Assume that any branch beginning with '(' means 'no branch', or whatever
// 'no branch' is in the current locale.
$matches = null;
if (preg_match('/^\* ([^\(].*)$/m', $stdout, $matches)) {
return $matches[1];
} }
return $branch;
}
public function getBranchName() {
list($err, $stdout, $stderr) = $this->execManualLocal(
'symbolic-ref --quiet HEAD');
if ($err === 0) {
// We expect the branch name to come qualified with a refs/heads/ prefix.
// Verify this, and strip it.
$ref = rtrim($stdout);
$branch = $this->getBranchNameFromRef($ref);
if (!$branch) {
throw new Exception(
pht('Failed to parse %s output!', 'git symbolic-ref'));
}
return $branch;
} else if ($err === 1) {
// Exit status 1 with --quiet indicates that HEAD is detached.
return null; return null;
} else {
throw new Exception(
pht('Command %s failed: %s', 'git symbolic-ref', $stderr));
}
} }
public function getRemoteURI() { public function getRemoteURI() {
list($stdout) = $this->execxLocal('remote show -n origin'); // "git ls-remote --get-url" is the appropriate plumbing to get the remote
// URI. "git config remote.origin.url", on the other hand, may not be as
$matches = null; // accurate (for example, it does not take into account possible URL
if (preg_match('/^\s*Fetch URL: (.*)$/m', $stdout, $matches)) { // rewriting rules set by the user through "url.<base>.insteadOf"). However,
return trim($matches[1]); // the --get-url flag requires git 1.7.5.
$version = $this->getGitVersion();
if (version_compare($version, '1.7.5', '>=')) {
list($stdout) = $this->execxLocal('ls-remote --get-url origin');
} else {
list($stdout) = $this->execxLocal('config remote.origin.url');
} }
$uri = rtrim($stdout);
// 'origin' is what ls-remote outputs if no origin remote URI exists
if (!$uri || $uri === 'origin') {
return null; return null;
} }
return $uri;
}
public function getSourceControlPath() { public function getSourceControlPath() {
// TODO: Try to get something useful here. // TODO: Try to get something useful here.
return null; return null;
@ -780,37 +811,42 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
} }
public function getBlame($path) { public function getBlame($path) {
// TODO: 'git blame' supports --porcelain and we should probably use it.
list($stdout) = $this->execxLocal( list($stdout) = $this->execxLocal(
'blame --date=iso -w -M %s -- %s', 'blame --porcelain -w -M %s -- %s',
$this->getBaseCommit(), $this->getBaseCommit(),
$path); $path);
// the --porcelain format prints at least one header line per source line,
// then the source line prefixed by a tab character
$blame_info = preg_split('/^\t.*\n/m', rtrim($stdout));
// commit info is not repeated in these headers, so cache it
$revision_data = array();
$blame = array(); $blame = array();
foreach (explode("\n", trim($stdout)) as $line) { foreach ($blame_info as $line_info) {
if (!strlen($line)) { $revision = substr($line_info, 0, 40);
continue; $data = idx($revision_data, $revision, array());
if (empty($data)) {
$matches = array();
if (!preg_match('/^author (.*)$/m', $line_info, $matches)) {
throw new Exception(
pht(
'Unexpected output from %s: no author for commit %s',
'git blame',
$revision));
}
$data['author'] = $matches[1];
$data['from_first_commit'] = preg_match('/^boundary$/m', $line_info);
$revision_data[$revision] = $data;
} }
// lines predating a git repo's history are blamed to the oldest revision, // Ignore lines predating the git repository (on a boundary commit)
// with the commit hash prepended by a ^. we shouldn't count these lines // rather than blaming them on the oldest diff's unfortunate author
// as blaming to the oldest diff's unfortunate author if (!$data['from_first_commit']) {
if ($line[0] == '^') { $blame[] = array($data['author'], $revision);
continue;
} }
$matches = null;
$ok = preg_match(
'/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/',
$line,
$matches);
if (!$ok) {
throw new Exception(pht("Bad blame? `%s'", $line));
}
$revision = $matches[1];
$author = $matches[2];
$blame[] = array($author, $revision);
} }
return $blame; return $blame;
@ -886,25 +922,22 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
* @return list<dict<string, string>> Dictionary of branch information. * @return list<dict<string, string>> Dictionary of branch information.
*/ */
public function getAllBranches() { public function getAllBranches() {
list($branch_info) = $this->execxLocal( list($ref_list) = $this->execxLocal(
'branch --no-color'); 'for-each-ref --format=%s refs/heads',
$lines = explode("\n", rtrim($branch_info)); '%(refname)');
$refs = explode("\n", rtrim($ref_list));
$current = $this->getBranchName();
$result = array(); $result = array();
foreach ($lines as $line) { foreach ($refs as $ref) {
$branch = $this->getBranchNameFromRef($ref);
if (preg_match('@^[* ]+\(no branch|detached from \w+/\w+\)@', $line)) { if ($branch) {
// This is indicating that the working copy is in a detached state;
// just ignore it.
continue;
}
list($current, $name) = preg_split('/\s+/', $line, 2);
$result[] = array( $result[] = array(
'current' => !empty($current), 'current' => ($branch === $current),
'name' => $name, 'name' => $branch,
); );
} }
}
return $result; return $result;
} }
@ -1134,11 +1167,28 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
$commits[] = $merge_base; $commits[] = $merge_base;
$head_branch_count = null; $head_branch_count = null;
$all_branch_names = ipull($this->getAllBranches(), 'name');
foreach ($commits as $commit) { foreach ($commits as $commit) {
// Ideally, we would use something like "for-each-ref --contains"
// to get a filtered list of branches ready for script consumption.
// Instead, try to get predictable output from "branch --contains".
list($branches) = $this->execxLocal( list($branches) = $this->execxLocal(
'branch --contains %s', '-c column.ui=never -c color.ui=never branch --contains %s',
$commit); $commit);
$branches = array_filter(explode("\n", $branches)); $branches = array_filter(explode("\n", $branches));
// Filter the list, removing the "current" marker (*) and ignoring
// anything other than known branch names (mainly, any possible
// "detached HEAD" or "no branch" line).
foreach ($branches as $key => $branch) {
$branch = trim($branch, ' *');
if (in_array($branch, $all_branch_names)) {
$branches[$key] = $branch;
} else {
unset($branches[$key]);
}
}
if ($head_branch_count === null) { if ($head_branch_count === null) {
// If this is the first commit, it's HEAD. Count how many // If this is the first commit, it's HEAD. Count how many
// branches it is on; we want to include commits on the same // branches it is on; we want to include commits on the same
@ -1147,9 +1197,6 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
// for whatever reason. // for whatever reason.
$head_branch_count = count($branches); $head_branch_count = count($branches);
} else if (count($branches) > $head_branch_count) { } else if (count($branches) > $head_branch_count) {
foreach ($branches as $key => $branch) {
$branches[$key] = trim($branch, ' *');
}
$branches = implode(', ', $branches); $branches = implode(', ', $branches);
$this->setBaseCommitExplanation( $this->setBaseCommitExplanation(
pht( pht(

View file

@ -11,7 +11,7 @@ final class ArcanistLintersWorkflow extends ArcanistWorkflow {
public function getCommandSynopses() { public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT return phutil_console_format(<<<EOTEXT
**linters** [__options__] **linters** [__options__] [__name__]
EOTEXT EOTEXT
); );
} }
@ -21,6 +21,8 @@ EOTEXT
Supports: cli Supports: cli
List the available and configured linters, with information about List the available and configured linters, with information about
what they do and which versions are installed. what they do and which versions are installed.
if __name__ is provided, the linter with that name will be displayed.
EOTEXT EOTEXT
)); ));
} }
@ -30,6 +32,14 @@ EOTEXT
'verbose' => array( 'verbose' => array(
'help' => pht('Show detailed information, including options.'), 'help' => pht('Show detailed information, including options.'),
), ),
'search' => array(
'param' => 'search',
'repeat' => true,
'help' => pht(
'Search for linters. Search is case-insensitive, and is performed'.
'against name and description of each linter.'),
),
'*' => 'exact',
); );
} }
@ -46,6 +56,164 @@ EOTEXT
$built = array(); $built = array();
} }
$linter_info = $this->getLintersInfo($linters, $built);
$status_map = $this->getStatusMap();
$pad = ' ';
$color_map = array(
'configured' => 'green',
'available' => 'yellow',
'error' => 'red',
);
$is_verbose = $this->getArgument('verbose');
$exact = $this->getArgument('exact');
$search_terms = $this->getArgument('search');
if ($exact && $search_terms) {
throw new ArcanistUsageException(
'Specify either search expression or exact name');
}
if ($exact) {
$linter_info = $this->findExactNames($linter_info, $exact);
if (!$linter_info) {
$console->writeOut(
"%s\n",
pht(
'No match found. Try `%s %s` to search for a linter.',
'arc linters --search',
$exact[0]));
return;
}
$is_verbose = true;
}
if ($search_terms) {
$linter_info = $this->filterByNames($linter_info, $search_terms);
}
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;
}
if ($is_verbose) {
$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;
}
$options = $linter['options'];
if ($options) {
$console->writeOut(
"\n%s**%s**\n\n",
$pad,
pht('Configuration Options'));
$last_option = last_key($options);
foreach ($options as $option => $option_spec) {
$console->writeOut(
"%s__%s__ (%s)\n",
$pad,
$option,
$option_spec['type']);
$console->writeOut(
"%s\n",
phutil_console_wrap(
$option_spec['help'],
strlen($pad) + 2));
if ($option != $last_option) {
$console->writeOut("\n");
}
}
$print_tail = true;
}
if ($print_tail) {
$console->writeOut("\n");
}
}
}
if (!$is_verbose) {
$console->writeOut(
"%s\n",
pht('(Run `%s` for more details.)', 'arc linters --verbose'));
}
}
/**
* 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;
}
private function getLintersInfo(array $linters, array $built) {
// Note that an engine can emit multiple linters of the same class to run // 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 // different rulesets on different groups of files, so these linters do not
// necessarily have unique classes or types. // necessarily have unique classes or types.
@ -85,131 +253,40 @@ EOTEXT
); );
} }
$linter_info = isort($linter_info, 'short'); return 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']; private function filterByNames(array $linters, array $search_terms) {
$uri = $linter['uri']; $filtered = array();
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']);
}
foreach ($linters as $key => $linter) {
$name = $linter['name'];
$short = $linter['short'];
$description = $linter['description']; $description = $linter['description'];
if ($description) { foreach ($search_terms as $term) {
$console->writeOut( if (stripos($name, $term) !== false ||
"\n%s\n", stripos($short, $term) !== false ||
phutil_console_wrap($linter['description'], strlen($pad))); stripos($description, $term) !== false) {
$print_tail = true; $filtered[$key] = $linter;
}
$options = $linter['options'];
if ($options && $this->getArgument('verbose')) {
$console->writeOut(
"\n%s**%s**\n\n",
$pad,
pht('Configuration Options'));
$last_option = last_key($options);
foreach ($options as $option => $option_spec) {
$console->writeOut(
"%s__%s__ (%s)\n",
$pad,
$option,
$option_spec['type']);
$console->writeOut(
"%s\n",
phutil_console_wrap(
$option_spec['help'],
strlen($pad) + 2));
if ($option != $last_option) {
$console->writeOut("\n");
} }
} }
$print_tail = true; }
return $filtered;
} }
if ($print_tail) { private function findExactNames(array $linters, array $names) {
$console->writeOut("\n"); $filtered = array();
foreach ($linters as $key => $linter) {
$name = $linter['name'];
foreach ($names as $term) {
if (strcasecmp($name, $term) == 0) {
$filtered[$key] = $linter;
} }
} }
if (!$this->getArgument('verbose')) {
$console->writeOut(
"%s\n",
pht('(Run `%s` for more details.)', 'arc linters --verbose'));
} }
} return $filtered;
/**
* 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;
} }
} }