Page MenuHomeSealhub

No OneTemporary

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

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)

Event Timeline