Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F10360699
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
68 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/lint/engine/base/ArcanistLintEngine.php b/src/lint/engine/base/ArcanistLintEngine.php
index d2dbd56e..5a199d4b 100644
--- a/src/lint/engine/base/ArcanistLintEngine.php
+++ b/src/lint/engine/base/ArcanistLintEngine.php
@@ -1,199 +1,201 @@
<?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.
*/
abstract class ArcanistLintEngine {
protected $workingCopy;
protected $fileData = array();
protected $charToLine = array();
protected $lineToFirstChar = array();
private $results = array();
private $minimumSeverity = null;
private $changedLines = array();
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);
}
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];
}
public function getFilePathOnDisk($path) {
- return $path;
+ return Filesystem::resolvePath(
+ $path,
+ $this->getWorkingCopy()->getProjectRoot());
}
public function setMinimumSeverity($severity) {
$this->minimumSeverity = $severity;
return $this;
}
public function getCommitHookMode() {
return false;
}
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).
$changed = $this->getPathChangedLines($path);
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) {
+ $result->setFilePathOnDisk($this->getFilePathOnDisk($path));
if (isset($this->fileData[$path])) {
// Only set the data if any linter loaded it. The goal here is to
// avoid binaries when we don't actually care about their contents,
// for performance.
$result->setData($this->fileData[$path]);
- $result->setFilePathOnDisk($this->getFilePathOnDisk($path));
}
}
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);
}
}
diff --git a/src/lint/linter/base/ArcanistLinter.php b/src/lint/linter/base/ArcanistLinter.php
index 5f841247..55c7c669 100644
--- a/src/lint/linter/base/ArcanistLinter.php
+++ b/src/lint/linter/base/ArcanistLinter.php
@@ -1,181 +1,186 @@
<?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.
*/
abstract class ArcanistLinter {
protected $paths = array();
protected $data = array();
protected $engine;
protected $activePath;
protected $messages = array();
protected $stopAllLinters = false;
private $customSeverityMap = array();
public function setCustomSeverityMap(array $map) {
$this->customSeverityMap = $map;
return $this;
}
public function getActivePath() {
return $this->activePath;
}
public function stopAllLinters() {
$this->stopAllLinters = true;
return $this;
}
public function didStopAllLinters() {
return $this->stopAllLinters;
}
public function addPath($path) {
$this->paths[$path] = $path;
return $this;
}
public function getPaths() {
return array_values($this->paths);
}
public function addData($path, $data) {
$this->data[$path] = $data;
return $this;
}
protected function getData($path) {
if (!array_key_exists($path, $this->data)) {
throw new Exception("Data is not provided for path '{$path}'!");
}
return $this->data[$path];
}
public function setEngine($engine) {
$this->engine = $engine;
return $this;
}
protected function getEngine() {
return $this->engine;
}
public function getLintMessageFullCode($short_code) {
return $this->getLinterName().$short_code;
}
public function getLintMessageSeverity($code) {
$map = $this->customSeverityMap;
if (isset($map[$code])) {
return $map[$code];
}
$map = $this->getLintSeverityMap();
if (isset($map[$code])) {
return $map[$code];
}
return ArcanistLintSeverity::SEVERITY_ERROR;
}
public function getLintMessageName($code) {
$map = $this->getLintNameMap();
if (isset($map[$code])) {
return $map[$code];
}
return "Unknown lint message!";
}
protected function addLintMessage(ArcanistLintMessage $message) {
$this->messages[] = $message;
return $message;
}
public function getLintMessages() {
return $this->messages;
}
protected function raiseLintAtLine(
$line,
$char,
$code,
$desc,
$original = null,
$replacement = null) {
$dict = array(
'path' => $this->getActivePath(),
'line' => $line,
'char' => $char,
'code' => $this->getLintMessageFullCode($code),
'severity' => $this->getLintMessageSeverity($code),
'name' => $this->getLintMessageName($code),
'description' => $desc,
);
if ($original !== null) {
$dict['original'] = $original;
}
if ($replacement !== null) {
$dict['replacement'] = $replacement;
}
return $this->addLintMessage(ArcanistLintMessage::newFromDictionary($dict));
}
protected function raiseLintAtPath(
$code,
$desc) {
$path = $this->getActivePath();
return $this->raiseLintAtLine(null, null, $code, $desc, null, null);
}
protected function raiseLintAtOffset(
$offset,
$code,
$desc,
$original = null,
$replacement = null) {
$path = $this->getActivePath();
$engine = $this->getEngine();
- list($line, $char) = $engine->getLineAndCharFromOffset($path, $offset);
+ if ($offset === null) {
+ $line = null;
+ $char = null;
+ } else {
+ list($line, $char) = $engine->getLineAndCharFromOffset($path, $offset);
+ }
return $this->raiseLintAtLine(
$line + 1,
$char + 1,
$code,
$desc,
$original,
$replacement);
}
public function willLintPath($path) {
$this->stopAllLinters = false;
$this->activePath = $path;
}
abstract public function willLintPaths(array $paths);
abstract public function lintPath($path);
abstract public function getLinterName();
abstract public function getLintSeverityMap();
abstract public function getLintNameMap();
}
diff --git a/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php b/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
index bde388bb..709b0fb3 100644
--- a/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
+++ b/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
@@ -1,478 +1,493 @@
<?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.
*/
class ArcanistPhutilModuleLinter extends ArcanistLinter {
const LINT_UNDECLARED_CLASS = 1;
const LINT_UNDECLARED_FUNCTION = 2;
const LINT_UNDECLARED_INTERFACE = 3;
const LINT_UNDECLARED_SOURCE = 4;
const LINT_UNUSED_MODULE = 5;
const LINT_UNUSED_SOURCE = 6;
const LINT_INIT_REBUILD = 7;
const LINT_UNKNOWN_CLASS = 8;
const LINT_UNKNOWN_FUNCTION = 9;
const LINT_ANALYZER_SIGNATURE = 100;
const LINT_ANALYZER_DYNAMIC = 101;
const LINT_ANALYZER_NO_INIT = 102;
const LINT_ANALYZER_MULTIPLE_CLASSES = 103;
const LINT_ANALYZER_CLASS_FILENAME = 104;
public function getLintNameMap() {
return array(
self::LINT_UNDECLARED_CLASS => 'Use of Undeclared Class',
self::LINT_UNDECLARED_FUNCTION => 'Use of Undeclared Function',
self::LINT_UNDECLARED_INTERFACE => 'Use of Undeclared Interface',
self::LINT_UNDECLARED_SOURCE => 'Use of Nonexistent File',
self::LINT_UNUSED_SOURCE => 'Unused Source',
self::LINT_UNUSED_MODULE => 'Unused Module',
self::LINT_INIT_REBUILD => 'Rebuilt __init__.php File',
self::LINT_UNKNOWN_CLASS => 'Unknown Class',
self::LINT_UNKNOWN_FUNCTION => 'Unknown Function',
self::LINT_ANALYZER_SIGNATURE => 'Analyzer: Bad Call Signature',
self::LINT_ANALYZER_DYNAMIC => 'Analyzer: Dynamic Dependency',
self::LINT_ANALYZER_NO_INIT => 'Analyzer: No __init__.php File',
self::LINT_ANALYZER_MULTIPLE_CLASSES
=> 'Analyzer: File Declares Multiple Classes',
self::LINT_ANALYZER_CLASS_FILENAME
=> 'Analyzer: Filename Does Not Match Class Declaration',
);
}
public function getLinterName() {
return 'PHU';
}
public function getLintSeverityMap() {
return array(
self::LINT_ANALYZER_DYNAMIC => ArcanistLintSeverity::SEVERITY_WARNING,
);
}
private $moduleInfo = array();
private $unknownClasses = array();
private $unknownFunctions = array();
private function setModuleInfo($key, array $info) {
$this->moduleInfo[$key] = $info;
}
private function getModulePathOnDisk($key) {
$info = $this->moduleInfo[$key];
return $info['root'].'/'.$info['module'];
}
private function getModuleDisplayName($key) {
$info = $this->moduleInfo[$key];
return $info['module'];
}
private function isPhutilLibraryMetadata($path) {
$file = basename($path);
return !strncmp('__phutil_library_', $file, strlen('__phutil_library_'));
}
public function willLintPaths(array $paths) {
$modules = array();
$moduleinfo = array();
$project_root = $this->getEngine()->getWorkingCopy()->getProjectRoot();
foreach ($paths as $path) {
$absolute_path = $project_root.'/'.$path;
$library_root = phutil_get_library_root_for_path($absolute_path);
if (!$library_root) {
continue;
}
if ($this->isPhutilLibraryMetadata($path)) {
continue;
}
$library_name = phutil_get_library_name_for_root($library_root);
if (!is_dir($path)) {
$path = dirname($path);
}
$path = Filesystem::resolvePath(
$path,
$project_root);
if ($path == $library_root) {
continue;
}
$module_name = Filesystem::readablePath($path, $library_root);
$module_key = $library_name.':'.$module_name;
if (empty($modules[$module_key])) {
$modules[$module_key] = $module_key;
$this->setModuleInfo($module_key, array(
'library' => $library_name,
'root' => $library_root,
'module' => $module_name,
));
}
}
if (!$modules) {
return;
}
$modules = array_keys($modules);
$arc_root = phutil_get_library_root('arcanist');
$bin = dirname($arc_root).'/scripts/phutil_analyzer.php';
$futures = array();
- foreach ($modules as $key) {
+ foreach ($modules as $mkey => $key) {
$disk_path = $this->getModulePathOnDisk($key);
- $futures[$key] = new ExecFuture(
- '%s %s',
- $bin,
- $disk_path);
+ if (Filesystem::pathExists($disk_path)) {
+ $futures[$key] = new ExecFuture(
+ '%s %s',
+ $bin,
+ $disk_path);
+ } else {
+ // This can occur in git when you add a module in HEAD and then remove
+ // it in unstaged changes in the working copy. Just ignore it.
+ unset($modules[$mkey]);
+ }
}
$requirements = array();
foreach (Futures($futures) as $key => $future) {
$requirements[$key] = $future->resolveJSON();
}
$dependencies = array();
$futures = array();
foreach ($requirements as $key => $requirement) {
foreach ($requirement['messages'] as $message) {
list($where, $text, $code, $description) = $message;
if ($where) {
$where = array($where);
}
$this->raiseLintInModule(
$key,
$code,
$description,
$where,
$text);
}
foreach ($requirement['requires']['module'] as $req_module => $where) {
if (isset($requirements[$req_module])) {
$dependencies[$req_module] = $requirements[$req_module];
} else {
list($library_name, $module_name) = explode(':', $req_module);
$library_root = phutil_get_library_root($library_name);
$this->setModuleInfo($req_module, array(
'library' => $library_name,
'root' => $library_root,
'module' => $module_name,
));
$disk_path = $this->getModulePathOnDisk($req_module);
$futures[$req_module] = new ExecFuture(
'%s %s',
$bin,
$disk_path);
}
}
}
foreach (Futures($futures) as $key => $future) {
$dependencies[$key] = $future->resolveJSON();
}
foreach ($requirements as $key => $spec) {
$deps = array_intersect_key(
$dependencies,
$spec['requires']['module']);
$this->lintModule($key, $spec, $deps);
}
}
private function lintModule($key, $spec, $deps) {
$resolvable = array();
$need_classes = array();
$need_functions = array();
$drop_modules = array();
$used = array();
static $types = array(
'class' => self::LINT_UNDECLARED_CLASS,
'interface' => self::LINT_UNDECLARED_INTERFACE,
'function' => self::LINT_UNDECLARED_FUNCTION,
);
foreach ($types as $type => $lint_code) {
foreach ($spec['requires'][$type] as $name => $places) {
$declared = $this->checkDependency(
$type,
$name,
$deps);
if (!$declared) {
$module = $this->getModuleDisplayName($key);
$message = $this->raiseLintInModule(
$key,
$lint_code,
"Module '{$module}' uses {$type} '{$name}' but does not include ".
"any module which declares it.",
$places);
if ($type == 'class' || $type == 'interface') {
$class_spec = PhutilLibraryMapRegistry::findClass(
$library = null,
$name);
if ($class_spec) {
if (phutil_autoload_class($name)) {
$resolvable[] = $message;
$need_classes[$name] = $class_spec;
} else {
if (empty($this->unknownClasses[$name])) {
$this->unknownClasses[$name] = true;
$library = $class_spec['library'];
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_CLASS,
"Class '{$name}' exists in the library map for library ".
"'{$library}', but could not be loaded. You may need to ".
"rebuild the library map.",
$places);
}
}
} else {
if (empty($this->unknownClasses[$name])) {
$this->unknownClasses[$name] = true;
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_CLASS,
"Class '{$name}' could not be found in any known library. ".
"You may need to rebuild the map for the library which ".
"contains it.",
$places);
}
}
} else {
$func_spec = PhutilLibraryMapRegistry::findFunction(
$library = null,
$name);
if ($func_spec) {
if (phutil_autoload_function($name)) {
$resolvable[] = $message;
$need_functions[$name] = $func_spec;
} else {
if (empty($this->unknownFunctions[$name])) {
$this->unknownFunctions[$name] = true;
$library = $func_spec['library'];
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_FUNCTION,
"Function '{$name}' exists in the library map for library ".
"'{$library}', but could not be loaded. You may need to ".
"rebuild the library map.",
$places);
}
}
} else {
if (empty($this->unknownFunctions[$name])) {
$this->unknownFunctions[$name] = true;
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_FUNCTION,
"Function '{$name}' could not be found in any known ".
"library. You may need to rebuild the map for the library ".
"which contains it.",
$places);
}
}
}
}
$used[$declared] = true;
}
}
$unused = array_diff_key($deps, $used);
foreach ($unused as $unused_module_key => $ignored) {
$module = $this->getModuleDisplayName($key);
$unused_module = $this->getModuleDisplayName($unused_module_key);
$resolvable[] = $this->raiseLintInModule(
$key,
self::LINT_UNUSED_MODULE,
"Module '{$module}' requires module '{$unused_module}' but does not ".
"use anything it declares.",
$spec['requires']['module'][$unused_module_key]);
$drop_modules[] = $unused_module_key;
}
foreach ($spec['requires']['source'] as $file => $where) {
if (empty($spec['declares']['source'][$file])) {
$module = $this->getModuleDisplayName($key);
$resolvable[] = $this->raiseLintInModule(
$key,
self::LINT_UNDECLARED_SOURCE,
"Module '{$module}' requires source '{$file}', but it does not ".
"exist.",
$where);
}
}
foreach ($spec['declares']['source'] as $file => $ignored) {
if (empty($spec['requires']['source'][$file])) {
$module = $this->getModuleDisplayName($key);
$resolvable[] = $this->raiseLintInModule(
$key,
self::LINT_UNUSED_SOURCE,
"Module '{$module}' does not include source file '{$file}'.",
null);
}
}
if ($resolvable) {
$new_file = $this->buildNewModuleInit(
$key,
$spec,
$need_classes,
$need_functions,
$drop_modules);
$init_path = $this->getModulePathOnDisk($key).'/__init__.php';
- $init_path = Filesystem::readablePath($init_path);
- if (file_exists($init_path)) {
+ $try_path = Filesystem::readablePath($init_path);
+ if (Filesystem::pathExists($try_path)) {
+ $init_path = $try_path;
$old_file = Filesystem::readFile($init_path);
- $this->willLintPath($init_path);
- $message = $this->raiseLintAtOffset(
- 0,
- self::LINT_INIT_REBUILD,
- "This regenerated phutil '__init__.php' file is suggested to ".
- "address lint problems with static dependencies in the module.",
- $old_file,
- $new_file);
- $message->setDependentMessages($resolvable);
+ } else {
+ $old_file = '';
+ }
+ $this->willLintPath($init_path);
+ $message = $this->raiseLintAtOffset(
+ null,
+ self::LINT_INIT_REBUILD,
+ "This generated phutil '__init__.php' file is suggested to address ".
+ "lint problems with static dependencies in the module.",
+ $old_file,
+ $new_file);
+ $message->setDependentMessages($resolvable);
+ foreach ($resolvable as $message) {
+ $message->setObsolete(true);
}
+ $message->setGenerateFile(true);
}
}
private function buildNewModuleInit(
$key,
$spec,
$need_classes,
$need_functions,
$drop_modules) {
$init = array();
$init[] = '<?php';
$at = '@';
$init[] = <<<EOHEADER
/**
* This file is automatically generated. Lint this module to rebuild it.
* {$at}generated
*/
EOHEADER;
$init[] = null;
$modules = $spec['requires']['module'];
foreach ($drop_modules as $drop) {
unset($modules[$drop]);
}
foreach ($need_classes as $need => $class_spec) {
$modules[$class_spec['library'].':'.$class_spec['module']] = true;
}
foreach ($need_functions as $need => $func_spec) {
$modules[$func_spec['library'].':'.$func_spec['module']] = true;
}
ksort($modules);
$last = null;
foreach ($modules as $module_key => $ignored) {
if (is_array($ignored)) {
$in_init = false;
$in_file = false;
foreach ($ignored as $where) {
list($file, $line) = explode(':', $where);
if ($file == '__init__.php') {
$in_init = true;
} else {
$in_file = true;
}
}
if ($in_file && !$in_init) {
// If this is a runtime include, don't try to put it in the
// __init__ file.
continue;
}
}
list($library, $module_name) = explode(':', $module_key);
if ($last != $library) {
$last = $library;
if ($last != null) {
$init[] = null;
}
}
$library = "'".addcslashes($library, "'\\")."'";
$module_name = "'".addcslashes($module_name, "'\\")."'";
$init[] = "phutil_require_module({$library}, {$module_name});";
}
$init[] = null;
$init[] = null;
$files = array_keys($spec['declares']['source']);
sort($files);
foreach ($files as $file) {
$file = "'".addcslashes($file, "'\\")."'";
$init[] = "phutil_require_source({$file});";
}
$init[] = null;
return implode("\n", $init);
}
private function checkDependency($type, $name, $deps) {
foreach ($deps as $key => $dep) {
if (isset($dep['declares'][$type][$name])) {
return $key;
}
}
return false;
}
public function raiseLintInModule($key, $code, $desc, $places, $text = null) {
if ($places) {
foreach ($places as $place) {
list($file, $offset) = explode(':', $place);
$this->willLintPath(
- Filesystem::readablePath($this->getModulePathOnDisk($key).'/'.$file));
+ Filesystem::readablePath(
+ $this->getModulePathOnDisk($key).'/'.$file,
+ $this->getEngine()->getWorkingCopy()->getProjectRoot()));
return $this->raiseLintAtOffset(
$offset,
$code,
$desc,
$text);
}
} else {
$this->willLintPath($this->getModuleDisplayName($key));
return $this->raiseLintAtPath(
$code,
$desc);
}
}
public function lintPath($path) {
return;
}
}
diff --git a/src/lint/message/ArcanistLintMessage.php b/src/lint/message/ArcanistLintMessage.php
index fd938786..6a8e1495 100644
--- a/src/lint/message/ArcanistLintMessage.php
+++ b/src/lint/message/ArcanistLintMessage.php
@@ -1,169 +1,189 @@
<?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.
*/
class ArcanistLintMessage {
protected $path;
protected $line;
protected $char;
protected $code;
protected $severity;
protected $name;
protected $description;
protected $originalText;
protected $replacementText;
protected $appliedToDisk;
+ protected $generateFile;
protected $dependentMessages = array();
+ protected $obsolete;
public static function newFromDictionary(array $dict) {
$message = new ArcanistLintMessage();
$message->setPath($dict['path']);
$message->setLine($dict['line']);
$message->setChar($dict['char']);
$message->setCode($dict['code']);
$message->setSeverity($dict['severity']);
$message->setName($dict['name']);
$message->setDescription($dict['description']);
if (isset($dict['original'])) {
$message->setOriginalText($dict['original']);
}
if (isset($dict['replacement'])) {
$message->setReplacementText($dict['replacement']);
}
return $message;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function setLine($line) {
$this->line = $line;
return $this;
}
public function getLine() {
return $this->line;
}
public function setChar($char) {
$this->char = $char;
return $this;
}
public function getChar() {
return $this->char;
}
public function setCode($code) {
$this->code = $code;
return $this;
}
public function getCode() {
return $this->code;
}
public function setSeverity($severity) {
$this->severity = $severity;
return $this;
}
public function getSeverity() {
return $this->severity;
}
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setOriginalText($original) {
$this->originalText = $original;
return $this;
}
public function getOriginalText() {
return $this->originalText;
}
public function setReplacementText($replacement) {
$this->replacementText = $replacement;
return $this;
}
public function getReplacementText() {
return $this->replacementText;
}
public function isError() {
return $this->getSeverity() == ArcanistLintSeverity::SEVERITY_ERROR;
}
public function isWarning() {
return $this->getSeverity() == ArcanistLintSeverity::SEVERITY_WARNING;
}
public function hasFileContext() {
return ($this->getLine() !== null);
}
+ public function setGenerateFile($generate_file) {
+ $this->generateFile = $generate_file;
+ return $this;
+ }
+
+ public function getGenerateFile() {
+ return $this->generateFile;
+ }
+
+ public function setObsolete($obsolete) {
+ $this->obsolete = $obsolete;
+ return $this;
+ }
+
+ public function getObsolete() {
+ return $this->obsolete;
+ }
+
public function isPatchable() {
return ($this->getReplacementText() !== null);
}
public function didApplyPatch() {
if ($this->appliedToDisk) {
return;
}
$this->appliedToDisk = true;
foreach ($this->dependentMessages as $message) {
$message->didApplyPatch();
}
return $this;
}
public function isPatchApplied() {
return $this->appliedToDisk;
}
public function setDependentMessages(array $messages) {
$this->dependentMessages = $messages;
return $this;
}
}
diff --git a/src/lint/patcher/ArcanistLintPatcher.php b/src/lint/patcher/ArcanistLintPatcher.php
index a2ab8825..e8289845 100644
--- a/src/lint/patcher/ArcanistLintPatcher.php
+++ b/src/lint/patcher/ArcanistLintPatcher.php
@@ -1,148 +1,151 @@
<?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.
*/
final class ArcanistLintPatcher {
private $dirtyUntil = 0;
private $characterDelta = 0;
private $modifiedData = null;
private $lineOffsets = null;
private $lintResult = null;
private $applyMessages = array();
public static function newFromArcanistLintResult(ArcanistLintResult $result) {
$obj = new ArcanistLintPatcher();
$obj->lintResult = $result;
return $obj;
}
public function getUnmodifiedFileContent() {
return $this->lintResult->getData();
}
public function getModifiedFileContent() {
if ($this->modifiedData === null) {
$this->buildModifiedFile();
}
return $this->modifiedData;
}
public function writePatchToDisk() {
$path = $this->lintResult->getFilePathOnDisk();
$data = $this->getModifiedFileContent();
$ii = null;
do {
$lint = $path.'.linted'.($ii++);
} while (file_exists($lint));
// Copy existing file to preserve permissions. 'chmod --reference' is not
// supported under OSX.
- execx('cp -p %s %s', $path, $lint);
+ if (Filesystem::pathExists($path)) {
+ // This path may not exist if we're generating a new file.
+ execx('cp -p %s %s', $path, $lint);
+ }
Filesystem::writeFile($lint, $data);
list($err) = exec_manual("mv -f %s %s", $lint, $path);
if ($err) {
throw new Exception(
"Unable to overwrite path `{$path}', patched version was left ".
"at `{$lint}'.");
}
foreach ($this->applyMessages as $message) {
$message->didApplyPatch();
}
}
private function __construct() {
}
private function buildModifiedFile() {
$data = $this->getUnmodifiedFileContent();
foreach ($this->lintResult->getMessages() as $lint) {
if (!$lint->isPatchable()) {
continue;
}
$orig_offset = $this->getCharacterOffset($lint->getLine() - 1);
$orig_offset += $lint->getChar() - 1;
$dirty = $this->getDirtyCharacterOffset();
if ($dirty > $orig_offset) {
continue;
}
// Adjust the character offset by the delta *after* checking for
// dirtiness. The dirty character cursor is a cursor on the original file,
// and should be compared with the patch position in the original file.
$working_offset = $orig_offset + $this->getCharacterDelta();
$old_str = $lint->getOriginalText();
$old_len = strlen($old_str);
$new_str = $lint->getReplacementText();
$new_len = strlen($new_str);
$data = substr_replace($data, $new_str, $working_offset, $old_len);
$this->changeCharacterDelta($new_len - $old_len);
$this->setDirtyCharacterOffset($orig_offset + $old_len);
$this->applyMessages[] = $lint;
}
$this->modifiedData = $data;
}
private function getCharacterOffset($line_num) {
if ($this->lineOffsets === null) {
$lines = explode("\n", $this->getUnmodifiedFileContent());
$this->lineOffsets = array(0);
$last = 0;
foreach ($lines as $line) {
$this->lineOffsets[] = $last + strlen($line) + 1;
$last += strlen($line) + 1;
}
}
if ($line_num >= count($this->lineOffsets)) {
throw new Exception("Data has fewer than `{$line}' lines.");
}
return idx($this->lineOffsets, $line_num);
}
private function setDirtyCharacterOffset($offset) {
$this->dirtyUntil = $offset;
return $this;
}
private function getDirtyCharacterOffset() {
return $this->dirtyUntil;
}
private function changeCharacterDelta($change) {
$this->characterDelta += $change;
return $this;
}
private function getCharacterDelta() {
return $this->characterDelta;
}
}
diff --git a/src/lint/renderer/ArcanistLintRenderer.php b/src/lint/renderer/ArcanistLintRenderer.php
index 8f74f5d2..12510377 100644
--- a/src/lint/renderer/ArcanistLintRenderer.php
+++ b/src/lint/renderer/ArcanistLintRenderer.php
@@ -1,189 +1,190 @@
<?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.
*/
class ArcanistLintRenderer {
private $summaryMode;
public function setSummaryMode($mode) {
$this->summaryMode = $mode;
}
public function renderLintResult(ArcanistLintResult $result) {
if ($this->summaryMode) {
return $this->renderResultSummary($result);
} else {
return $this->renderResultFull($result);
}
}
protected function renderResultFull(ArcanistLintResult $result) {
$messages = $result->getMessages();
$path = $result->getPath();
$lines = explode("\n", $result->getData());
$text = array();
$text[] = phutil_console_format('**>>>** Lint for __%s__:', $path);
$text[] = null;
foreach ($messages as $message) {
if ($message->isError()) {
$color = 'red';
} else {
$color = 'yellow';
}
$severity = ArcanistLintSeverity::getStringForSeverity(
$message->getSeverity());
$code = $message->getCode();
$name = $message->getName();
$description = phutil_console_wrap($message->getDescription(), 4);
$text[] = phutil_console_format(
" **<bg:{$color}> %s </bg>** (%s) __%s__\n".
" %s\n",
$severity,
$code,
$name,
$description);
if ($message->hasFileContext()) {
$text[] = $this->renderContext($message, $lines);
}
}
$text[] = null;
$text[] = null;
return implode("\n", $text);
}
protected function renderResultSummary(ArcanistLintResult $result) {
$messages = $result->getMessages();
$path = $result->getPath();
$text = array();
$text[] = $path.":";
foreach ($messages as $message) {
$name = $message->getName();
$severity = ArcanistLintSeverity::getStringForSeverity(
$message->getSeverity());
$line = $message->getLine();
$text[] = " {$severity} on line {$line}: {$name}";
}
$text[] = null;
return implode("\n", $text);
}
protected function renderContext(
ArcanistLintMessage $message,
array $line_data) {
$lines_of_context = 3;
$out = array();
$line_num = min($message->getLine(), count($line_data));
$line_num = max(1, $line_num);
// Print out preceding context before the impacted region.
$cursor = max(1, $line_num - $lines_of_context);
for (; $cursor < $line_num; $cursor++) {
$out[] = $this->renderLine($cursor, $line_data[$cursor - 1]);
}
// Print out the impacted region itself.
$diff = $message->isPatchable() ? '-' : null;
$text = $message->getOriginalText();
$text_lines = explode("\n", $text);
$text_length = count($text_lines);
for (; $cursor < $line_num + $text_length; $cursor++) {
$chevron = ($cursor == $line_num);
- $data = $line_data[$cursor - 1];
+ // We may not have any data if, e.g., the old file does not exist.
+ $data = idx($line_data, $cursor - 1, null);
// Highlight the problem substring.
$text_line = $text_lines[$cursor - $line_num];
if (strlen($text_line)) {
$data = substr_replace(
$data,
phutil_console_format('##%s##', $text_line),
($cursor == $line_num)
? $message->getChar() - 1
: 0,
strlen($text_line));
}
$out[] = $this->renderLine($cursor, $data, $chevron, $diff);
}
if ($message->isPatchable()) {
$patch = $message->getReplacementText();
$patch_lines = explode("\n", $patch);
$offset = 0;
foreach ($patch_lines as $patch_line) {
if (isset($line_data[$line_num - 1 + $offset])) {
$base = $line_data[$line_num - 1 + $offset];
} else {
$base = '';
}
if ($offset == 0) {
$start = $message->getChar() - 1;
} else {
$start = 0;
}
if (isset($text_lines[$offset])) {
$len = strlen($text_lines[$offset]);
} else {
$len = 0;
}
$patched = substr_replace(
$base,
phutil_console_format('##%s##', $patch_line),
$start,
$len);
$out[] = $this->renderLine(null, $patched, false, '+');
$offset++;
}
}
$lines_count = count($line_data);
$end = min($lines_count, $cursor + $lines_of_context);
for (; $cursor < $end; $cursor++) {
$out[] = $this->renderLine($cursor, $line_data[$cursor - 1]);
}
$out[] = null;
return implode("\n", $out);
}
protected function renderLine($line, $data, $chevron = false, $diff = null) {
$chevron = $chevron ? '>>>' : '';
return sprintf(
" %3s %1s %6s %s",
$chevron,
$diff,
$line,
$data);
}
}
diff --git a/src/lint/renderer/__init__.php b/src/lint/renderer/__init__.php
index c89f46b8..e8872d7c 100644
--- a/src/lint/renderer/__init__.php
+++ b/src/lint/renderer/__init__.php
@@ -1,14 +1,15 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'lint/severity');
phutil_require_module('phutil', 'console');
+phutil_require_module('phutil', 'utils');
phutil_require_source('ArcanistLintRenderer.php');
diff --git a/src/lint/result/ArcanistLintResult.php b/src/lint/result/ArcanistLintResult.php
index 5eb25ead..d33dee42 100644
--- a/src/lint/result/ArcanistLintResult.php
+++ b/src/lint/result/ArcanistLintResult.php
@@ -1,90 +1,105 @@
<?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.
*/
final class ArcanistLintResult {
protected $path;
protected $data;
protected $filePathOnDisk;
protected $messages = array();
+ protected $effectiveMessages = array();
private $needsSort;
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function addMessage(ArcanistLintMessage $message) {
$this->messages[] = $message;
$this->needsSort = true;
return $this;
}
public function getMessages() {
if ($this->needsSort) {
- $this->sortMessages();
+ $this->sortAndFilterMessages();
}
- return $this->messages;
+ return $this->effectiveMessages;
}
public function setData($data) {
$this->data = $data;
return $this;
}
public function getData() {
return $this->data;
}
public function setFilePathOnDisk($file_path_on_disk) {
$this->filePathOnDisk = $file_path_on_disk;
return $this;
}
public function getFilePathOnDisk() {
return $this->filePathOnDisk;
}
public function isPatchable() {
foreach ($this->messages as $message) {
if ($message->isPatchable()) {
return true;
}
}
return false;
}
- private function sortMessages() {
+ private function sortAndFilterMessages() {
$messages = $this->messages;
+
+ foreach ($messages as $key => $message) {
+ if ($message->getObsolete()) {
+ unset($messages[$key]);
+ continue;
+ }
+ if ($message->getGenerateFile()) {
+ $messages = array(
+ $key => $message,
+ );
+ break;
+ }
+ }
+
$map = array();
foreach ($messages as $key => $message) {
$map[$key] = ($message->getLine() * (2 << 12)) + $message->getChar();
}
asort($map);
$messages = array_select_keys($messages, array_keys($map));
- $this->messages = $messages;
+
+ $this->effectiveMessages = $messages;
$this->needsSort = false;
- return $this;
}
}
diff --git a/src/workflow/base/ArcanistBaseWorkflow.php b/src/workflow/base/ArcanistBaseWorkflow.php
index daf6c647..54bd74f7 100644
--- a/src/workflow/base/ArcanistBaseWorkflow.php
+++ b/src/workflow/base/ArcanistBaseWorkflow.php
@@ -1,542 +1,547 @@
<?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.
*/
class ArcanistBaseWorkflow {
private $conduit;
private $userGUID;
private $userName;
private $repositoryAPI;
private $workingCopy;
private $arguments;
private $command;
private $arcanistConfiguration;
private $parentWorkflow;
private $changeCache = array();
public function __construct() {
}
public function setArcanistConfiguration($arcanist_configuration) {
$this->arcanistConfiguration = $arcanist_configuration;
return $this;
}
public function getArcanistConfiguration() {
return $this->arcanistConfiguration;
}
public function getCommandHelp() {
return get_class($this).": Undocumented";
}
public function requiresWorkingCopy() {
return false;
}
public function requiresConduit() {
return false;
}
public function requiresAuthentication() {
return false;
}
public function requiresRepositoryAPI() {
return false;
}
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
public function setUserName($user_name) {
$this->userName = $user_name;
return $this;
}
public function getUserName() {
return $this->userName;
}
public function getArguments() {
return array();
}
private function setParentWorkflow($parent_workflow) {
$this->parentWorkflow = $parent_workflow;
return $this;
}
protected function getParentWorkflow() {
return $this->parentWorkflow;
}
public function buildChildWorkflow($command, array $argv) {
$arc_config = $this->getArcanistConfiguration();
$workflow = $arc_config->buildWorkflow($command);
$workflow->setParentWorkflow($this);
$workflow->setCommand($command);
if ($this->repositoryAPI) {
$workflow->setRepositoryAPI($this->repositoryAPI);
}
if ($this->userGUID) {
$workflow->setUserGUID($this->getUserGUID());
$workflow->setUserName($this->getUserName());
}
if ($this->conduit) {
$workflow->setConduit($this->conduit);
}
if ($this->workingCopy) {
$workflow->setWorkingCopy($this->workingCopy);
}
$workflow->setArcanistConfiguration($arc_config);
$workflow->parseArguments(array_values($argv));
return $workflow;
}
public function getArgument($key, $default = null) {
$args = $this->arguments;
if (!array_key_exists($key, $args)) {
return $default;
}
return $args[$key];
}
final public function getCompleteArgumentSpecification() {
$spec = $this->getArguments();
$arc_config = $this->getArcanistConfiguration();
$command = $this->getCommand();
$spec += $arc_config->getCustomArgumentsForCommand($command);
return $spec;
}
public function parseArguments(array $args) {
$spec = $this->getCompleteArgumentSpecification();
$dict = array();
$more_key = null;
if (!empty($spec['*'])) {
$more_key = $spec['*'];
unset($spec['*']);
$dict[$more_key] = array();
}
$short_to_long_map = array();
foreach ($spec as $long => $options) {
if (!empty($options['short'])) {
$short_to_long_map[$options['short']] = $long;
}
}
$more = array();
for ($ii = 0; $ii < count($args); $ii++) {
$arg = $args[$ii];
$arg_name = null;
$arg_key = null;
if ($arg == '--') {
$more = array_merge(
$more,
array_slice($args, $ii + 1));
break;
} else if (!strncmp($arg, '--', 2)) {
$arg_key = substr($arg, 2);
if (!array_key_exists($arg_key, $spec)) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
} else if (!strncmp($arg, '-', 1)) {
$arg_key = substr($arg, 1);
if (empty($short_to_long_map[$arg_key])) {
throw new ArcanistUsageException(
"Unknown argument '{$arg_key}'. Try 'arc help'.");
}
$arg_key = $short_to_long_map[$arg_key];
} else {
$more[] = $arg;
continue;
}
$options = $spec[$arg_key];
if (empty($options['param'])) {
$dict[$arg_key] = true;
} else {
if ($ii == count($args) - 1) {
throw new ArcanistUsageException(
"Option '{$arg}' requires a parameter.");
}
$dict[$arg_key] = $args[$ii + 1];
$ii++;
}
}
if ($more) {
if ($more_key) {
$dict[$more_key] = $more;
} else {
$example = reset($more);
throw new ArcanistUsageException(
"Unrecognized argument '{$example}'. Try 'arc help'.");
}
}
foreach ($dict as $key => $value) {
if (empty($spec[$key]['conflicts'])) {
continue;
}
foreach ($spec[$key]['conflicts'] as $conflict => $more) {
if (isset($dict[$conflict])) {
if ($more) {
$more = ': '.$more;
} else {
$more = '.';
}
// TODO: We'll always display these as long-form, when the user might
// have typed them as short form.
throw new ArcanistUsageException(
"Arguments '--{$key}' and '--{$conflict}' are mutually exclusive".
$more);
}
}
}
$this->arguments = $dict;
$this->didParseArguments();
return $this;
}
protected function didParseArguments() {
// Override this to customize workflow argument behavior.
}
public function getWorkingCopy() {
if (!$this->workingCopy) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a working copy, override ".
"requiresWorkingCopy() to return true.");
}
return $this->workingCopy;
}
public function setWorkingCopy(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
public function getConduit() {
if (!$this->conduit) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a Conduit, override ".
"requiresConduit() to return true.");
}
return $this->conduit;
}
public function setConduit(ConduitClient $conduit) {
$this->conduit = $conduit;
return $this;
}
public function getUserGUID() {
if (!$this->userGUID) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires authentication, override ".
"requiresAuthentication() to return true.");
}
return $this->userGUID;
}
public function setUserGUID($guid) {
$this->userGUID = $guid;
return $this;
}
public function setRepositoryAPI($api) {
$this->repositoryAPI = $api;
return $this;
}
public function getRepositoryAPI() {
if (!$this->repositoryAPI) {
$workflow = get_class($this);
throw new Exception(
"This workflow ('{$workflow}') requires a Repository API, override ".
"requiresRepositoryAPI() to return true.");
}
return $this->repositoryAPI;
}
protected function shouldRequireCleanUntrackedFiles() {
return empty($this->arguments['allow-untracked']);
}
protected function requireCleanWorkingCopy() {
$api = $this->getRepositoryAPI();
$untracked = $api->getUntrackedChanges();
if ($this->shouldRequireCleanUntrackedFiles()) {
if (!empty($untracked)) {
throw new ArcanistUsageException(
"You have untracked files in this working copy:\n".
" ".implode("\n ", $untracked)."\n\n".
"Add or delete them before proceeding, or include them in your ".
"ignore rules. To bypass this check, use --allow-untracked.");
}
}
if ($api->getMergeConflicts()) {
throw new ArcanistUsageException(
"You have merge conflicts in this working copy. Resolve merge ".
"conflicts before proceeding.");
}
if ($api->getUnstagedChanges()) {
throw new ArcanistUsageException(
"You have unstaged changes in this branch. Stage and commit (or ".
"revert) them before proceeding.");
}
if ($api->getUncommittedChanges()) {
throw new ArcanistUsageException(
"You have uncommitted changes in this branch. Commit (or revert) them ".
"before proceeding.");
}
}
protected function chooseRevision(
array $revision_data,
$revision_id,
$prompt = null) {
$revisions = array();
foreach ($revision_data as $data) {
$ref = ArcanistDifferentialRevisionRef::newFromDictionary($data);
$revisions[$ref->getID()] = $ref;
}
if ($revision_id) {
$revision_id = $this->normalizeRevisionID($revision_id);
if (empty($revisions[$revision_id])) {
throw new ArcanistChooseInvalidRevisionException();
}
return $revisions[$revision_id];
}
if (!count($revisions)) {
throw new ArcanistChooseNoRevisionsException();
}
$repository_api = $this->getRepositoryAPI();
$candidates = array();
$cur_path = $repository_api->getPath();
foreach ($revisions as $revision) {
$source_path = $revision->getSourcePath();
if ($source_path == $cur_path) {
$candidates[] = $revision;
}
}
if (count($candidates) == 1) {
$candidate = reset($candidates);
$revision_id = $candidate->getID();
}
if ($revision_id) {
return $revisions[$revision_id];
}
$revision_indexes = array_keys($revisions);
echo "\n";
$ii = 1;
foreach ($revisions as $revision) {
echo ' ['.$ii++.'] D'.$revision->getID().' '.$revision->getName()."\n";
}
while (true) {
$id = phutil_console_prompt($prompt);
$id = trim(strtoupper($id), 'D');
if (isset($revisions[$id])) {
return $revisions[$id];
}
if (isset($revision_indexes[$id - 1])) {
return $revisions[$revision_indexes[$id - 1]];
}
}
}
protected function loadDiffBundleFromConduit(
ConduitClient $conduit,
$diff_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'diff_id' => $diff_id,
));
}
protected function loadRevisionBundleFromConduit(
ConduitClient $conduit,
$revision_id) {
return $this->loadBundleFromConduit(
$conduit,
array(
'revision_id' => $revision_id,
));
}
private function loadBundleFromConduit(
ConduitClient $conduit,
$params) {
$future = $conduit->callMethod('differential.getdiff', $params);
$diff = $future->resolve();
$changes = array();
foreach ($diff['changes'] as $changedict) {
$changes[] = ArcanistDiffChange::newFromDictionary($changedict);
}
$bundle = ArcanistBundle::newFromChanges($changes);
return $bundle;
}
protected function getChangedLines($path, $mode) {
if (is_dir($path)) {
return array();
}
$change = $this->getChange($path);
$lines = $change->getChangedLines($mode);
return array_keys($lines);
}
private function getChange($path) {
$repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistSubversionAPI) {
if (empty($this->changeCache[$path])) {
$diff = $repository_api->getRawDiffText($path);
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
if (count($changes) != 1) {
throw new Exception("Expected exactly one change.");
}
$this->changeCache[$path] = reset($changes);
}
} else {
if (empty($this->changeCache)) {
$diff = $repository_api->getFullGitDiff();
$parser = new ArcanistDiffParser();
$changes = $parser->parseDiff($diff);
foreach ($changes as $change) {
$this->changeCache[$change->getCurrentPath()] = $change;
}
}
}
if (empty($this->changeCache[$path])) {
- // TODO: This can legitimately occur under git if you make a change,
- // "git commit" it, and then revert the change in the working copy and
- // run "arc lint". We should probably just make a dummy, empty changeset
- // in this case, at least under git.
- throw new Exception(
- "Trying to get change for unchanged path '{$path}'!");
+ if ($repository_api instanceof ArcanistGitAPI) {
+ // This can legitimately occur under git if you make a change, "git
+ // commit" it, and then revert the change in the working copy and run
+ // "arc lint".
+ $change = new ArcanistDiffChange();
+ $change->setCurrentPath($path);
+ return $change;
+ } else {
+ throw new Exception(
+ "Trying to get change for unchanged path '{$path}'!");
+ }
}
return $this->changeCache[$path];
}
final public function willRunWorkflow() {
$spec = $this->getCompleteArgumentSpecification();
foreach ($this->arguments as $arg => $value) {
if (empty($spec[$arg])) {
continue;
}
$options = $spec[$arg];
if (!empty($options['supports'])) {
$system_name = $this->getRepositoryAPI()->getSourceControlSystemName();
if (!in_array($system_name, $options['supports'])) {
$extended_info = null;
if (!empty($options['nosupport'][$system_name])) {
$extended_info = ' '.$options['nosupport'][$system_name];
}
throw new ArcanistUsageException(
"Option '--{$arg}' is not supported under {$system_name}.".
$extended_info);
}
}
}
}
protected function parseGitRelativeCommit(ArcanistGitAPI $api, array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException(
"Specify exactly one commit.");
}
$base = reset($argv);
if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$merge_base = $base;
} else {
list($err, $merge_base) = exec_manual(
'(cd %s; git merge-base %s HEAD)',
$api->getPath(),
$base);
if ($err) {
throw new ArcanistUsageException(
"Unable to parse git commit name '{$base}'.");
}
}
$api->setRelativeCommit(trim($merge_base));
}
protected function normalizeRevisionID($revision_id) {
return ltrim(strtoupper($revision_id), 'D');
}
}
diff --git a/src/workflow/lint/ArcanistLintWorkflow.php b/src/workflow/lint/ArcanistLintWorkflow.php
index dc7c9301..e32d3426 100644
--- a/src/workflow/lint/ArcanistLintWorkflow.php
+++ b/src/workflow/lint/ArcanistLintWorkflow.php
@@ -1,226 +1,229 @@
<?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.
*/
class ArcanistLintWorkflow extends ArcanistBaseWorkflow {
const RESULT_OKAY = 0;
const RESULT_WARNINGS = 1;
const RESULT_ERRORS = 2;
const RESULT_SKIP = 3;
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**lint** [__options__] [__paths__] (svn)
**lint** [__options__] [__commit_range__] (git)
Supports: git, svn
Run static analysis on changes to check for mistakes. If no files
are specified, lint will be run on all files which have been modified.
EOTEXT
);
}
public function getArguments() {
return array(
'lintall' => array(
'help' =>
"Show all lint warnings, not just those on changed lines."
),
'summary' => array(
'help' =>
"Show lint warnings in a more compact format."
),
'advice' => array(
'help' =>
"Show lint advice, not just warnings and errors."
),
'engine' => array(
'param' => 'classname',
'help' =>
"Override configured lint engine for this project."
),
'*' => 'paths',
);
}
public function requiresWorkingCopy() {
return true;
}
public function run() {
$working_copy = $this->getWorkingCopy();
$engine = $this->getArgument('engine');
if (!$engine) {
$engine = $working_copy->getConfig('lint_engine');
}
$should_lint_all = $this->getArgument('lintall');
$repository_api = null;
if (!$should_lint_all) {
try {
$repository_api = ArcanistRepositoryAPI::newAPIFromWorkingCopyIdentity(
$working_copy);
$this->setRepositoryAPI($repository_api);
} catch (ArcanistUsageException $ex) {
throw new ArcanistUsageException(
$ex->getMessage()."\n\n".
"Use '--lintall' to ignore working copy changes when running lint.");
}
if ($repository_api instanceof ArcanistSubversionAPI) {
$paths = $repository_api->getWorkingCopyStatus();
$list = new FileList($this->getArgument('paths'));
foreach ($paths as $path => $flags) {
if (!$list->contains($path)) {
unset($paths[$path]);
}
}
} else {
$this->parseGitRelativeCommit(
$repository_api,
$this->getArgument('paths'));
$paths = $repository_api->getWorkingCopyStatus();
}
foreach ($paths as $path => $flags) {
if ($flags & ArcanistRepositoryAPI::FLAG_UNTRACKED) {
unset($paths[$path]);
}
}
$paths = array_keys($paths);
} else {
$paths = $this->getArgument('paths');
if (empty($paths)) {
throw new ArcanistUsageException(
"You must specify one or more files to lint when using '--lintall'.");
}
}
if (!$engine) {
throw new ArcanistNoEngineException(
"No lint engine configured for this project. Edit .arcconfig to ".
"specify a lint engine.");
}
$ok = phutil_autoload_class($engine);
if (!$ok) {
throw new ArcanistUsageException(
"Configured lint engine '{$engine}' could not be loaded.");
}
$engine = newv($engine, array());
$engine->setWorkingCopy($working_copy);
if ($this->getArgument('advice')) {
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ADVICE);
} else {
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_WARNING);
}
$engine->setPaths($paths);
if (!$should_lint_all) {
foreach ($paths as $path) {
$engine->setPathChangedLines(
$path,
$this->getChangedLines($path, 'new'));
}
}
$results = $engine->run();
$apply_patches = true;
$prompt_patches = true;
$wrote_to_disk = false;
$renderer = new ArcanistLintRenderer();
if ($this->getArgument('summary')) {
$renderer->setSummaryMode(true);
}
foreach ($results as $result) {
if (!$result->getMessages()) {
continue;
}
echo $renderer->renderLintResult($result);
if ($apply_patches && $result->isPatchable()) {
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
$old = $patcher->getUnmodifiedFileContent();
$new = $patcher->getModifiedFileContent();
if ($prompt_patches) {
$old_file = $result->getFilePathOnDisk();
+ if (!Filesystem::pathExists($old_file)) {
+ $old_file = '/dev/null';
+ }
$new_file = new TempFile();
Filesystem::writeFile($new_file, $new);
// TODO: Improve the behavior here, make it more like
// difference_render().
passthru(csprintf("diff -u %s %s", $old_file, $new_file));
$prompt = phutil_console_format(
"Apply this patch to __%s__?",
$result->getPath());
if (!phutil_console_confirm($prompt, $default_no = false)) {
continue;
}
}
$patcher->writePatchToDisk();
$wrote_to_disk = true;
}
}
if ($wrote_to_disk && ($repository_api instanceof ArcanistGitAPI)) {
$amend = phutil_console_confirm(
"Amend HEAD with lint patches?",
$default_no = false);
if (!$amend) {
- throw new ArcanistUsageException("Resolve lint changes and rediff.");
+ throw new ArcanistUsageException("Resolve lint changes and relint.");
}
execx(
'(cd %s; git commit -a --amend -C HEAD)',
$repository_api->getPath());
}
$result_code = self::RESULT_OKAY;
foreach ($results as $result) {
foreach ($result->getMessages() as $message) {
if (!$message->isPatchApplied()) {
if ($message->isError()) {
$result_code = self::RESULT_ERRORS;
break;
} else if ($message->isWarning()) {
$result_code = self::RESULT_WARNINGS;
}
}
}
}
if (!$this->getParentWorkflow()) {
if ($result_code == self::RESULT_OKAY) {
echo phutil_console_format(
"<bg:green>** OKAY **</bg> No lint warnings.\n");
}
}
return $result_code;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 8, 08:22 (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1034337
Default Alt Text
(68 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment