2011-01-10 00:22:25 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Copyright 2011 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.
|
|
|
|
*/
|
|
|
|
|
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
|
|
|
|
* 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.
|
2011-02-19 20:36:08 +01:00
|
|
|
*
|
|
|
|
* @group lint
|
|
|
|
*/
|
2011-01-10 00:22:25 +01:00
|
|
|
abstract class ArcanistLintEngine {
|
|
|
|
|
|
|
|
protected $workingCopy;
|
|
|
|
protected $fileData = array();
|
|
|
|
|
|
|
|
protected $charToLine = array();
|
|
|
|
protected $lineToFirstChar = array();
|
|
|
|
private $results = 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;
|
2011-01-10 00:22:25 +01:00
|
|
|
|
|
|
|
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, array $changed) {
|
|
|
|
$this->changedLines[$path] = array_fill_keys($changed, true);
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPathChangedLines($path) {
|
|
|
|
return idx($this->changedLines, $path);
|
|
|
|
}
|
|
|
|
|
2011-02-15 23:57:24 +01:00
|
|
|
public function setFileData($data) {
|
|
|
|
$this->fileData = $data + $this->fileData;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setCommitHookMode($mode) {
|
|
|
|
$this->commitHookMode = $mode;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
protected function loadData($path) {
|
|
|
|
if (!isset($this->fileData[$path])) {
|
|
|
|
$disk_path = $this->getFilePathOnDisk($path);
|
|
|
|
$this->fileData[$path] = Filesystem::readFile($disk_path);
|
|
|
|
}
|
|
|
|
return $this->fileData[$path];
|
|
|
|
}
|
|
|
|
|
2011-02-15 23:57:24 +01:00
|
|
|
public function pathExists($path) {
|
|
|
|
if ($this->getCommitHookMode()) {
|
|
|
|
return (idx($this->fileData, $path) !== null);
|
|
|
|
} else {
|
|
|
|
$disk_path = $this->getFilePathOnDisk($path);
|
|
|
|
return Filesystem::pathExists($disk_path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-01-10 00:22:25 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
public function setMinimumSeverity($severity) {
|
|
|
|
$this->minimumSeverity = $severity;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getCommitHookMode() {
|
2011-02-15 23:57:24 +01:00
|
|
|
return $this->commitHookMode;
|
2011-01-10 00:22:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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.");
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($linters as $linter) {
|
|
|
|
$linter->setEngine($this);
|
|
|
|
$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]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$minimum = $this->minimumSeverity;
|
|
|
|
foreach ($linter->getLintMessages() as $message) {
|
|
|
|
if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// When a user runs "arc diff", we default to raising only warnings on
|
|
|
|
// lines they have changed (errors are still raised anywhere in the
|
|
|
|
// file).
|
2011-01-12 05:21:17 +01:00
|
|
|
$changed = $this->getPathChangedLines($message->getPath());
|
2011-01-10 00:22:25 +01:00
|
|
|
if ($changed !== null && !$message->isError()) {
|
|
|
|
if (empty($changed[$message->getLine()])) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$result = $this->getResultForPath($message->getPath());
|
|
|
|
$result->addMessage($message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->results;
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract protected function buildLinters();
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|