Page MenuHomeSealhub

No OneTemporary

diff --git a/scripts/arcanist.php b/scripts/arcanist.php
index 2dc7611a..0d35fc95 100755
--- a/scripts/arcanist.php
+++ b/scripts/arcanist.php
@@ -1,616 +1,616 @@
#!/usr/bin/env php
<?php
sanity_check_environment();
require_once dirname(__FILE__).'/__init_script__.php';
ini_set('memory_limit', -1);
$original_argv = $argv;
$args = new PhutilArgumentParser($argv);
$args->parseStandardArguments();
$args->parsePartial(
array(
array(
'name' => 'load-phutil-library',
'param' => 'path',
'help' => 'Load a libphutil library.',
'repeat' => true,
),
array(
'name' => 'skip-arcconfig',
),
array(
'name' => 'conduit-uri',
'param' => 'uri',
'help' => 'Connect to Phabricator install specified by __uri__.',
),
array(
'name' => 'conduit-version',
'param' => 'version',
'help' => '(Developers) Mock client version in protocol handshake.',
),
array(
'name' => 'conduit-timeout',
'param' => 'timeout',
'help' => 'Set Conduit timeout (in seconds).',
),
));
$config_trace_mode = $args->getArg('trace');
$force_conduit = $args->getArg('conduit-uri');
$force_conduit_version = $args->getArg('conduit-version');
$conduit_timeout = $args->getArg('conduit-timeout');
$skip_arcconfig = $args->getArg('skip-arcconfig');
$load = $args->getArg('load-phutil-library');
$help = $args->getArg('help');
$argv = $args->getUnconsumedArgumentVector();
$args = array_values($argv);
$working_directory = getcwd();
$console = PhutilConsole::getConsole();
$config = null;
$workflow = null;
try {
$console->writeLog(
"libphutil loaded from '%s'.\n",
phutil_get_library_root('phutil'));
$console->writeLog(
"arcanist loaded from '%s'.\n",
phutil_get_library_root('arcanist'));
if (!$args) {
if ($help) {
$args = array('help');
} else {
throw new ArcanistUsageException("No command provided. Try 'arc help'.");
}
} else if ($help) {
array_unshift($args, 'help');
}
$configuration_manager = new ArcanistConfigurationManager();
$global_config = $configuration_manager->readUserArcConfig();
$system_config = $configuration_manager->readSystemArcConfig();
if ($skip_arcconfig) {
$working_copy = ArcanistWorkingCopyIdentity::newDummyWorkingCopy();
} else {
$working_copy =
ArcanistWorkingCopyIdentity::newFromPath($working_directory);
}
$configuration_manager->setWorkingCopyIdentity($working_copy);
reenter_if_this_is_arcanist_or_libphutil(
$console,
$working_copy,
$original_argv);
// Load additional libraries, which can provide new classes like configuration
// overrides, linters and lint engines, unit test engines, etc.
// If the user specified "--load-phutil-library" one or more times from
// the command line, we load those libraries **instead** of whatever else
// is configured. This is basically a debugging feature to let you force
// specific libraries to load regardless of the state of the world.
if ($load) {
$console->writeLog(
"Using '--load-phutil-library' flag, configuration will be ignored ".
"and configured libraries will not be loaded."."\n");
// Load the flag libraries. These must load, since the user specified them
// explicitly.
arcanist_load_libraries(
$load,
$must_load = true,
$lib_source = 'a "--load-phutil-library" flag',
$working_copy);
} else {
// Load libraries in system 'load' config. In contrast to global config, we
// fail hard here because this file is edited manually, so if 'arc' breaks
// that doesn't make it any more difficult to correct.
arcanist_load_libraries(
idx($system_config, 'load', array()),
$must_load = true,
$lib_source = 'the "load" setting in system config',
$working_copy);
// Load libraries in global 'load' config, as per "arc set-config load". We
// need to fail softly if these break because errors would prevent the user
// from running "arc set-config" to correct them.
arcanist_load_libraries(
idx($global_config, 'load', array()),
$must_load = false,
$lib_source = 'the "load" setting in global config',
$working_copy);
// Load libraries in ".arcconfig". Libraries here must load.
arcanist_load_libraries(
- $working_copy->getConfig('load'),
+ $working_copy->getProjectConfig('load'),
$must_load = true,
$lib_source = 'the "load" setting in ".arcconfig"',
$working_copy);
}
$user_config = $configuration_manager->readUserConfigurationFile();
- $config_class = $working_copy->getConfig('arcanist_configuration');
+ $config_class = $working_copy->getProjectConfig('arcanist_configuration');
if ($config_class) {
$config = new $config_class();
} else {
$config = new ArcanistConfiguration();
}
$command = strtolower($args[0]);
$args = array_slice($args, 1);
$workflow = $config->selectWorkflow(
$command,
$args,
$configuration_manager,
$console);
$workflow->setConfigurationManager($configuration_manager);
$workflow->setArcanistConfiguration($config);
$workflow->setCommand($command);
$workflow->setWorkingDirectory($working_directory);
$workflow->parseArguments($args);
// Write the command into the environment so that scripts (for example, local
// Git commit hooks) can detect that they're being run via `arc` and change
// their behaviors.
putenv('ARCANIST='.$command);
if ($force_conduit_version) {
$workflow->forceConduitVersion($force_conduit_version);
}
if ($conduit_timeout) {
$workflow->setConduitTimeout($conduit_timeout);
}
$need_working_copy = $workflow->requiresWorkingCopy();
$need_conduit = $workflow->requiresConduit();
$need_auth = $workflow->requiresAuthentication();
$need_repository_api = $workflow->requiresRepositoryAPI();
$want_repository_api = $workflow->desiresRepositoryAPI();
$want_working_copy = $workflow->desiresWorkingCopy() ||
$want_repository_api;
$need_conduit = $need_conduit ||
$need_auth;
$need_working_copy = $need_working_copy ||
$need_repository_api;
if ($need_working_copy || $want_working_copy) {
if ($need_working_copy && !$working_copy->getProjectRoot()) {
throw new ArcanistUsageException(
"This command must be run in a Git, Mercurial or Subversion working ".
"copy.");
}
$configuration_manager->setWorkingCopyIdentity($working_copy);
}
if ($force_conduit) {
$conduit_uri = $force_conduit;
} else {
$project_conduit_uri =
$configuration_manager->getProjectConfig('conduit_uri');
if ($project_conduit_uri) {
$conduit_uri = $project_conduit_uri;
} else {
$conduit_uri = idx($global_config, 'default');
}
}
if ($conduit_uri) {
// Set the URI path to '/api/'. TODO: Originally, I contemplated letting
// you deploy Phabricator somewhere other than the domain root, but ended
// up never pursuing that. We should get rid of all "/api/" silliness
// in things users are expected to configure. This is already happening
// to some degree, e.g. "arc install-certificate" does it for you.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_uri->setPath('/api/');
$conduit_uri = (string)$conduit_uri;
}
$workflow->setConduitURI($conduit_uri);
// Apply global CA bundle from configs.
$ca_bundle = $configuration_manager->getConfigFromAnySource('https.cabundle');
if ($ca_bundle) {
$ca_bundle = Filesystem::resolvePath(
$ca_bundle, $working_copy->getProjectRoot());
HTTPSFuture::setGlobalCABundleFromPath($ca_bundle);
}
$blind_key = 'https.blindly-trust-domains';
$blind_trust = $configuration_manager->getConfigFromAnySource($blind_key);
if ($blind_trust) {
HTTPSFuture::setBlindlyTrustDomains($blind_trust);
}
if ($need_conduit) {
if (!$conduit_uri) {
$message = phutil_console_format(
"This command requires arc to connect to a Phabricator install, but ".
"no Phabricator installation is configured. To configure a ".
"Phabricator URI:\n\n".
" - set a default location with `arc set-config default <uri>`; or\n".
" - specify '--conduit-uri=uri' explicitly; or\n".
" - run 'arc' in a working copy with an '.arcconfig'.\n");
$message = phutil_console_wrap($message);
throw new ArcanistUsageException($message);
}
$workflow->establishConduit();
}
$hosts_config = idx($user_config, 'hosts', array());
$host_config = idx($hosts_config, $conduit_uri, array());
$user_name = idx($host_config, 'user');
$certificate = idx($host_config, 'cert');
$description = implode(' ', $original_argv);
$credentials = array(
'user' => $user_name,
'certificate' => $certificate,
'description' => $description,
);
$workflow->setConduitCredentials($credentials);
if ($need_auth) {
if (!$user_name || !$certificate) {
$arc = 'arc';
if ($force_conduit) {
$arc .= csprintf(' --conduit-uri=%s', $conduit_uri);
}
throw new ArcanistUsageException(
phutil_console_format(
"YOU NEED TO __INSTALL A CERTIFICATE__ TO LOGIN TO PHABRICATOR\n\n".
"You are trying to connect to '{$conduit_uri}' but do not have ".
"a certificate installed for this host. Run:\n\n".
" $ **{$arc} install-certificate**\n\n".
"...to install one."));
}
$workflow->authenticateConduit();
}
if ($need_repository_api || ($want_repository_api && $working_copy)) {
$repository_api = ArcanistRepositoryAPI::newAPIFromConfigurationManager(
$configuration_manager);
$workflow->setRepositoryAPI($repository_api);
}
$listeners = $configuration_manager->getConfigFromAnySource(
'events.listeners');
if ($listeners) {
foreach ($listeners as $listener) {
$console->writeLog(
"Registering event listener '%s'.\n",
$listener);
try {
id(new $listener())->register();
} catch (PhutilMissingSymbolException $ex) {
// Continue anwyay, since you may otherwise be unable to run commands
// like `arc set-config events.listeners` in order to repair the damage
// you've caused.
$console->writeErr(
"ERROR: Failed to load event listener '%s'!\n",
$listener);
}
}
}
$config->willRunWorkflow($command, $workflow);
$workflow->willRunWorkflow();
try {
$err = $workflow->run();
$config->didRunWorkflow($command, $workflow, $err);
} catch (Exception $e) {
$workflow->finalize();
throw $e;
}
$workflow->finalize();
exit((int)$err);
} catch (ArcanistNoEffectException $ex) {
echo $ex->getMessage()."\n";
} catch (Exception $ex) {
$is_usage = ($ex instanceof ArcanistUsageException);
if ($is_usage) {
echo phutil_console_format(
"**Usage Exception:** %s\n",
$ex->getMessage());
}
if ($config) {
$config->didAbortWorkflow($command, $workflow, $ex);
}
if ($config_trace_mode) {
echo "\n";
throw $ex;
}
if (!$is_usage) {
echo phutil_console_format(
"**Exception**\n%s\n%s\n",
$ex->getMessage(),
"(Run with --trace for a full exception trace.)");
}
exit(1);
}
/**
* Perform some sanity checks against the possible diversity of PHP builds in
* the wild, like very old versions and builds that were compiled with flags
* that exclude core functionality.
*/
function sanity_check_environment() {
// NOTE: We don't have phutil_is_windows() yet here.
$is_windows = (DIRECTORY_SEPARATOR != '/');
// We use stream_socket_pair() which is not available on Windows earlier.
$min_version = ($is_windows ? '5.3.0' : '5.2.3');
$cur_version = phpversion();
if (version_compare($cur_version, $min_version, '<')) {
die_with_bad_php(
"You are running PHP version '{$cur_version}', which is older than ".
"the minimum version, '{$min_version}'. Update to at least ".
"'{$min_version}'.");
}
if ($is_windows) {
$need_functions = array(
'curl_init' => array('builtin-dll', 'php_curl.dll'),
);
} else {
$need_functions = array(
'curl_init' => array(
'text',
"You need to install the cURL PHP extension, maybe with ".
"'apt-get install php5-curl' or 'yum install php53-curl' or ".
"something similar."),
'json_decode' => array('flag', '--without-json'),
);
}
$problems = array();
$config = null;
$show_config = false;
foreach ($need_functions as $fname => $resolution) {
if (function_exists($fname)) {
continue;
}
static $info;
if ($info === null) {
ob_start();
phpinfo(INFO_GENERAL);
$info = ob_get_clean();
$matches = null;
if (preg_match('/^Configure Command =>\s*(.*?)$/m', $info, $matches)) {
$config = $matches[1];
}
}
$generic = true;
list($what, $which) = $resolution;
if ($what == 'flag' && strpos($config, $which) !== false) {
$show_config = true;
$generic = false;
$problems[] =
"This build of PHP was compiled with the configure flag '{$which}', ".
"which means it does not have the function '{$fname}()'. This ".
"function is required for arc to run. Rebuild PHP without this flag. ".
"You may also be able to build or install the relevant extension ".
"separately.";
}
if ($what == 'builtin-dll') {
$generic = false;
$problems[] =
"Your install of PHP does not have the '{$which}' extension enabled. ".
"Edit your php.ini file and uncomment the line which reads ".
"'extension={$which}'.";
}
if ($what == 'text') {
$generic = false;
$problems[] = $which;
}
if ($generic) {
$problems[] =
"This build of PHP is missing the required function '{$fname}()'. ".
"Rebuild PHP or install the extension which provides '{$fname}()'.";
}
}
if ($problems) {
if ($show_config) {
$problems[] = "PHP was built with this configure command:\n\n{$config}";
}
die_with_bad_php(implode("\n\n", $problems));
}
}
function die_with_bad_php($message) {
echo "\nPHP CONFIGURATION ERRORS\n\n";
echo $message;
echo "\n\n";
exit(1);
}
function arcanist_load_libraries(
$load,
$must_load,
$lib_source,
ArcanistWorkingCopyIdentity $working_copy) {
if (!$load) {
return;
}
if (!is_array($load)) {
$error = "Libraries specified by {$lib_source} are invalid; expected ".
"a list. Check your configuration.";
$console = PhutilConsole::getConsole();
$console->writeErr("WARNING: %s\n", $error);
return;
}
foreach ($load as $location) {
// Try to resolve the library location. We look in several places, in
// order:
//
// 1. Inside the working copy. This is for phutil libraries within the
// project. For instance "library/src" will resolve to
// "./library/src" if it exists.
// 2. In the same directory as the working copy. This allows you to
// check out a library alongside a working copy and reference it.
// If we haven't resolved yet, "library/src" will try to resolve to
// "../library/src" if it exists.
// 3. Using normal libphutil resolution rules. Generally, this means
// that it checks for libraries next to libphutil, then libraries
// in the PHP include_path.
//
// Note that absolute paths will just resolve absolutely through rule (1).
$resolved = false;
// Check inside the working copy. This also checks absolute paths, since
// they'll resolve absolute and just ignore the project root.
$resolved_location = Filesystem::resolvePath(
$location,
$working_copy->getProjectRoot());
if (Filesystem::pathExists($resolved_location)) {
$location = $resolved_location;
$resolved = true;
}
// If we didn't find anything, check alongside the working copy.
if (!$resolved) {
$resolved_location = Filesystem::resolvePath(
$location,
dirname($working_copy->getProjectRoot()));
if (Filesystem::pathExists($resolved_location)) {
$location = $resolved_location;
$resolved = true;
}
}
$console = PhutilConsole::getConsole();
$console->writeLog(
"Loading phutil library from '%s'...\n",
$location);
$error = null;
try {
phutil_load_library($location);
} catch (PhutilBootloaderException $ex) {
$error = "Failed to load phutil library at location '{$location}'. ".
"This library is specified by {$lib_source}. Check that the ".
"setting is correct and the library is located in the right ".
"place.";
if ($must_load) {
throw new ArcanistUsageException($error);
} else {
fwrite(STDERR, phutil_console_wrap('WARNING: '.$error."\n\n"));
}
} catch (PhutilLibraryConflictException $ex) {
if ($ex->getLibrary() != 'arcanist') {
throw $ex;
}
$arc_dir = dirname(dirname(__FILE__));
$error =
"You are trying to run one copy of Arcanist on another copy of ".
"Arcanist. This operation is not supported. To execute Arcanist ".
"operations against this working copy, run './bin/arc' (from the ".
"current working copy) not some other copy of 'arc' (you ran one ".
"from '{$arc_dir}').";
throw new ArcanistUsageException($error);
}
}
}
/**
* NOTE: SPOOKY BLACK MAGIC
*
* When arc is run in a copy of arcanist other than itself, or a copy of
* libphutil other than the one we loaded, reenter the script and force it
* to use the current working directory instead of the default.
*
* In the case of execution inside arcanist/, we force execution of the local
* arc binary.
*
* In the case of execution inside libphutil/, we force the local copy to load
* instead of the one selected by default rules.
*
* @param PhutilConsole Console.
* @param ArcanistWorkingCopyIdentity The current working copy.
* @param array Original arc arguments.
* @return void
*/
function reenter_if_this_is_arcanist_or_libphutil(
PhutilConsole $console,
ArcanistWorkingCopyIdentity $working_copy,
array $original_argv) {
$project_id = $working_copy->getProjectID();
if ($project_id != 'arcanist' && $project_id != 'libphutil') {
// We're not in a copy of arcanist or libphutil.
return;
}
$library_names = array(
'arcanist' => 'arcanist',
'libphutil' => 'phutil',
);
$library_root = phutil_get_library_root($library_names[$project_id]);
$project_root = $working_copy->getProjectRoot();
if (Filesystem::isDescendant($library_root, $project_root)) {
// We're in a copy of arcanist or libphutil, but already loaded the correct
// copy. Continue execution normally.
return;
}
if ($project_id == 'libphutil') {
$console->writeLog(
"This is libphutil! Forcing this copy to load...\n");
$original_argv[0] = dirname(phutil_get_library_root('arcanist')).'/bin/arc';
$libphutil_path = $project_root;
} else {
$console->writeLog(
"This is arcanist! Forcing this copy to run...\n");
$original_argv[0] = $project_root.'/bin/arc';
$libphutil_path = dirname(phutil_get_library_root('phutil'));
}
if (phutil_is_windows()) {
$err = phutil_passthru(
'set ARC_PHUTIL_PATH=%s & %Ls',
$libphutil_path,
$original_argv);
} else {
$err = phutil_passthru(
'ARC_PHUTIL_PATH=%s %Ls',
$libphutil_path,
$original_argv);
}
exit($err);
}
diff --git a/src/configuration/ArcanistConfigurationManager.php b/src/configuration/ArcanistConfigurationManager.php
index a9202de4..04494b90 100644
--- a/src/configuration/ArcanistConfigurationManager.php
+++ b/src/configuration/ArcanistConfigurationManager.php
@@ -1,248 +1,248 @@
<?php
/**
* This class holds everything related to configuration and configuration files.
*
* @group config
*/
final class ArcanistConfigurationManager {
private $runtimeConfig = array();
private $workingCopy = null;
public function setWorkingCopyIdentity(
ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
}
/* -( Get config )--------------------------------------------------------- */
const CONFIG_SOURCE_RUNTIME = 'runtime';
const CONFIG_SOURCE_LOCAL = 'local';
const CONFIG_SOURCE_PROJECT = 'project';
const CONFIG_SOURCE_USER = 'user';
const CONFIG_SOURCE_SYSTEM = 'system';
public function getProjectConfig($key) {
if ($this->workingCopy) {
- return $this->workingCopy->getConfig($key);
+ return $this->workingCopy->getProjectConfig($key);
}
return null;
}
public function getLocalConfig($key) {
if ($this->workingCopy) {
return $this->workingCopy->getLocalConfig($key);
}
return null;
}
public function getWorkingCopyIdentity() {
return $this->workingCopy;
}
/**
* Read a configuration directive from any available configuration source.
* This includes the directive in local, user and system configuration in
* addition to project configuration, and configuration provided as command
* arguments ("runtime").
* The precedence is runtime > local > project > user > system
*
* @param key Key to read.
* @param wild Default value if key is not found.
* @return wild Value, or default value if not found.
*
* @task config
*/
public function getConfigFromAnySource($key, $default = null) {
$all = $this->getConfigFromAllSources($key);
return empty($all) ? $default : head($all);
}
/**
* For the advanced case where you want customized configuratin handling.
*
* Reads the configuration from all available sources, returning a map (array)
* of results, with the source as key. Missing values will not be in the map,
* so an empty array will be returned if no results are found.
*
* The map is ordered by the cannonical sources precedence, which is:
* runtime > local > project > user > system
*
* @param key Key to read
* @return array Mapping of source => value read. Sources with no value are
* not in the array.
*
* @task config
*/
public function getConfigFromAllSources($key) {
$results = array();
$settings = new ArcanistSettings();
$pval = idx($this->runtimeConfig, $key);
if ($pval !== null) {
$results[self::CONFIG_SOURCE_RUNTIME] =
$settings->willReadValue($key, $pval);
}
$pval = $this->getLocalConfig($key);
if ($pval !== null) {
$results[self::CONFIG_SOURCE_LOCAL] =
$settings->willReadValue($key, $pval);
}
$pval = $this->getProjectConfig($key);
if ($pval !== null) {
$results[self::CONFIG_SOURCE_PROJECT] =
$settings->willReadValue($key, $pval);
}
$user_config = $this->readUserArcConfig();
$pval = idx($user_config, $key);
if ($pval !== null) {
$results[self::CONFIG_SOURCE_USER] =
$settings->willReadValue($key, $pval);
}
$system_config = $this->readSystemArcConfig();
$pval = idx($system_config, $key);
if ($pval !== null) {
$results[self::CONFIG_SOURCE_SYSTEM] =
$settings->willReadValue($key, $pval);
}
return $results;
}
/**
* Sets a runtime config value that takes precedence over any static
* config values.
*
* @param key Key to set.
* @param value The value of the key.
*
* @task config
*/
public function setRuntimeConfig($key, $value) {
$this->runtimeConfig[$key] = $value;
return $this;
}
/* -( Read/write config )--------------------------------------------------- */
public function readLocalArcConfig() {
if ($this->workingCopy) {
return $this->workingCopy->readLocalArcConfig();
}
return array();
}
public function writeLocalArcConfig(array $config) {
if ($this->workingCopy) {
return $this->workingCopy->writeLocalArcConfig($config);
}
return false;
}
/**
* This is probably not the method you're looking for; try
* @{method:readUserArcConfig}.
*/
public function readUserConfigurationFile() {
$user_config = array();
$user_config_path = self::getUserConfigurationFileLocation();
if (Filesystem::pathExists($user_config_path)) {
if (!phutil_is_windows()) {
$mode = fileperms($user_config_path);
if (!$mode) {
throw new Exception("Unable to get perms of '{$user_config_path}'!");
}
if ($mode & 0177) {
// Mode should allow only owner access.
$prompt = "File permissions on your ~/.arcrc are too open. ".
"Fix them by chmod'ing to 600?";
if (!phutil_console_confirm($prompt, $default_no = false)) {
throw new ArcanistUsageException("Set ~/.arcrc to file mode 600.");
}
execx('chmod 600 %s', $user_config_path);
// Drop the stat cache so we don't read the old permissions if
// we end up here again. If we don't do this, we may prompt the user
// to fix permissions multiple times.
clearstatcache();
}
}
$user_config_data = Filesystem::readFile($user_config_path);
$user_config = json_decode($user_config_data, true);
if (!is_array($user_config)) {
throw new ArcanistUsageException(
"Your '~/.arcrc' file is not a valid JSON file.");
}
}
return $user_config;
}
/**
* This is probably not the method you're looking for; try
* @{method:writeUserArcConfig}.
*/
public function writeUserConfigurationFile($config) {
$json_encoder = new PhutilJSON();
$json = $json_encoder->encodeFormatted($config);
$path = self::getUserConfigurationFileLocation();
Filesystem::writeFile($path, $json);
if (!phutil_is_windows()) {
execx('chmod 600 %s', $path);
}
}
public function getUserConfigurationFileLocation() {
if (phutil_is_windows()) {
return getenv('APPDATA').'/.arcrc';
} else {
return getenv('HOME').'/.arcrc';
}
}
public function readUserArcConfig() {
return idx(self::readUserConfigurationFile(), 'config', array());
}
public function writeUserArcConfig(array $options) {
$config = self::readUserConfigurationFile();
$config['config'] = $options;
self::writeUserConfigurationFile($config);
}
public function getSystemArcConfigLocation() {
if (phutil_is_windows()) {
return Filesystem::resolvePath(
'Phabricator/Arcanist/config',
getenv('ProgramData'));
} else {
return '/etc/arcconfig';
}
}
public function readSystemArcConfig() {
$system_config = array();
$system_config_path = self::getSystemArcConfigLocation();
if (Filesystem::pathExists($system_config_path)) {
$file = Filesystem::readFile($system_config_path);
if ($file) {
$system_config = json_decode($file, true);
}
}
return $system_config;
}
}
diff --git a/src/lint/linter/ArcanistCSSLintLinter.php b/src/lint/linter/ArcanistCSSLintLinter.php
index 3272c47d..4c9620c2 100644
--- a/src/lint/linter/ArcanistCSSLintLinter.php
+++ b/src/lint/linter/ArcanistCSSLintLinter.php
@@ -1,113 +1,113 @@
<?php
/**
* Uses "CSS lint" to detect checkstyle errors in css code.
* To use this linter, you must install CSS lint.
* ##npm install csslint -g## (don't forget the -g flag or NPM will install
* the package locally).
*
* Based on ArcanistPhpcsLinter.php
*
* lint.csslint.options
* lint.csslint.bin
*
* @group linter
*/
final class ArcanistCSSLintLinter extends ArcanistExternalLinter {
public function getLinterName() {
return 'CSSLint';
}
public function getLinterConfigurationName() {
return 'csslint';
}
public function getMandatoryFlags() {
return '--format=lint-xml';
}
public function getDefaultFlags() {
- $working_copy = $this->getEngine()->getWorkingCopy();
+ $config = $this->getEngine()->getConfigurationManager();
- $options = $working_copy->getConfig('lint.csslint.options');
+ $options = $config->getConfigFromAnySource('lint.csslint.options');
// TODO: Deprecation warning.
return $options;
}
public function getDefaultBinary() {
// TODO: Deprecation warning.
- $working_copy = $this->getEngine()->getWorkingCopy();
- $bin = $working_copy->getConfig('lint.csslint.bin');
+ $config = $this->getEngine()->getConfigurationManager();
+ $bin = $config->getConfigFromAnySource('lint.csslint.bin');
if ($bin) {
return $bin;
}
return 'csslint';
}
public function getInstallInstructions() {
return pht('Install CSSLint using `npm install -g csslint`.');
}
protected function parseLinterOutput($path, $err, $stdout, $stderr) {
$report_dom = new DOMDocument();
$ok = @$report_dom->loadXML($stdout);
if (!$ok) {
return false;
}
$files = $report_dom->getElementsByTagName('file');
$messages = array();
foreach ($files as $file) {
foreach ($file->childNodes as $child) {
if (!($child instanceof DOMElement)) {
continue;
}
$data = $this->getData($path);
$lines = explode("\n", $data);
$name = $child->getAttribute('reason');
$severity = ($child->getAttribute('severity') == 'warning')
? ArcanistLintSeverity::SEVERITY_WARNING
: ArcanistLintSeverity::SEVERITY_ERROR;
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($child->getAttribute('line'));
$message->setChar($child->getAttribute('char'));
$message->setCode('CSSLint');
$message->setDescription($child->getAttribute('reason'));
$message->setSeverity($severity);
if ($child->hasAttribute('line')) {
$line = $lines[$child->getAttribute('line') - 1];
$text = substr($line, $child->getAttribute('char') - 1);
$message->setOriginalText($text);
}
$messages[] = $message;
}
}
return $messages;
}
protected function getLintCodeFromLinterConfigurationKey($code) {
// NOTE: We can't figure out which rule generated each message, so we
// can not customize severities. I opened a pull request to add this
// ability; see:
//
// https://github.com/stubbornella/csslint/pull/409
throw new Exception(
pht(
"CSSLint does not currently support custom severity levels, because ".
"rules can't be identified from messages in output. ".
"See Pull Request #409."));
}
}
diff --git a/src/lint/linter/ArcanistCppcheckLinter.php b/src/lint/linter/ArcanistCppcheckLinter.php
index 767cb38a..2f529a3a 100644
--- a/src/lint/linter/ArcanistCppcheckLinter.php
+++ b/src/lint/linter/ArcanistCppcheckLinter.php
@@ -1,126 +1,126 @@
<?php
/**
* Uses cppcheck to do basic checks in a cpp file
*
* You can get it here:
* http://cppcheck.sourceforge.net/
*
* @group linter
*/
final class ArcanistCppcheckLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'cppcheck';
}
public function getLintOptions() {
- $working_copy = $this->getEngine()->getWorkingCopy();
+ $config = $this->getEngine()->getConfigurationManager();
// You will for sure want some options. The below default tends to be ok
- $options = $working_copy->getConfig(
+ $options = $config->getConfigFromAnySource(
'lint.cppcheck.options',
'-j2 --inconclusive --enable=performance,style,portability,information');
return $options;
}
public function getLintPath() {
- $working_copy = $this->getEngine()->getWorkingCopy();
- $prefix = $working_copy->getConfig('lint.cppcheck.prefix');
- $bin = $working_copy->getConfig('lint.cppcheck.bin', 'cppcheck');
+ $config = $this->getEngine()->getConfigurationManager();
+ $prefix = $config->getConfigFromAnySource('lint.cppcheck.prefix');
+ $bin = $config->getConfigFromAnySource('lint.cppcheck.bin', 'cppcheck');
if ($prefix !== null) {
if (!Filesystem::pathExists($prefix.'/'.$bin)) {
throw new ArcanistUsageException(
"Unable to find cppcheck binary in a specified directory. Make ".
"sure that 'lint.cppcheck.prefix' and 'lint.cppcheck.bin' keys are ".
"set correctly. If you'd rather use a copy of cppcheck installed ".
"globally, you can just remove these keys from your .arcconfig.");
}
$bin = csprintf("%s/%s", $prefix, $bin);
return $bin;
}
// Look for globally installed cppcheck
list($err) = exec_manual('which %s', $bin);
if ($err) {
throw new ArcanistUsageException(
"cppcheck does not appear to be installed on this system. Install ".
"it (from http://cppcheck.sourceforge.net/) or configure ".
"'lint.cppcheck.prefix' in your .arcconfig to point to the ".
"directory where it resides."
);
}
return $bin;
}
public function lintPath($path) {
$bin = $this->getLintPath();
$options = $this->getLintOptions();
list($rc, $stdout, $stderr) = exec_manual(
"%C %C --inline-suppr --xml-version=2 -q %s",
$bin,
$options,
$this->getEngine()->getFilePathOnDisk($path));
if ($rc === 1) {
throw new Exception("cppcheck failed to run correctly:\n".$stderr);
}
$dom = new DOMDocument();
libxml_clear_errors();
if ($dom->loadXML($stderr) === false || libxml_get_errors()) {
throw new ArcanistUsageException('cppcheck Linter failed to load ' .
'output. Something happened when running cppcheck. ' .
"Output:\n$stderr" .
"\nTry running lint with --trace flag to get more details.");
}
$errors = $dom->getElementsByTagName('error');
foreach ($errors as $error) {
$loc_node = $error->getElementsByTagName('location');
if (!$loc_node) {
continue;
}
$location = $loc_node->item(0);
if (!$location) {
continue;
}
$file = $location->getAttribute('file');
$line = $location->getAttribute('line');
$id = $error->getAttribute('id');
$severity = $error->getAttribute('severity');
$msg = $error->getAttribute('msg');
$inconclusive = $error->getAttribute('inconclusive');
$verbose_msg = $error->getAttribute('verbose');
$severity_code = ArcanistLintSeverity::SEVERITY_WARNING;
if ($inconclusive) {
$severity_code = ArcanistLintSeverity::SEVERITY_ADVICE;
} else if (stripos($severity, 'error') !== false) {
$severity_code = ArcanistLintSeverity::SEVERITY_ERROR;
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($line);
$message->setCode($severity);
$message->setName($id);
$message->setDescription($msg);
$message->setSeverity($severity_code);
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistCpplintLinter.php b/src/lint/linter/ArcanistCpplintLinter.php
index 24117790..e13d3fcb 100644
--- a/src/lint/linter/ArcanistCpplintLinter.php
+++ b/src/lint/linter/ArcanistCpplintLinter.php
@@ -1,96 +1,96 @@
<?php
/**
* Uses google's cpplint.py to check code.
*
* You can get it here:
* http://google-styleguide.googlecode.com/svn/trunk/cpplint/cpplint.py
* @group linter
*/
final class ArcanistCpplintLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'cpplint.py';
}
public function getLintOptions() {
- $working_copy = $this->getEngine()->getWorkingCopy();
- $options = $working_copy->getConfig('lint.cpplint.options', '');
+ $config = $this->getEngine()->getConfigurationManager();
+ $options = $config->getConfigFromAnySource('lint.cpplint.options', '');
return $options;
}
public function getLintPath() {
- $working_copy = $this->getEngine()->getWorkingCopy();
- $prefix = $working_copy->getConfig('lint.cpplint.prefix');
- $bin = $working_copy->getConfig('lint.cpplint.bin', 'cpplint.py');
+ $config = $this->getEngine()->getConfigurationManager();
+ $prefix = $config->getConfigFromAnySource('lint.cpplint.prefix');
+ $bin = $config->getConfigFromAnySource('lint.cpplint.bin', 'cpplint.py');
if ($prefix !== null) {
if (!Filesystem::pathExists($prefix.'/'.$bin)) {
throw new ArcanistUsageException(
"Unable to find cpplint.py binary in a specified directory. Make ".
"sure that 'lint.cpplint.prefix' and 'lint.cpplint.bin' keys are ".
"set correctly. If you'd rather use a copy of cpplint installed ".
"globally, you can just remove these keys from your .arcconfig.");
}
$bin = csprintf("%s/%s", $prefix, $bin);
return $bin;
}
// Look for globally installed cpplint.py
list($err) = exec_manual('which %s', $bin);
if ($err) {
throw new ArcanistUsageException(
"cpplint.py does not appear to be installed on this system. Install ".
"it (e.g., with 'wget \"http://google-styleguide.googlecode.com/".
"svn/trunk/cpplint/cpplint.py\"') or configure 'lint.cpplint.prefix' ".
"in your .arcconfig to point to the directory where it resides. ".
"Also don't forget to chmod a+x cpplint.py!");
}
return $bin;
}
public function lintPath($path) {
$bin = $this->getLintPath();
$options = $this->getLintOptions();
$f = new ExecFuture("%C %C -", $bin, $options);
$f->write($this->getData($path));
list($err, $stdout, $stderr) = $f->resolve();
if ($err === 2) {
throw new Exception("cpplint failed to run correctly:\n".$stderr);
}
$lines = explode("\n", $stderr);
$messages = array();
foreach ($lines as $line) {
$line = trim($line);
$matches = null;
$regex = '/^-:(\d+):\s*(.*)\s*\[(.*)\] \[(\d+)\]$/';
if (!preg_match($regex, $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[1]);
$message->setCode($matches[3]);
$message->setName($matches[3]);
$message->setDescription($matches[2]);
$message->setSeverity(ArcanistLintSeverity::SEVERITY_WARNING);
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistFlake8Linter.php b/src/lint/linter/ArcanistFlake8Linter.php
index bdcebd75..a7ed986a 100644
--- a/src/lint/linter/ArcanistFlake8Linter.php
+++ b/src/lint/linter/ArcanistFlake8Linter.php
@@ -1,124 +1,124 @@
<?php
/**
* Uses "flake8" to detect various errors in Python code.
* Requires version 1.7.0 or newer of flake8.
*
* @group linter
*/
final class ArcanistFlake8Linter extends ArcanistExternalLinter {
public function getLinterName() {
return 'flake8';
}
public function getLinterConfigurationName() {
return 'flake8';
}
public function getDefaultFlags() {
// TODO: Deprecated.
- $working_copy = $this->getEngine()->getWorkingCopy();
+ $config = $this->getEngine()->getConfigurationManager();
- $options = $working_copy->getConfig('lint.flake8.options', '');
+ $options = $config->getConfigFromAnySource('lint.flake8.options', '');
return $options;
}
public function getDefaultBinary() {
- $working_copy = $this->getEngine()->getWorkingCopy();
- $prefix = $working_copy->getConfig('lint.flake8.prefix');
- $bin = $working_copy->getConfig('lint.flake8.bin', 'flake8');
+ $config = $this->getEngine()->getConfigurationManager();
+ $prefix = $config->getConfigFromAnySource('lint.flake8.prefix');
+ $bin = $config->getConfigFromAnySource('lint.flake8.bin', 'flake8');
if ($prefix || ($bin != 'flake8')) {
return $prefix.'/'.$bin;
}
return 'flake8';
}
public function getInstallInstructions() {
return pht('Install flake8 using `easy_install flake8`.');
}
public function supportsReadDataFromStdin() {
return true;
}
public function getReadDataFromStdinFilename() {
return '-';
}
public function shouldExpectCommandErrors() {
return true;
}
protected function parseLinterOutput($path, $err, $stdout, $stderr) {
$lines = phutil_split_lines($stdout, $retain_endings = false);
$messages = array();
foreach ($lines as $line) {
$matches = null;
// stdin:2: W802 undefined name 'foo' # pyflakes
// stdin:3:1: E302 expected 2 blank lines, found 1 # pep8
$regexp = '/^(.*?):(\d+):(?:(\d+):)? (\S+) (.*)$/';
if (!preg_match($regexp, $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
if (!empty($matches[3])) {
$message->setChar($matches[3]);
}
$message->setCode($matches[4]);
$message->setName($this->getLinterName().' '.$matches[3]);
$message->setDescription($matches[5]);
$message->setSeverity($this->getLintMessageSeverity($matches[4]));
$messages[] = $message;
}
if ($err && !$messages) {
return false;
}
return $messages;
}
protected function getDefaultMessageSeverity($code) {
if (preg_match('/^C/', $code)) {
// "C": Cyclomatic complexity
return ArcanistLintSeverity::SEVERITY_ADVICE;
} else if (preg_match('/^W/', $code)) {
// "W": PEP8 Warning
return ArcanistLintSeverity::SEVERITY_WARNING;
} else {
// "E": PEP8 Error
// "F": PyFlakes Error
return ArcanistLintSeverity::SEVERITY_ERROR;
}
}
protected function getLintCodeFromLinterConfigurationKey($code) {
if (!preg_match('/^(E|W|C|F)\d+$/', $code)) {
throw new Exception(
pht(
'Unrecognized lint message code "%s". Expected a valid flake8 '.
'lint code like "%s", or "%s", or "%s", or "%s".',
$code,
"E225",
"W291",
"F811",
"C901"));
}
return $code;
}
}
diff --git a/src/lint/linter/ArcanistJSHintLinter.php b/src/lint/linter/ArcanistJSHintLinter.php
index 1392b5e9..9f5ddb15 100644
--- a/src/lint/linter/ArcanistJSHintLinter.php
+++ b/src/lint/linter/ArcanistJSHintLinter.php
@@ -1,176 +1,177 @@
<?php
/**
* Uses "JSHint" to detect errors and potential problems in JavaScript code.
* To use this linter, you must install jshint through NPM (Node Package
* Manager). You can configure different JSHint options on a per-file basis.
*
* If you have NodeJS installed you should be able to install jshint with
* ##npm install jshint -g## (don't forget the -g flag or NPM will install
* the package locally). If your system is unusual, you can manually specify
* the location of jshint and its dependencies by configuring these keys in
* your .arcconfig:
*
* lint.jshint.prefix
* lint.jshint.bin
*
* If you want to configure custom options for your project, create a JSON
* file with these options and add the path to the file to your .arcconfig
* by configuring this key:
*
* lint.jshint.config
*
* Example JSON file (config.json):
*
* {
* "predef": [ // Custom globals
* "myGlobalVariable",
* "anotherGlobalVariable"
* ],
*
* "es5": true, // Allow ES5 syntax
* "strict": true // Require strict mode
* }
*
* For more options see http://www.jshint.com/options/.
*
* @group linter
*/
final class ArcanistJSHintLinter extends ArcanistLinter {
const JSHINT_ERROR = 1;
public function getLinterName() {
return 'JSHint';
}
public function getLinterConfigurationName() {
return 'jshint';
}
public function getLintSeverityMap() {
return array(
self::JSHINT_ERROR => ArcanistLintSeverity::SEVERITY_ERROR
);
}
// placeholder if/until we get a map code -> name map
// jshint only offers code -> description right now (parsed as 'reason')
public function getLintMessageName($code) {
return "JSHint".$code;
}
public function getLintNameMap() {
return array(
self::JSHINT_ERROR => "JSHint Error"
);
}
public function getJSHintOptions() {
- $working_copy = $this->getEngine()->getWorkingCopy();
+ $config_manager = $this->getEngine()->getConfigurationManager();
$options = '--reporter '.dirname(realpath(__FILE__)).'/reporter.js';
- $config = $working_copy->getConfig('lint.jshint.config');
+ $config = $config_manager->getConfigFromAnySource('lint.jshint.config');
+ $working_copy = $this->getEngine()->getWorkingCopy();
if ($config !== null) {
$config = Filesystem::resolvePath(
$config,
$working_copy->getProjectRoot());
if (!Filesystem::pathExists($config)) {
throw new ArcanistUsageException(
"Unable to find custom options file defined by ".
"'lint.jshint.config'. Make sure that the path is correct.");
}
$options .= ' --config '.$config;
}
return $options;
}
private function getJSHintPath() {
- $working_copy = $this->getEngine()->getWorkingCopy();
- $prefix = $working_copy->getConfig('lint.jshint.prefix');
- $bin = $working_copy->getConfig('lint.jshint.bin');
+ $config_manager = $this->getEngine()->getConfigurationManager();
+ $prefix = $config_manager->getConfigFromAnySource('lint.jshint.prefix');
+ $bin = $config_manager->getConfigFromAnySource('lint.jshint.bin');
if ($bin === null) {
$bin = "jshint";
}
if ($prefix !== null) {
$bin = $prefix."/".$bin;
if (!Filesystem::pathExists($bin)) {
throw new ArcanistUsageException(
"Unable to find JSHint binary in a specified directory. Make sure ".
"that 'lint.jshint.prefix' and 'lint.jshint.bin' keys are set ".
"correctly. If you'd rather use a copy of JSHint installed ".
"globally, you can just remove these keys from your .arcconfig");
}
return $bin;
}
if (!Filesystem::binaryExists($bin)) {
throw new ArcanistUsageException(
"JSHint does not appear to be installed on this system. Install it ".
"(e.g., with 'npm install jshint -g') or configure ".
"'lint.jshint.prefix' in your .arcconfig to point to the directory ".
"where it resides.");
}
return $bin;
}
public function willLintPaths(array $paths) {
if (!$this->isCodeEnabled(self::JSHINT_ERROR)) {
return;
}
$jshint_bin = $this->getJSHintPath();
$jshint_options = $this->getJSHintOptions();
$futures = array();
foreach ($paths as $path) {
$filepath = $this->getEngine()->getFilePathOnDisk($path);
$futures[$path] = new ExecFuture(
"%s %s %C",
$jshint_bin,
$filepath,
$jshint_options);
}
foreach (Futures($futures)->limit(8) as $path => $future) {
$this->results[$path] = $future->resolve();
}
}
public function lintPath($path) {
if (!$this->isCodeEnabled(self::JSHINT_ERROR)) {
return;
}
list($rc, $stdout, $stderr) = $this->results[$path];
if ($rc === 0) {
return;
}
$errors = json_decode($stdout);
if (!is_array($errors)) {
// Something went wrong and we can't decode the output. Exit abnormally.
throw new ArcanistUsageException(
"JSHint returned unparseable output.\n".
"stdout:\n\n{$stdout}".
"stderr:\n\n{$stderr}");
}
foreach ($errors as $err) {
$this->raiseLintAtLine(
$err->line,
$err->col,
$err->code,
$err->reason);
}
}
}
diff --git a/src/lint/linter/ArcanistLicenseLinter.php b/src/lint/linter/ArcanistLicenseLinter.php
index 536f35a3..1f071ddf 100644
--- a/src/lint/linter/ArcanistLicenseLinter.php
+++ b/src/lint/linter/ArcanistLicenseLinter.php
@@ -1,61 +1,61 @@
<?php
/**
* @deprecated
*/
abstract class ArcanistLicenseLinter extends ArcanistLinter {
const LINT_NO_LICENSE_HEADER = 1;
public function willLintPaths(array $paths) {
return;
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
self::LINT_NO_LICENSE_HEADER => 'No License Header',
);
}
abstract protected function getLicenseText($copyright_holder);
abstract protected function getLicensePatterns();
public function lintPath($path) {
$copyright_holder = $this->getConfig('copyright_holder');
if ($copyright_holder === null) {
- $working_copy = $this->getEngine()->getWorkingCopy();
- $copyright_holder = $working_copy->getConfig('copyright_holder');
+ $config = $this->getEngine()->getConfigurationManager();
+ $copyright_holder = $config->getConfigFromAnySource('copyright_holder');
}
if (!$copyright_holder) {
return;
}
$patterns = $this->getLicensePatterns();
$license = $this->getLicenseText($copyright_holder);
$data = $this->getData($path);
$matches = 0;
foreach ($patterns as $pattern) {
if (preg_match($pattern, $data, $matches)) {
$expect = rtrim(implode('', array_slice($matches, 1)))."\n".$license;
if (trim($matches[0]) != trim($expect)) {
$this->raiseLintAtOffset(
0,
self::LINT_NO_LICENSE_HEADER,
'This file has a missing or out of date license header.',
$matches[0],
ltrim($expect));
}
break;
}
}
}
}
diff --git a/src/lint/linter/ArcanistPEP8Linter.php b/src/lint/linter/ArcanistPEP8Linter.php
index c314145f..5b7c9edb 100644
--- a/src/lint/linter/ArcanistPEP8Linter.php
+++ b/src/lint/linter/ArcanistPEP8Linter.php
@@ -1,129 +1,129 @@
<?php
/**
* Uses "pep8.py" to enforce PEP8 rules for Python.
*
* @group linter
*/
final class ArcanistPEP8Linter extends ArcanistExternalLinter {
public function getLinterName() {
return 'PEP8';
}
public function getLinterConfigurationName() {
return 'pep8';
}
public function getCacheVersion() {
list($stdout) = execx('%C --version', $this->getExecutableCommand());
return $stdout.$this->getCommandFlags();
}
public function getDefaultFlags() {
// TODO: Warn that all of this is deprecated.
- $working_copy = $this->getEngine()->getWorkingCopy();
- $options = $working_copy->getConfig('lint.pep8.options');
+ $config = $this->getEngine()->getConfigurationManager();
+ $options = $config->getConfigFromAnySource('lint.pep8.options');
if ($options === null) {
$options = $this->getConfig('options');
}
return $options;
}
public function shouldUseInterpreter() {
return ($this->getDefaultBinary() !== 'pep8');
}
public function getDefaultInterpreter() {
return 'python2.6';
}
public function getDefaultBinary() {
if (Filesystem::binaryExists('pep8')) {
return 'pep8';
}
- $working_copy = $this->getEngine()->getWorkingCopy();
- $old_prefix = $working_copy->getConfig('lint.pep8.prefix');
- $old_bin = $working_copy->getConfig('lint.pep8.bin');
+ $config = $this->getEngine()->getConfigurationManager();
+ $old_prefix = $config->getConfigFromAnySource('lint.pep8.prefix');
+ $old_bin = $config->getConfigFromAnySource('lint.pep8.bin');
if ($old_prefix || $old_bin) {
// TODO: Deprecation warning.
$old_bin = nonempty($old_bin, 'pep8');
return $old_prefix.'/'.$old_bin;
}
$arc_root = dirname(phutil_get_library_root('arcanist'));
return $arc_root.'/externals/pep8/pep8.py';
}
public function getInstallInstructions() {
return pht('Install PEP8 using `easy_install pep8`.');
}
public function shouldExpectCommandErrors() {
return true;
}
protected function parseLinterOutput($path, $err, $stdout, $stderr) {
$lines = phutil_split_lines($stdout, $retain_endings = false);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match('/^(.*?):(\d+):(\d+): (\S+) (.*)$/', $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setChar($matches[3]);
$message->setCode($matches[4]);
$message->setName('PEP8 '.$matches[4]);
$message->setDescription($matches[5]);
$message->setSeverity($this->getLintMessageSeverity($matches[4]));
$messages[] = $message;
}
if ($err && !$messages) {
return false;
}
return $messages;
}
protected function getDefaultMessageSeverity($code) {
if (preg_match('/^W/', $code)) {
return ArcanistLintSeverity::SEVERITY_WARNING;
} else {
// TODO: Once severities/.arclint are more usable, restore this to
// "ERROR".
// return ArcanistLintSeverity::SEVERITY_ERROR;
return ArcanistLintSeverity::SEVERITY_WARNING;
}
}
protected function getLintCodeFromLinterConfigurationKey($code) {
if (!preg_match('/^(E|W)\d+$/', $code)) {
throw new Exception(
pht(
'Unrecognized lint message code "%s". Expected a valid PEP8 '.
'lint code like "%s" or "%s".',
$code,
"E101",
"W291"));
}
return $code;
}
}
diff --git a/src/lint/linter/ArcanistPhpcsLinter.php b/src/lint/linter/ArcanistPhpcsLinter.php
index ff34a185..acb53b4b 100644
--- a/src/lint/linter/ArcanistPhpcsLinter.php
+++ b/src/lint/linter/ArcanistPhpcsLinter.php
@@ -1,125 +1,125 @@
<?php
/**
* Uses "PHP_CodeSniffer" to detect checkstyle errors in php code.
* To use this linter, you must install PHP_CodeSniffer.
* http://pear.php.net/package/PHP_CodeSniffer.
*
* Optional configurations in .arcconfig:
*
* lint.phpcs.standard
* lint.phpcs.options
* lint.phpcs.bin
*
* @group linter
*/
final class ArcanistPhpcsLinter extends ArcanistExternalLinter {
private $reports;
public function getLinterName() {
return 'PHPCS';
}
public function getLinterConfigurationName() {
return 'phpcs';
}
public function getMandatoryFlags() {
return '--report=xml';
}
public function getInstallInstructions() {
return pht('Install PHPCS with `pear install PHP_CodeSniffer`.');
}
public function getDefaultFlags() {
// TODO: Deprecation warnings.
- $working_copy = $this->getEngine()->getWorkingCopy();
+ $config = $this->getEngine()->getConfigurationManager();
- $options = $working_copy->getConfig('lint.phpcs.options');
+ $options = $config->getConfigFromAnySource('lint.phpcs.options');
- $standard = $working_copy->getConfig('lint.phpcs.standard');
+ $standard = $config->getConfigFromAnySource('lint.phpcs.standard');
$options .= !empty($standard) ? ' --standard=' . $standard : '';
return $options;
}
public function getDefaultBinary() {
// TODO: Deprecation warnings.
- $working_copy = $this->getEngine()->getWorkingCopy();
- $bin = $working_copy->getConfig('lint.phpcs.bin');
+ $config = $this->getEngine()->getConfigurationManager();
+ $bin = $config->getConfigFromAnySource('lint.phpcs.bin');
if ($bin) {
return $bin;
}
return 'phpcs';
}
public function shouldExpectCommandErrors() {
return true;
}
protected function parseLinterOutput($path, $err, $stdout, $stderr) {
// NOTE: Some version of PHPCS after 1.4.6 stopped printing a valid, empty
// XML document to stdout in the case of no errors. If PHPCS exits with
// error 0, just ignore output.
if (!$err) {
return array();
}
$report_dom = new DOMDocument();
$ok = @$report_dom->loadXML($stdout);
if (!$ok) {
return false;
}
$files = $report_dom->getElementsByTagName('file');
$messages = array();
foreach ($files as $file) {
foreach ($file->childNodes as $child) {
if (!($child instanceof DOMElement)) {
continue;
}
if ($child->tagName == 'error') {
$prefix = 'E';
} else {
$prefix = 'W';
}
$code = 'PHPCS.'.$prefix.'.'.$child->getAttribute('source');
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($child->getAttribute('line'));
$message->setChar($child->getAttribute('column'));
$message->setCode($code);
$message->setDescription($child->nodeValue);
$message->setSeverity($this->getLintMessageSeverity($code));
$messages[] = $message;
}
}
return $messages;
}
protected function getDefaultMessageSeverity($code) {
if (preg_match('/^PHPCS\\.W\\./', $code)) {
return ArcanistLintSeverity::SEVERITY_WARNING;
} else {
return ArcanistLintSeverity::SEVERITY_ERROR;
}
}
protected function getLintCodeFromLinterConfigurationKey($code) {
if (!preg_match('/^PHPCS\\.(E|W)\\./', $code)) {
throw new Exception(
"Invalid severity code '{$code}', should begin with 'PHPCS.'.");
}
return $code;
}
}
diff --git a/src/lint/linter/ArcanistPyFlakesLinter.php b/src/lint/linter/ArcanistPyFlakesLinter.php
index 99c867a1..da6a10b2 100644
--- a/src/lint/linter/ArcanistPyFlakesLinter.php
+++ b/src/lint/linter/ArcanistPyFlakesLinter.php
@@ -1,112 +1,112 @@
<?php
/**
* Uses "PyFlakes" to detect various errors in Python code.
*
* @group linter
*/
final class ArcanistPyFlakesLinter extends ArcanistLinter {
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'PyFlakes';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array(
);
}
public function getPyFlakesOptions() {
return null;
}
public function lintPath($path) {
- $working_copy = $this->getEngine()->getWorkingCopy();
- $pyflakes_path = $working_copy->getConfig('lint.pyflakes.path');
- $pyflakes_prefix = $working_copy->getConfig('lint.pyflakes.prefix');
+ $config = $this->getEngine()->getConfigurationManager();
+ $pyflakes_path = $config->getConfigFromAnySource('lint.pyflakes.path');
+ $pyflakes_prefix = $config->getConfigFromAnySource('lint.pyflakes.prefix');
// Default to just finding pyflakes in the users path
$pyflakes_bin = 'pyflakes';
$python_path = array();
// If a pyflakes path was specified, then just use that as the
// pyflakes binary and assume that the libraries will be imported
// correctly.
//
// If no pyflakes path was specified and a pyflakes prefix was
// specified, then use the binary from this prefix and add it to
// the PYTHONPATH environment variable so that the libs are imported
// correctly. This is useful when pyflakes is installed into a
// non-default location.
if ($pyflakes_path !== null) {
$pyflakes_bin = $pyflakes_path;
} else if ($pyflakes_prefix !== null) {
$pyflakes_bin = $pyflakes_prefix.'/bin/pyflakes';
$python_path[] = $pyflakes_prefix.'/lib/python2.7/site-packages';
$python_path[] = $pyflakes_prefix.'/lib/python2.7/dist-packages';
$python_path[] = $pyflakes_prefix.'/lib/python2.6/site-packages';
$python_path[] = $pyflakes_prefix.'/lib/python2.6/dist-packages';
}
$python_path[] = '';
$python_path = implode(':', $python_path);
$options = $this->getPyFlakesOptions();
$f = new ExecFuture(
'/usr/bin/env PYTHONPATH=%s$PYTHONPATH %s %C',
$python_path,
$pyflakes_bin,
$options);
$f->write($this->getData($path));
try {
list($stdout, $_) = $f->resolvex();
} catch (CommandException $e) {
// PyFlakes will return an exit code of 1 if warnings/errors
// are found but print nothing to stderr in this case. Therefore,
// if we see any output on stderr or a return code other than 1 or 0,
// pyflakes failed.
if ($e->getError() !== 1 || $e->getStderr() !== '') {
throw $e;
} else {
$stdout = $e->getStdout();
}
}
$lines = explode("\n", $stdout);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match('/^(.*?):(\d+): (.*)$/', $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$severity = ArcanistLintSeverity::SEVERITY_WARNING;
$description = $matches[3];
$error_regexp = '/(^undefined|^duplicate|before assignment$)/';
if (preg_match($error_regexp, $description)) {
$severity = ArcanistLintSeverity::SEVERITY_ERROR;
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setCode($this->getLinterName());
$message->setDescription($description);
$message->setSeverity($severity);
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistPyLintLinter.php b/src/lint/linter/ArcanistPyLintLinter.php
index ad885fb0..6bdca57f 100644
--- a/src/lint/linter/ArcanistPyLintLinter.php
+++ b/src/lint/linter/ArcanistPyLintLinter.php
@@ -1,250 +1,255 @@
<?php
/**
* Uses "PyLint" to detect various errors in Python code. To use this linter,
* you must install pylint and configure which codes you want to be reported as
* errors, warnings and advice.
*
* You should be able to install pylint with ##sudo easy_install pylint##. If
* your system is unusual, you can manually specify the location of pylint and
* its dependencies by configuring these keys in your .arcconfig:
*
* lint.pylint.prefix
* lint.pylint.logilab_astng.prefix
* lint.pylint.logilab_common.prefix
*
* You can specify additional command-line options to pass to PyLint by
* setting ##lint.pylint.options##. You may also specify a list of additional
* entries for PYTHONPATH with ##lint.pylint.pythonpath##. Those can be
* absolute or relative to the project root.
*
* If you have a PyLint rcfile, specify its path with
* ##lint.pylint.rcfile##. It can be absolute or relative to the project
* root. Be sure not to define ##output-format##, or if you do, set it to
* ##text##.
*
* Specify which PyLint messages map to which Arcanist messages by defining
* the following regular expressions:
*
* lint.pylint.codes.error
* lint.pylint.codes.warning
* lint.pylint.codes.advice
*
* The regexps are run in that order; the first to match determines which
* Arcanist severity applies, if any. For example, to capture all PyLint
* "E...." errors as Arcanist errors, set ##lint.pylint.codes.error## to:
*
* ^E.*
*
* You can also match more granularly:
*
* ^E(0001|0002)$
*
* According to ##man pylint##, there are 5 kind of messages:
*
* (C) convention, for programming standard violation
* (R) refactor, for bad code smell
* (W) warning, for python specific problems
* (E) error, for probable bugs in the code
* (F) fatal, if an error occurred which prevented pylint from
* doing further processing.
*
* @group linter
*/
final class ArcanistPyLintLinter extends ArcanistLinter {
private function getMessageCodeSeverity($code) {
- $working_copy = $this->getEngine()->getWorkingCopy();
+ $config = $this->getEngine()->getConfigurationManager();
- $error_regexp = $working_copy->getConfig('lint.pylint.codes.error');
- $warning_regexp = $working_copy->getConfig('lint.pylint.codes.warning');
- $advice_regexp = $working_copy->getConfig('lint.pylint.codes.advice');
+ $error_regexp =
+ $config->getConfigFromAnySource('lint.pylint.codes.error');
+ $warning_regexp =
+ $config->getConfigFromAnySource('lint.pylint.codes.warning');
+ $advice_regexp =
+ $config->getConfigFromAnySource('lint.pylint.codes.advice');
if (!$error_regexp && !$warning_regexp && !$advice_regexp) {
throw new ArcanistUsageException(
"You are invoking the PyLint linter but have not configured any of ".
"'lint.pylint.codes.error', 'lint.pylint.codes.warning', or ".
"'lint.pylint.codes.advice'. Consult the documentation for ".
"ArcanistPyLintLinter.");
}
$code_map = array(
ArcanistLintSeverity::SEVERITY_ERROR => $error_regexp,
ArcanistLintSeverity::SEVERITY_WARNING => $warning_regexp,
ArcanistLintSeverity::SEVERITY_ADVICE => $advice_regexp,
);
foreach ($code_map as $sev => $codes) {
if ($codes === null) {
continue;
}
if (!is_array($codes)) {
$codes = array($codes);
}
foreach ($codes as $code_re) {
if (preg_match("/{$code_re}/", $code)) {
return $sev;
}
}
}
// If the message code doesn't match any of the provided regex's,
// then just disable it.
return ArcanistLintSeverity::SEVERITY_DISABLED;
}
private function getPyLintPath() {
$pylint_bin = "pylint";
// Use the PyLint prefix specified in the config file
- $working_copy = $this->getEngine()->getWorkingCopy();
- $prefix = $working_copy->getConfig('lint.pylint.prefix');
+ $config = $this->getEngine()->getConfigurationManager();
+ $prefix = $config->getConfigFromAnySource('lint.pylint.prefix');
if ($prefix !== null) {
$pylint_bin = $prefix."/bin/".$pylint_bin;
}
if (!Filesystem::pathExists($pylint_bin)) {
list($err) = exec_manual('which %s', $pylint_bin);
if ($err) {
throw new ArcanistUsageException(
"PyLint does not appear to be installed on this system. Install it ".
"(e.g., with 'sudo easy_install pylint') or configure ".
"'lint.pylint.prefix' in your .arcconfig to point to the directory ".
"where it resides.");
}
}
return $pylint_bin;
}
private function getPyLintPythonPath() {
// Get non-default install locations for pylint and its dependencies
// libraries.
- $working_copy = $this->getEngine()->getWorkingCopy();
+ $config = $this->getEngine()->getConfigurationManager();
$prefixes = array(
- $working_copy->getConfig('lint.pylint.prefix'),
- $working_copy->getConfig('lint.pylint.logilab_astng.prefix'),
- $working_copy->getConfig('lint.pylint.logilab_common.prefix'),
+ $config->getConfigFromAnySource('lint.pylint.prefix'),
+ $config->getConfigFromAnySource('lint.pylint.logilab_astng.prefix'),
+ $config->getConfigFromAnySource('lint.pylint.logilab_common.prefix'),
);
// Add the libraries to the python search path
$python_path = array();
foreach ($prefixes as $prefix) {
if ($prefix !== null) {
$python_path[] = $prefix.'/lib/python2.7/site-packages';
$python_path[] = $prefix.'/lib/python2.7/dist-packages';
$python_path[] = $prefix.'/lib/python2.6/site-packages';
$python_path[] = $prefix.'/lib/python2.6/dist-packages';
}
}
- $config_paths = $working_copy->getConfig('lint.pylint.pythonpath');
+ $working_copy = $this->getEngine()->getWorkingCopy();
+ $config_paths = $config->getConfigFromAnySource('lint.pylint.pythonpath');
if ($config_paths !== null) {
foreach ($config_paths as $config_path) {
if ($config_path !== null) {
$python_path[] =
Filesystem::resolvePath($config_path,
$working_copy->getProjectRoot());
}
}
}
$python_path[] = '';
return implode(":", $python_path);
}
private function getPyLintOptions() {
// '-rn': don't print lint report/summary at end
// '-iy': show message codes for lint warnings/errors
$options = array('-rn', '-iy');
$working_copy = $this->getEngine()->getWorkingCopy();
+ $config = $this->getEngine()->getConfigurationManager();
// Specify an --rcfile, either absolute or relative to the project root.
// Stupidly, the command line args above are overridden by rcfile, so be
// careful.
- $rcfile = $working_copy->getConfig('lint.pylint.rcfile');
+ $rcfile = $config->getConfigFromAnySource('lint.pylint.rcfile');
if ($rcfile !== null) {
$rcfile = Filesystem::resolvePath(
$rcfile,
$working_copy->getProjectRoot());
$options[] = csprintf('--rcfile=%s', $rcfile);
}
// Add any options defined in the config file for PyLint
- $config_options = $working_copy->getConfig('lint.pylint.options');
+ $config_options = $config->getConfigFromAnySource('lint.pylint.options');
if ($config_options !== null) {
$options = array_merge($options, $config_options);
}
return implode(" ", $options);
}
public function willLintPaths(array $paths) {
return;
}
public function getLinterName() {
return 'PyLint';
}
public function getLintSeverityMap() {
return array();
}
public function getLintNameMap() {
return array();
}
public function lintPath($path) {
$pylint_bin = $this->getPyLintPath();
$python_path = $this->getPyLintPythonPath();
$options = $this->getPyLintOptions();
$path_on_disk = $this->getEngine()->getFilePathOnDisk($path);
try {
list($stdout, $_) = execx(
'/usr/bin/env PYTHONPATH=%s$PYTHONPATH %s %C %s',
$python_path,
$pylint_bin,
$options,
$path_on_disk);
} catch (CommandException $e) {
if ($e->getError() == 32) {
// According to ##man pylint## the exit status of 32 means there was a
// usage error. That's bad, so actually exit abnormally.
throw $e;
} else {
// The other non-zero exit codes mean there were messages issued,
// which is expected, so don't exit.
$stdout = $e->getStdout();
}
}
$lines = explode("\n", $stdout);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match(
'/([A-Z]\d+): *(\d+)(?:|,\d*): *(.*)$/',
$line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setCode($matches[1]);
$message->setName($this->getLinterName()." ".$matches[1]);
$message->setDescription($matches[3]);
$message->setSeverity($this->getMessageCodeSeverity($matches[1]));
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistRubyLinter.php b/src/lint/linter/ArcanistRubyLinter.php
index 24a9ec7c..08619c79 100644
--- a/src/lint/linter/ArcanistRubyLinter.php
+++ b/src/lint/linter/ArcanistRubyLinter.php
@@ -1,86 +1,86 @@
<?php
/**
* Uses `ruby` to detect various errors in Ruby code.
*
* @group linter
*/
final class ArcanistRubyLinter extends ArcanistExternalLinter {
public function getLinterName() {
return 'RUBY';
}
public function getLinterConfigurationName() {
return 'ruby';
}
public function getDefaultBinary() {
// TODO: Deprecation warning.
- $working_copy = $this->getEngine()->getWorkingCopy();
- $prefix = $working_copy->getConfig('lint.ruby.prefix');
+ $config = $this->getEngine()->getConfigurationManager();
+ $prefix = $config->getConfigFromAnySource('lint.ruby.prefix');
if ($prefix !== null) {
$ruby_bin = $prefix.'ruby';
}
return 'ruby';
}
public function getInstallInstructions() {
return pht('Install `ruby` from <http://www.ruby-lang.org/>.');
}
public function supportsReadDataFromStdin() {
return true;
}
public function shouldExpectCommandErrors() {
return true;
}
protected function getMandatoryFlags() {
// -w: turn on warnings
// -c: check syntax
return '-w -c';
}
protected function getDefaultMessageSeverity($code) {
return ArcanistLintSeverity::SEVERITY_ERROR;
}
protected function parseLinterOutput($path, $err, $stdout, $stderr) {
$lines = phutil_split_lines($stderr, $retain_endings = false);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match("/(.*?):(\d+): (.*?)$/", $line, $matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$code = head(explode(',', $matches[3]));
$message = new ArcanistLintMessage();
$message->setPath($path);
$message->setLine($matches[2]);
$message->setCode($this->getLinterName());
$message->setName(pht('Syntax Error'));
$message->setDescription($matches[3]);
$message->setSeverity($this->getLintMessageSeverity($code));
$messages[] = $message;
}
if ($err && !$messages) {
return false;
}
return $messages;
}
}
diff --git a/src/lint/linter/ArcanistScalaSBTLinter.php b/src/lint/linter/ArcanistScalaSBTLinter.php
index 10aabbb2..4963373a 100644
--- a/src/lint/linter/ArcanistScalaSBTLinter.php
+++ b/src/lint/linter/ArcanistScalaSBTLinter.php
@@ -1,91 +1,91 @@
<?php
/**
* Uses `sbt compile` to detect various warnings/errors in Scala code.
*
* @group linter
*/
final class ArcanistScalaSBTLinter extends ArcanistLinter {
public function getLinterName() {
return 'ScalaSBT';
}
public function canRun() {
// Check if this looks like a SBT project. If it doesn't, throw, because
// we rely fairly heavily on `sbt compile` working, below. We don't want
// to call out to scalac ourselves, because then we'll end up in Class Path
// Hell. We let the build system handle this for us.
if (!Filesystem::pathExists('project/Build.scala') &&
!Filesystem::pathExists('build.sbt')) {
return false;
}
return true;
}
private function getSBTPath() {
$sbt_bin = "sbt";
// Use the SBT prefix specified in the config file
- $working_copy = $this->getEngine()->getWorkingCopy();
- $prefix = $working_copy->getConfig('lint.scala_sbt.prefix');
+ $config = $this->getEngine()->getConfigurationManager();
+ $prefix = $config->getConfigFromAnySource('lint.scala_sbt.prefix');
if ($prefix !== null) {
$sbt_bin = $prefix . $sbt_bin;
}
if (!Filesystem::pathExists($sbt_bin)) {
list($err) = exec_manual('which %s', $sbt_bin);
if ($err) {
throw new ArcanistUsageException(
"SBT does not appear to be installed on this system. Install it or ".
"add 'lint.scala_sbt.prefix' in your .arcconfig to point to ".
"the directory where it resides.");
}
}
return $sbt_bin;
}
private function getMessageCodeSeverity($type_of_error) {
switch ($type_of_error) {
case 'warn':
return ArcanistLintSeverity::SEVERITY_WARNING;
case 'error':
return ArcanistLintSeverity::SEVERITY_ERROR;
}
}
public function lintPath($path) {
$sbt = $this->getSBTPath();
// Tell SBT to not use color codes so our regex life is easy.
// TODO: Should this be "clean compile" instead of "compile"?
$f = new ExecFuture("%s -Dsbt.log.noformat=true compile", $sbt);
list($err, $stdout, $stderr) = $f->resolve();
$lines = explode("\n", $stdout);
$messages = array();
foreach ($lines as $line) {
$matches = null;
if (!preg_match(
"/\[(warn|error)\] (.*?):(\d+): (.*?)$/",
$line,
$matches)) {
continue;
}
foreach ($matches as $key => $match) {
$matches[$key] = trim($match);
}
$message = new ArcanistLintMessage();
$message->setPath($matches[2]);
$message->setLine($matches[3]);
$message->setCode($this->getLinterName());
$message->setDescription($matches[4]);
$message->setSeverity($this->getMessageCodeSeverity($matches[1]));
$this->addLintMessage($message);
}
}
}
diff --git a/src/lint/linter/ArcanistXHPASTLinter.php b/src/lint/linter/ArcanistXHPASTLinter.php
index 556f33ee..8f11bb99 100644
--- a/src/lint/linter/ArcanistXHPASTLinter.php
+++ b/src/lint/linter/ArcanistXHPASTLinter.php
@@ -1,2341 +1,2341 @@
<?php
/**
* Uses XHPAST to apply lint rules to PHP.
*
* @group linter
*/
final class ArcanistXHPASTLinter extends ArcanistBaseXHPASTLinter {
private $futures = array();
private $trees = array();
const LINT_PHP_SYNTAX_ERROR = 1;
const LINT_UNABLE_TO_PARSE = 2;
const LINT_VARIABLE_VARIABLE = 3;
const LINT_EXTRACT_USE = 4;
const LINT_UNDECLARED_VARIABLE = 5;
const LINT_PHP_SHORT_TAG = 6;
const LINT_PHP_ECHO_TAG = 7;
const LINT_PHP_CLOSE_TAG = 8;
const LINT_NAMING_CONVENTIONS = 9;
const LINT_IMPLICIT_CONSTRUCTOR = 10;
const LINT_DYNAMIC_DEFINE = 12;
const LINT_STATIC_THIS = 13;
const LINT_PREG_QUOTE_MISUSE = 14;
const LINT_PHP_OPEN_TAG = 15;
const LINT_TODO_COMMENT = 16;
const LINT_EXIT_EXPRESSION = 17;
const LINT_COMMENT_STYLE = 18;
const LINT_CLASS_FILENAME_MISMATCH = 19;
const LINT_TAUTOLOGICAL_EXPRESSION = 20;
const LINT_PLUS_OPERATOR_ON_STRINGS = 21;
const LINT_DUPLICATE_KEYS_IN_ARRAY = 22;
const LINT_REUSED_ITERATORS = 23;
const LINT_BRACE_FORMATTING = 24;
const LINT_PARENTHESES_SPACING = 25;
const LINT_CONTROL_STATEMENT_SPACING = 26;
const LINT_BINARY_EXPRESSION_SPACING = 27;
const LINT_ARRAY_INDEX_SPACING = 28;
const LINT_RAGGED_CLASSTREE_EDGE = 29;
const LINT_IMPLICIT_FALLTHROUGH = 30;
const LINT_PHP_53_FEATURES = 31;
const LINT_REUSED_AS_ITERATOR = 32;
const LINT_COMMENT_SPACING = 34;
const LINT_PHP_54_FEATURES = 35;
const LINT_SLOWNESS = 36;
const LINT_CLOSING_CALL_PAREN = 37;
const LINT_CLOSING_DECL_PAREN = 38;
const LINT_REUSED_ITERATOR_REFERENCE = 39;
const LINT_KEYWORD_CASING = 40;
public function getLintNameMap() {
return array(
self::LINT_PHP_SYNTAX_ERROR => 'PHP Syntax Error!',
self::LINT_UNABLE_TO_PARSE => 'Unable to Parse',
self::LINT_VARIABLE_VARIABLE => 'Use of Variable Variable',
self::LINT_EXTRACT_USE => 'Use of extract()',
self::LINT_UNDECLARED_VARIABLE => 'Use of Undeclared Variable',
self::LINT_PHP_SHORT_TAG => 'Use of Short Tag "<?"',
self::LINT_PHP_ECHO_TAG => 'Use of Echo Tag "<?="',
self::LINT_PHP_CLOSE_TAG => 'Use of Close Tag "?>"',
self::LINT_NAMING_CONVENTIONS => 'Naming Conventions',
self::LINT_IMPLICIT_CONSTRUCTOR => 'Implicit Constructor',
self::LINT_DYNAMIC_DEFINE => 'Dynamic define()',
self::LINT_STATIC_THIS => 'Use of $this in Static Context',
self::LINT_PREG_QUOTE_MISUSE => 'Misuse of preg_quote()',
self::LINT_PHP_OPEN_TAG => 'Expected Open Tag',
self::LINT_TODO_COMMENT => 'TODO Comment',
self::LINT_EXIT_EXPRESSION => 'Exit Used as Expression',
self::LINT_COMMENT_STYLE => 'Comment Style',
self::LINT_CLASS_FILENAME_MISMATCH => 'Class-Filename Mismatch',
self::LINT_TAUTOLOGICAL_EXPRESSION => 'Tautological Expression',
self::LINT_PLUS_OPERATOR_ON_STRINGS => 'Not String Concatenation',
self::LINT_DUPLICATE_KEYS_IN_ARRAY => 'Duplicate Keys in Array',
self::LINT_REUSED_ITERATORS => 'Reuse of Iterator Variable',
self::LINT_BRACE_FORMATTING => 'Brace placement',
self::LINT_PARENTHESES_SPACING => 'Spaces Inside Parentheses',
self::LINT_CONTROL_STATEMENT_SPACING => 'Space After Control Statement',
self::LINT_BINARY_EXPRESSION_SPACING => 'Space Around Binary Operator',
self::LINT_ARRAY_INDEX_SPACING => 'Spacing Before Array Index',
self::LINT_RAGGED_CLASSTREE_EDGE => 'Class Not abstract Or final',
self::LINT_IMPLICIT_FALLTHROUGH => 'Implicit Fallthrough',
self::LINT_PHP_53_FEATURES => 'Use Of PHP 5.3 Features',
self::LINT_PHP_54_FEATURES => 'Use Of PHP 5.4 Features',
self::LINT_REUSED_AS_ITERATOR => 'Variable Reused As Iterator',
self::LINT_COMMENT_SPACING => 'Comment Spaces',
self::LINT_SLOWNESS => 'Slow Construct',
self::LINT_CLOSING_CALL_PAREN => 'Call Formatting',
self::LINT_CLOSING_DECL_PAREN => 'Declaration Formatting',
self::LINT_REUSED_ITERATOR_REFERENCE => 'Reuse of Iterator References',
self::LINT_KEYWORD_CASING => 'Keyword Conventions',
);
}
public function getLinterName() {
return 'XHP';
}
public function getLintSeverityMap() {
$disabled = ArcanistLintSeverity::SEVERITY_DISABLED;
$advice = ArcanistLintSeverity::SEVERITY_ADVICE;
$warning = ArcanistLintSeverity::SEVERITY_WARNING;
return array(
self::LINT_TODO_COMMENT => $disabled,
self::LINT_UNABLE_TO_PARSE => $warning,
self::LINT_NAMING_CONVENTIONS => $warning,
self::LINT_PREG_QUOTE_MISUSE => $advice,
self::LINT_BRACE_FORMATTING => $warning,
self::LINT_PARENTHESES_SPACING => $warning,
self::LINT_CONTROL_STATEMENT_SPACING => $warning,
self::LINT_BINARY_EXPRESSION_SPACING => $warning,
self::LINT_ARRAY_INDEX_SPACING => $warning,
self::LINT_IMPLICIT_FALLTHROUGH => $warning,
self::LINT_SLOWNESS => $warning,
self::LINT_COMMENT_SPACING => $advice,
self::LINT_CLOSING_CALL_PAREN => $warning,
self::LINT_CLOSING_DECL_PAREN => $warning,
self::LINT_REUSED_ITERATOR_REFERENCE => $warning,
self::LINT_KEYWORD_CASING => $warning,
// This is disabled by default because it implies a very strict policy
// which isn't necessary in the general case.
self::LINT_RAGGED_CLASSTREE_EDGE => $disabled,
// This is disabled by default because projects don't necessarily target
// a specific minimum version.
self::LINT_PHP_53_FEATURES => $disabled,
self::LINT_PHP_54_FEATURES => $disabled,
);
}
protected function buildFutures(array $paths) {
foreach ($paths as $path) {
if (!isset($this->futures[$path])) {
$this->futures[$path] = xhpast_get_parser_future($this->getData($path));
}
}
return array_select_keys($this->futures, $paths);
}
public function getXHPASTTreeForPath($path) {
if (!array_key_exists($path, $this->trees)) {
$this->trees[$path] = null;
try {
$this->trees[$path] = XHPASTTree::newFromDataAndResolvedExecFuture(
$this->getData($path),
$this->futures[$path]->resolve());
$root = $this->trees[$path]->getRootNode();
$root->buildSelectCache();
$root->buildTokenCache();
} catch (XHPASTSyntaxErrorException $ex) {
$this->raiseLintAtLine(
$ex->getErrorLine(),
1,
self::LINT_PHP_SYNTAX_ERROR,
'This file contains a syntax error: '.$ex->getMessage());
} catch (Exception $ex) {
$this->raiseLintAtPath(self::LINT_UNABLE_TO_PARSE, $ex->getMessage());
}
}
return $this->trees[$path];
}
public function getCacheVersion() {
$version = '4';
$path = xhpast_get_binary_path();
if (Filesystem::pathExists($path)) {
$version .= '-'.md5_file($path);
}
return $version;
}
protected function resolveFuture($path, Future $future) {
$tree = $this->getXHPASTTreeForPath($path);
if (!$tree) {
return;
}
$root = $tree->getRootNode();
$method_codes = array(
'lintStrstrUsedForCheck' => self::LINT_SLOWNESS,
'lintStrposUsedForStart' => self::LINT_SLOWNESS,
'lintPHP53Features' => self::LINT_PHP_53_FEATURES,
'lintPHP54Features' => self::LINT_PHP_54_FEATURES,
'lintImplicitFallthrough' => self::LINT_IMPLICIT_FALLTHROUGH,
'lintBraceFormatting' => self::LINT_BRACE_FORMATTING,
'lintTautologicalExpressions' => self::LINT_TAUTOLOGICAL_EXPRESSION,
'lintCommentSpaces' => self::LINT_COMMENT_SPACING,
'lintHashComments' => self::LINT_COMMENT_STYLE,
'lintReusedIterators' => self::LINT_REUSED_ITERATORS,
'lintReusedIteratorReferences' => self::LINT_REUSED_ITERATOR_REFERENCE,
'lintVariableVariables' => self::LINT_VARIABLE_VARIABLE,
'lintUndeclaredVariables' => array(
self::LINT_EXTRACT_USE,
self::LINT_REUSED_AS_ITERATOR,
self::LINT_UNDECLARED_VARIABLE,
),
'lintPHPTagUse' => array(
self::LINT_PHP_SHORT_TAG,
self::LINT_PHP_ECHO_TAG,
self::LINT_PHP_OPEN_TAG,
self::LINT_PHP_CLOSE_TAG,
),
'lintNamingConventions' => self::LINT_NAMING_CONVENTIONS,
'lintSurpriseConstructors' => self::LINT_IMPLICIT_CONSTRUCTOR,
'lintParenthesesShouldHugExpressions' => self::LINT_PARENTHESES_SPACING,
'lintSpaceAfterControlStatementKeywords' =>
self::LINT_CONTROL_STATEMENT_SPACING,
'lintSpaceAroundBinaryOperators' => self::LINT_BINARY_EXPRESSION_SPACING,
'lintDynamicDefines' => self::LINT_DYNAMIC_DEFINE,
'lintUseOfThisInStaticMethods' => self::LINT_STATIC_THIS,
'lintPregQuote' => self::LINT_PREG_QUOTE_MISUSE,
'lintExitExpressions' => self::LINT_EXIT_EXPRESSION,
'lintArrayIndexWhitespace' => self::LINT_ARRAY_INDEX_SPACING,
'lintTODOComments' => self::LINT_TODO_COMMENT,
'lintPrimaryDeclarationFilenameMatch' =>
self::LINT_CLASS_FILENAME_MISMATCH,
'lintPlusOperatorOnStrings' => self::LINT_PLUS_OPERATOR_ON_STRINGS,
'lintDuplicateKeysInArray' => self::LINT_DUPLICATE_KEYS_IN_ARRAY,
'lintRaggedClasstreeEdges' => self::LINT_RAGGED_CLASSTREE_EDGE,
'lintClosingCallParen' => self::LINT_CLOSING_CALL_PAREN,
'lintClosingDeclarationParen' => self::LINT_CLOSING_DECL_PAREN,
'lintKeywordCasing' => self::LINT_KEYWORD_CASING,
);
foreach ($method_codes as $method => $codes) {
foreach ((array)$codes as $code) {
if ($this->isCodeEnabled($code)) {
call_user_func(array($this, $method), $root);
break;
}
}
}
}
public function lintStrstrUsedForCheck($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
$operator = $operator->getConcreteString();
if ($operator != '===' && $operator != '!==') {
continue;
}
$false = $expression->getChildByIndex(0);
if ($false->getTypeName() == 'n_SYMBOL_NAME' &&
$false->getConcreteString() == 'false') {
$strstr = $expression->getChildByIndex(2);
} else {
$strstr = $false;
$false = $expression->getChildByIndex(2);
if ($false->getTypeName() != 'n_SYMBOL_NAME' ||
$false->getConcreteString() != 'false') {
continue;
}
}
if ($strstr->getTypeName() != 'n_FUNCTION_CALL') {
continue;
}
$name = strtolower($strstr->getChildByIndex(0)->getConcreteString());
if ($name == 'strstr' || $name == 'strchr') {
$this->raiseLintAtNode(
$strstr,
self::LINT_SLOWNESS,
"Use strpos() for checking if the string contains something.");
} else if ($name == 'stristr') {
$this->raiseLintAtNode(
$strstr,
self::LINT_SLOWNESS,
"Use stripos() for checking if the string contains something.");
}
}
}
public function lintStrposUsedForStart($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildOfType(1, 'n_OPERATOR');
$operator = $operator->getConcreteString();
if ($operator != '===' && $operator != '!==') {
continue;
}
$zero = $expression->getChildByIndex(0);
if ($zero->getTypeName() == 'n_NUMERIC_SCALAR' &&
$zero->getConcreteString() == '0') {
$strpos = $expression->getChildByIndex(2);
} else {
$strpos = $zero;
$zero = $expression->getChildByIndex(2);
if ($zero->getTypeName() != 'n_NUMERIC_SCALAR' ||
$zero->getConcreteString() != '0') {
continue;
}
}
if ($strpos->getTypeName() != 'n_FUNCTION_CALL') {
continue;
}
$name = strtolower($strpos->getChildByIndex(0)->getConcreteString());
if ($name == 'strpos') {
$this->raiseLintAtNode(
$strpos,
self::LINT_SLOWNESS,
"Use strncmp() for checking if the string starts with something.");
} else if ($name == 'stripos') {
$this->raiseLintAtNode(
$strpos,
self::LINT_SLOWNESS,
"Use strncasecmp() for checking if the string starts with ".
"something.");
}
}
}
public function lintPHP53Features($root) {
$functions = $root->selectTokensOfType('T_FUNCTION');
foreach ($functions as $function) {
$next = $function->getNextToken();
while ($next) {
if ($next->isSemantic()) {
break;
}
$next = $next->getNextToken();
}
if ($next) {
if ($next->getTypeName() == '(') {
$this->raiseLintAtToken(
$function,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but anonymous functions were '.
'not introduced until PHP 5.3.');
}
}
}
$namespaces = $root->selectTokensOfType('T_NAMESPACE');
foreach ($namespaces as $namespace) {
$this->raiseLintAtToken(
$namespace,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but namespaces were not introduced '.
'until PHP 5.3.');
}
// NOTE: This is only "use x;", in anonymous functions the node type is
// n_LEXICAL_VARIABLE_LIST even though both tokens are T_USE.
// TODO: We parse n_USE in a slightly crazy way right now; that would be
// a better selector once it's fixed.
$uses = $root->selectDescendantsOfType('n_USE_LIST');
foreach ($uses as $use) {
$this->raiseLintAtNode(
$use,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but namespaces were not introduced '.
'until PHP 5.3.');
}
$statics = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($statics as $static) {
$name = $static->getChildByIndex(0);
if ($name->getTypeName() != 'n_CLASS_NAME') {
continue;
}
if ($name->getConcreteString() == 'static') {
$this->raiseLintAtNode(
$name,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but `static::` was not introduced '.
'until PHP 5.3.');
}
}
$ternaries = $root->selectDescendantsOfType('n_TERNARY_EXPRESSION');
foreach ($ternaries as $ternary) {
$yes = $ternary->getChildByIndex(1);
if ($yes->getTypeName() == 'n_EMPTY') {
$this->raiseLintAtNode(
$ternary,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but short ternary was not '.
'introduced until PHP 5.3.');
}
}
$heredocs = $root->selectDescendantsOfType('n_HEREDOC');
foreach ($heredocs as $heredoc) {
if (preg_match('/^<<<[\'"]/', $heredoc->getConcreteString())) {
$this->raiseLintAtNode(
$heredoc,
self::LINT_PHP_53_FEATURES,
'This codebase targets PHP 5.2, but nowdoc was not introduced until '.
'PHP 5.3.');
}
}
$this->lintPHP53Functions($root);
}
private function lintPHP53Functions($root) {
$target = phutil_get_library_root('arcanist').
'/../resources/php_compat_info.json';
$compat_info = json_decode(file_get_contents($target), true);
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$node = $call->getChildByIndex(0);
$name = strtolower($node->getConcreteString());
$version = idx($compat_info['functions'], $name);
$windows = idx($compat_info['functions_windows'], $name);
if ($version) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but `{$name}()` was not ".
"introduced until PHP {$version}.");
} else if (array_key_exists($name, $compat_info['params'])) {
$params = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
foreach (array_values($params->getChildren()) as $i => $param) {
$version = idx($compat_info['params'][$name], $i);
if ($version) {
$this->raiseLintAtNode(
$param,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but parameter ".($i + 1)." ".
"of `{$name}()` was not introduced until PHP {$version}.");
}
}
} else if ($windows === '' || version_compare($windows, '5.3.0') > 0) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.3.0 on Windows, but `{$name}()` is not ".
"available there".
($windows ? " until PHP {$windows}" : "").".");
}
}
$classes = $root->selectDescendantsOfType('n_CLASS_NAME');
foreach ($classes as $node) {
$name = strtolower($node->getConcreteString());
$version = idx($compat_info['interfaces'], $name);
$version = idx($compat_info['classes'], $name, $version);
if ($version) {
$this->raiseLintAtNode(
$node,
self::LINT_PHP_53_FEATURES,
"This codebase targets PHP 5.2.3, but `{$name}` was not ".
"introduced until PHP {$version}.");
}
}
}
public function lintPHP54Features($root) {
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
foreach ($indexes as $index) {
$left = $index->getChildByIndex(0);
switch ($left->getTypeName()) {
case 'n_FUNCTION_CALL':
case 'n_METHOD_CALL':
$this->raiseLintAtNode(
$index->getChildByIndex(1),
self::LINT_PHP_54_FEATURES,
"The f()[...] syntax was not introduced until PHP 5.4, but this ".
"codebase targets an earlier version of PHP. You can rewrite ".
"this expression using idx().");
break;
}
}
}
private function lintImplicitFallthrough($root) {
$hook_obj = null;
$working_copy = $this->getEngine()->getWorkingCopy();
if ($working_copy) {
- $hook_class = $working_copy->getConfig('lint.xhpast.switchhook');
+ $hook_class = $working_copy->getProjectConfig('lint.xhpast.switchhook');
$hook_class = $this->getConfig('switchhook', $hook_class);
if ($hook_class) {
$hook_obj = newv($hook_class, array());
assert_instances_of(array($hook_obj), 'ArcanistXHPASTLintSwitchHook');
}
}
$switches = $root->selectDescendantsOfType('n_SWITCH');
foreach ($switches as $switch) {
$blocks = array();
$cases = $switch->selectDescendantsOfType('n_CASE');
foreach ($cases as $case) {
$blocks[] = $case;
}
$defaults = $switch->selectDescendantsOfType('n_DEFAULT');
foreach ($defaults as $default) {
$blocks[] = $default;
}
foreach ($blocks as $key => $block) {
// Collect all the tokens in this block which aren't at top level.
// We want to ignore "break", and "continue" in these blocks.
$lower_level = $block->selectDescendantsOfType('n_WHILE');
$lower_level->add($block->selectDescendantsOfType('n_DO_WHILE'));
$lower_level->add($block->selectDescendantsOfType('n_FOR'));
$lower_level->add($block->selectDescendantsOfType('n_FOREACH'));
$lower_level->add($block->selectDescendantsOfType('n_SWITCH'));
$lower_level_tokens = array();
foreach ($lower_level as $lower_level_block) {
$lower_level_tokens += $lower_level_block->getTokens();
}
// Collect all the tokens in this block which aren't in this scope
// (because they're inside class, function or interface declarations).
// We want to ignore all of these tokens.
$decls = $block->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$decls->add($block->selectDescendantsOfType('n_CLASS_DECLARATION'));
// For completeness; these can't actually have anything.
$decls->add($block->selectDescendantsOfType('n_INTERFACE_DECLARATION'));
$different_scope_tokens = array();
foreach ($decls as $decl) {
$different_scope_tokens += $decl->getTokens();
}
$lower_level_tokens += $different_scope_tokens;
// Get all the trailing nonsemantic tokens, since we need to look for
// "fallthrough" comments past the end of the semantic block.
$tokens = $block->getTokens();
$last = end($tokens);
while ($last && $last = $last->getNextToken()) {
if ($last->isSemantic()) {
break;
}
$tokens[$last->getTokenID()] = $last;
}
$blocks[$key] = array(
$tokens,
$lower_level_tokens,
$different_scope_tokens,
);
}
foreach ($blocks as $token_lists) {
list(
$tokens,
$lower_level_tokens,
$different_scope_tokens) = $token_lists;
// Test each block (case or default statement) to see if it's OK. It's
// OK if:
//
// - it is empty; or
// - it ends in break, return, throw, continue or exit at top level; or
// - it has a comment with "fallthrough" in its text.
// Empty blocks are OK, so we start this at `true` and only set it to
// false if we find a statement.
$block_ok = true;
// Keeps track of whether the current statement is one that validates
// the block (break, return, throw, continue) or something else.
$statement_ok = false;
foreach ($tokens as $token_id => $token) {
if (!$token->isSemantic()) {
// Liberally match "fall" in the comment text so that comments like
// "fallthru", "fall through", "fallthrough", etc., are accepted.
if (preg_match('/fall/i', $token->getValue())) {
$block_ok = true;
break;
}
continue;
}
$tok_type = $token->getTypeName();
if ($tok_type == 'T_FUNCTION' ||
$tok_type == 'T_CLASS' ||
$tok_type == 'T_INTERFACE') {
// These aren't statements, but mark the block as nonempty anyway.
$block_ok = false;
continue;
}
if ($tok_type == ';') {
if ($statement_ok) {
$statment_ok = false;
} else {
$block_ok = false;
}
continue;
}
if ($tok_type == 'T_BREAK' ||
$tok_type == 'T_CONTINUE') {
if (empty($lower_level_tokens[$token_id])) {
$statement_ok = true;
$block_ok = true;
}
continue;
}
if ($tok_type == 'T_RETURN' ||
$tok_type == 'T_THROW' ||
$tok_type == 'T_EXIT' ||
($hook_obj && $hook_obj->checkSwitchToken($token))) {
if (empty($different_scope_tokens[$token_id])) {
$statement_ok = true;
$block_ok = true;
}
continue;
}
}
if (!$block_ok) {
$this->raiseLintAtToken(
head($tokens),
self::LINT_IMPLICIT_FALLTHROUGH,
"This 'case' or 'default' has a nonempty block which does not ".
"end with 'break', 'continue', 'return', 'throw' or 'exit'. Did ".
"you forget to add one of those? If you intend to fall through, ".
"add a '// fallthrough' comment to silence this warning.");
}
}
}
}
private function lintBraceFormatting($root) {
foreach ($root->selectDescendantsOfType('n_STATEMENT_LIST') as $list) {
$tokens = $list->getTokens();
if (!$tokens || head($tokens)->getValue() != '{') {
continue;
}
list($before, $after) = $list->getSurroundingNonsemanticTokens();
if (!$before) {
$first = head($tokens);
// Only insert the space if we're after a closing parenthesis. If
// we're in a construct like "else{}", other rules will insert space
// after the 'else' correctly.
$prev = $first->getPrevToken();
if (!$prev || $prev->getValue() != ')') {
continue;
}
$this->raiseLintAtToken(
$first,
self::LINT_BRACE_FORMATTING,
'Put opening braces on the same line as control statements and '.
'declarations, with a single space before them.',
' '.$first->getValue());
} else if (count($before) == 1) {
$before = reset($before);
if ($before->getValue() != ' ') {
$this->raiseLintAtToken(
$before,
self::LINT_BRACE_FORMATTING,
'Put opening braces on the same line as control statements and '.
'declarations, with a single space before them.',
' ');
}
}
}
}
private function lintTautologicalExpressions($root) {
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
static $operators = array(
'-' => true,
'/' => true,
'-=' => true,
'/=' => true,
'<=' => true,
'<' => true,
'==' => true,
'===' => true,
'!=' => true,
'!==' => true,
'>=' => true,
'>' => true,
);
static $logical = array(
'||' => true,
'&&' => true,
);
foreach ($expressions as $expr) {
$operator = $expr->getChildByIndex(1)->getConcreteString();
if (!empty($operators[$operator])) {
$left = $expr->getChildByIndex(0)->getSemanticString();
$right = $expr->getChildByIndex(2)->getSemanticString();
if ($left == $right) {
$this->raiseLintAtNode(
$expr,
self::LINT_TAUTOLOGICAL_EXPRESSION,
'Both sides of this expression are identical, so it always '.
'evaluates to a constant.');
}
}
if (!empty($logical[$operator])) {
$left = $expr->getChildByIndex(0)->getSemanticString();
$right = $expr->getChildByIndex(2)->getSemanticString();
// NOTE: These will be null to indicate "could not evaluate".
$left = $this->evaluateStaticBoolean($left);
$right = $this->evaluateStaticBoolean($right);
if (($operator == '||' && ($left === true || $right === true)) ||
($operator == '&&' && ($left === false || $right === false))) {
$this->raiseLintAtNode(
$expr,
self::LINT_TAUTOLOGICAL_EXPRESSION,
'The logical value of this expression is static. Did you forget '.
'to remove some debugging code?');
}
}
}
}
/**
* Statically evaluate a boolean value from an XHP tree.
*
* TODO: Improve this and move it to XHPAST proper?
*
* @param string The "semantic string" of a single value.
* @return mixed ##true## or ##false## if the value could be evaluated
* statically; ##null## if static evaluation was not possible.
*/
private function evaluateStaticBoolean($string) {
switch (strtolower($string)) {
case '0':
case 'null':
case 'false':
return false;
case '1':
case 'true':
return true;
}
return null;
}
protected function lintCommentSpaces($root) {
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
$value = $comment->getValue();
if ($value[0] != '#') {
$match = null;
if (preg_match('@^(/[/*]+)[^/*\s]@', $value, $match)) {
$this->raiseLintAtOffset(
$comment->getOffset(),
self::LINT_COMMENT_SPACING,
'Put space after comment start.',
$match[1],
$match[1].' ');
}
}
}
}
protected function lintHashComments($root) {
foreach ($root->selectTokensOfType('T_COMMENT') as $comment) {
$value = $comment->getValue();
if ($value[0] != '#') {
continue;
}
$this->raiseLintAtOffset(
$comment->getOffset(),
self::LINT_COMMENT_STYLE,
'Use "//" single-line comments, not "#".',
'#',
(preg_match('/^#\S/', $value) ? '// ' : '//'));
}
}
/**
* Find cases where loops get nested inside each other but use the same
* iterator variable. For example:
*
* COUNTEREXAMPLE
* foreach ($list as $thing) {
* foreach ($stuff as $thing) { // <-- Raises an error for reuse of $thing
* // ...
* }
* }
*
*/
private function lintReusedIterators($root) {
$used_vars = array();
$for_loops = $root->selectDescendantsOfType('n_FOR');
foreach ($for_loops as $for_loop) {
$var_map = array();
// Find all the variables that are assigned to in the for() expression.
$for_expr = $for_loop->getChildOfType(0, 'n_FOR_EXPRESSION');
$bin_exprs = $for_expr->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($bin_exprs as $bin_expr) {
if ($bin_expr->getChildByIndex(1)->getConcreteString() == '=') {
$var = $bin_expr->getChildByIndex(0);
$var_map[$var->getConcreteString()] = $var;
}
}
$used_vars[$for_loop->getID()] = $var_map;
}
$foreach_loops = $root->selectDescendantsOfType('n_FOREACH');
foreach ($foreach_loops as $foreach_loop) {
$var_map = array();
$foreach_expr = $foreach_loop->getChildOftype(0, 'n_FOREACH_EXPRESSION');
// We might use one or two vars, i.e. "foreach ($x as $y => $z)" or
// "foreach ($x as $y)".
$possible_used_vars = array(
$foreach_expr->getChildByIndex(1),
$foreach_expr->getChildByIndex(2),
);
foreach ($possible_used_vars as $var) {
if ($var->getTypeName() == 'n_EMPTY') {
continue;
}
$name = $var->getConcreteString();
$name = trim($name, '&'); // Get rid of ref silliness.
$var_map[$name] = $var;
}
$used_vars[$foreach_loop->getID()] = $var_map;
}
$all_loops = $for_loops->add($foreach_loops);
foreach ($all_loops as $loop) {
$child_for_loops = $loop->selectDescendantsOfType('n_FOR');
$child_foreach_loops = $loop->selectDescendantsOfType('n_FOREACH');
$child_loops = $child_for_loops->add($child_foreach_loops);
$outer_vars = $used_vars[$loop->getID()];
foreach ($child_loops as $inner_loop) {
$inner_vars = $used_vars[$inner_loop->getID()];
$shared = array_intersect_key($outer_vars, $inner_vars);
if ($shared) {
$shared_desc = implode(', ', array_keys($shared));
$message = $this->raiseLintAtNode(
$inner_loop->getChildByIndex(0),
self::LINT_REUSED_ITERATORS,
"This loop reuses iterator variables ({$shared_desc}) from an ".
"outer loop. You might be clobbering the outer iterator. Change ".
"the inner loop to use a different iterator name.");
$locations = array();
foreach ($shared as $var) {
$locations[] = $this->getOtherLocation($var->getOffset());
}
$message->setOtherLocations($locations);
}
}
}
}
/**
* Find cases where a foreach loop is being iterated using a variable
* reference and the same variable is used outside of the loop without
* calling unset() or reassigning the variable to another variable
* reference.
*
* COUNTEREXAMPLE
* foreach ($ar as &$a) {
* // ...
* }
* $a = 1; // <-- Raises an error for using $a
*
*/
protected function lintReusedIteratorReferences($root) {
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
$body = $def->getChildByIndex(5);
if ($body->getTypeName() == 'n_EMPTY') {
// Abstract method declaration.
continue;
}
$exclude = array();
// Exclude uses of variables, unsets, and foreach loops
// within closures - they are checked on their own
$func_defs = $body->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($func_defs as $func_def) {
$vars = $func_def->selectDescendantsOfType('n_VARIABLE');
foreach ($vars as $var) {
$exclude[$var->getID()] = true;
}
$unset_lists = $func_def->selectDescendantsOfType('n_UNSET_LIST');
foreach ($unset_lists as $unset_list) {
$exclude[$unset_list->getID()] = true;
}
$foreaches = $func_def->selectDescendantsOfType('n_FOREACH');
foreach ($foreaches as $foreach) {
$exclude[$foreach->getID()] = true;
}
}
// Find all variables that are unset within the scope
$unset_vars = array();
$unset_lists = $body->selectDescendantsOfType('n_UNSET_LIST');
foreach ($unset_lists as $unset_list) {
if (isset($exclude[$unset_list->getID()])) {
continue;
}
$unset_list_vars = $unset_list->selectDescendantsOfType('n_VARIABLE');
foreach ($unset_list_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$unset_vars[$concrete][] = $var->getOffset();
$exclude[$var->getID()] = true;
}
}
// Find all reference variables in foreach expressions
$reference_vars = array();
$foreaches = $body->selectDescendantsOfType('n_FOREACH');
foreach ($foreaches as $foreach) {
if (isset($exclude[$foreach->getID()])) {
continue;
}
$foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
$var = $foreach_expr->getChildByIndex(2);
if ($var->getTypeName() != 'n_VARIABLE_REFERENCE') {
continue;
}
$reference = $var->getChildByIndex(0);
if ($reference->getTypeName() != 'n_VARIABLE') {
continue;
}
$reference_name = $this->getConcreteVariableString($reference);
$reference_vars[$reference_name][] = $reference->getOffset();
$exclude[$reference->getID()] = true;
// Exclude uses of the reference variable within the foreach loop
$foreach_vars = $foreach->selectDescendantsOfType('n_VARIABLE');
foreach ($foreach_vars as $var) {
$name = $this->getConcreteVariableString($var);
if ($name == $reference_name) {
$exclude[$var->getID()] = true;
}
}
}
// Allow usage if the reference variable is assigned to another
// reference variable
$binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binary as $expr) {
if ($expr->getChildByIndex(1)->getConcreteString() != '=') {
continue;
}
$lval = $expr->getChildByIndex(0);
if ($lval->getTypeName() != 'n_VARIABLE') {
continue;
}
$rval = $expr->getChildByIndex(2);
if ($rval->getTypeName() != 'n_VARIABLE_REFERENCE') {
continue;
}
// Counts as unsetting a variable
$concrete = $this->getConcreteVariableString($lval);
$unset_vars[$concrete][] = $lval->getOffset();
$exclude[$lval->getID()] = true;
}
$all_vars = array();
$all = $body->selectDescendantsOfType('n_VARIABLE');
foreach ($all as $var) {
if (isset($exclude[$var->getID()])) {
continue;
}
$name = $this->getConcreteVariableString($var);
if (!isset($reference_vars[$name])) {
continue;
}
// Find the closest reference offset to this variable
$reference_offset = null;
foreach ($reference_vars[$name] as $offset) {
if ($offset < $var->getOffset()) {
$reference_offset = $offset;
} else {
break;
}
}
if (!$reference_offset) {
continue;
}
// Check if an unset exists between reference and usage of this
// variable
$warn = true;
if (isset($unset_vars[$name])) {
foreach ($unset_vars[$name] as $unset_offset) {
if ($unset_offset > $reference_offset &&
$unset_offset < $var->getOffset()) {
$warn = false;
break;
}
}
}
if ($warn) {
$this->raiseLintAtNode(
$var,
self::LINT_REUSED_ITERATOR_REFERENCE,
'This variable was used already as a by-reference iterator ' .
'variable. Such variables survive outside the foreach loop, ' .
'do not reuse.');
}
}
}
}
protected function lintVariableVariables($root) {
$vvars = $root->selectDescendantsOfType('n_VARIABLE_VARIABLE');
foreach ($vvars as $vvar) {
$this->raiseLintAtNode(
$vvar,
self::LINT_VARIABLE_VARIABLE,
'Rewrite this code to use an array. Variable variables are unclear '.
'and hinder static analysis.');
}
}
protected function lintUndeclaredVariables($root) {
// These things declare variables in a function:
// Explicit parameters
// Assignment
// Assignment via list()
// Static
// Global
// Lexical vars
// Builtins ($this)
// foreach()
// catch
//
// These things make lexical scope unknowable:
// Use of extract()
// Assignment to variable variables ($$x)
// Global with variable variables
//
// These things don't count as "using" a variable:
// isset()
// empty()
// Static class variables
//
// The general approach here is to find each function/method declaration,
// then:
//
// 1. Identify all the variable declarations, and where they first occur
// in the function/method declaration.
// 2. Identify all the uses that don't really count (as above).
// 3. Everything else must be a use of a variable.
// 4. For each variable, check if any uses occur before the declaration
// and warn about them.
//
// We also keep track of where lexical scope becomes unknowable (e.g.,
// because the function calls extract() or uses dynamic variables,
// preventing us from keeping track of which variables are defined) so we
// can stop issuing warnings after that.
//
// TODO: Support functions defined inside other functions which is commonly
// used with anonymous functions.
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
// We keep track of the first offset where scope becomes unknowable, and
// silence any warnings after that. Default it to INT_MAX so we can min()
// it later to keep track of the first problem we encounter.
$scope_destroyed_at = PHP_INT_MAX;
$declarations = array(
'$this' => 0,
) + array_fill_keys($this->getSuperGlobalNames(), 0);
$declaration_tokens = array();
$exclude_tokens = array();
$vars = array();
// First up, find all the different kinds of declarations, as explained
// above. Put the tokens into the $vars array.
$param_list = $def->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
$param_vars = $param_list->selectDescendantsOfType('n_VARIABLE');
foreach ($param_vars as $var) {
$vars[] = $var;
}
// This is PHP5.3 closure syntax: function () use ($x) {};
$lexical_vars = $def
->getChildByIndex(4)
->selectDescendantsOfType('n_VARIABLE');
foreach ($lexical_vars as $var) {
$vars[] = $var;
}
$body = $def->getChildByIndex(5);
if ($body->getTypeName() == 'n_EMPTY') {
// Abstract method declaration.
continue;
}
$static_vars = $body
->selectDescendantsOfType('n_STATIC_DECLARATION')
->selectDescendantsOfType('n_VARIABLE');
foreach ($static_vars as $var) {
$vars[] = $var;
}
$global_vars = $body
->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
foreach ($global_vars as $var_list) {
foreach ($var_list->getChildren() as $var) {
if ($var->getTypeName() == 'n_VARIABLE') {
$vars[] = $var;
} else {
// Dynamic global variable, i.e. "global $$x;".
$scope_destroyed_at = min($scope_destroyed_at, $var->getOffset());
// An error is raised elsewhere, no need to raise here.
}
}
}
$catches = $body
->selectDescendantsOfType('n_CATCH')
->selectDescendantsOfType('n_VARIABLE');
foreach ($catches as $var) {
$vars[] = $var;
}
$binary = $body->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binary as $expr) {
if ($expr->getChildByIndex(1)->getConcreteString() != '=') {
continue;
}
$lval = $expr->getChildByIndex(0);
if ($lval->getTypeName() == 'n_VARIABLE') {
$vars[] = $lval;
} else if ($lval->getTypeName() == 'n_LIST') {
// Recursivey grab everything out of list(), since the grammar
// permits list() to be nested. Also note that list() is ONLY valid
// as an lval assignments, so we could safely lift this out of the
// n_BINARY_EXPRESSION branch.
$assign_vars = $lval->selectDescendantsOfType('n_VARIABLE');
foreach ($assign_vars as $var) {
$vars[] = $var;
}
}
if ($lval->getTypeName() == 'n_VARIABLE_VARIABLE') {
$scope_destroyed_at = min($scope_destroyed_at, $lval->getOffset());
// No need to raise here since we raise an error elsewhere.
}
}
$calls = $body->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = strtolower($call->getChildByIndex(0)->getConcreteString());
if ($name == 'empty' || $name == 'isset') {
$params = $call
->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
->selectDescendantsOfType('n_VARIABLE');
foreach ($params as $var) {
$exclude_tokens[$var->getID()] = true;
}
continue;
}
if ($name != 'extract') {
continue;
}
$scope_destroyed_at = min($scope_destroyed_at, $call->getOffset());
$this->raiseLintAtNode(
$call,
self::LINT_EXTRACT_USE,
'Avoid extract(). It is confusing and hinders static analysis.');
}
// Now we have every declaration except foreach(), handled below. Build
// two maps, one which just keeps track of which tokens are part of
// declarations ($declaration_tokens) and one which has the first offset
// where a variable is declared ($declarations).
foreach ($vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$declarations[$concrete] = min(
idx($declarations, $concrete, PHP_INT_MAX),
$var->getOffset());
$declaration_tokens[$var->getID()] = true;
}
// Excluded tokens are ones we don't "count" as being used, described
// above. Put them into $exclude_tokens.
$class_statics = $body
->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
$class_static_vars = $class_statics
->selectDescendantsOfType('n_VARIABLE');
foreach ($class_static_vars as $var) {
$exclude_tokens[$var->getID()] = true;
}
// Find all the variables in scope, and figure out where they are used.
// We want to find foreach() iterators which are both declared before and
// used after the foreach() loop.
$uses = array();
$all_vars = $body->selectDescendantsOfType('n_VARIABLE');
$all = array();
// NOTE: $all_vars is not a real array so we can't unset() it.
foreach ($all_vars as $var) {
// Be strict since it's easier; we don't let you reuse an iterator you
// declared before a loop after the loop, even if you're just assigning
// to it.
$concrete = $this->getConcreteVariableString($var);
$uses[$concrete][$var->getID()] = $var->getOffset();
if (isset($declaration_tokens[$var->getID()])) {
// We know this is part of a declaration, so it's fine.
continue;
}
if (isset($exclude_tokens[$var->getID()])) {
// We know this is part of isset() or similar, so it's fine.
continue;
}
$all[$var->getOffset()] = $concrete;
}
// Do foreach() last, we want to handle implicit redeclaration of a
// variable already in scope since this probably means we're ovewriting a
// local.
// NOTE: Processing foreach expressions in order allows programs which
// reuse iterator variables in other foreach() loops -- this is fine. We
// have a separate warning to prevent nested loops from reusing the same
// iterators.
$foreaches = $body->selectDescendantsOfType('n_FOREACH');
$all_foreach_vars = array();
foreach ($foreaches as $foreach) {
$foreach_expr = $foreach->getChildOfType(0, 'n_FOREACH_EXPRESSION');
$foreach_vars = array();
// Determine the end of the foreach() loop.
$foreach_tokens = $foreach->getTokens();
$last_token = end($foreach_tokens);
$foreach_end = $last_token->getOffset();
$key_var = $foreach_expr->getChildByIndex(1);
if ($key_var->getTypeName() == 'n_VARIABLE') {
$foreach_vars[] = $key_var;
}
$value_var = $foreach_expr->getChildByIndex(2);
if ($value_var->getTypeName() == 'n_VARIABLE') {
$foreach_vars[] = $value_var;
} else {
// The root-level token may be a reference, as in:
// foreach ($a as $b => &$c) { ... }
// Reach into the n_VARIABLE_REFERENCE node to grab the n_VARIABLE
// node.
$var = $value_var->getChildByIndex(0);
if ($var->getTypeName() == 'n_VARIABLE_VARIABLE') {
$var = $var->getChildByIndex(0);
}
$foreach_vars[] = $var;
}
// Remove all uses of the iterators inside of the foreach() loop from
// the $uses map.
foreach ($foreach_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$offset = $var->getOffset();
foreach ($uses[$concrete] as $id => $use_offset) {
if (($use_offset >= $offset) && ($use_offset < $foreach_end)) {
unset($uses[$concrete][$id]);
}
}
$all_foreach_vars[] = $var;
}
}
foreach ($all_foreach_vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$offset = $var->getOffset();
// If a variable was declared before a foreach() and is used after
// it, raise a message.
if (isset($declarations[$concrete])) {
if ($declarations[$concrete] < $offset) {
if (!empty($uses[$concrete]) &&
max($uses[$concrete]) > $offset) {
$message = $this->raiseLintAtNode(
$var,
self::LINT_REUSED_AS_ITERATOR,
'This iterator variable is a previously declared local '.
'variable. To avoid overwriting locals, do not reuse them '.
'as iterator variables.');
$message->setOtherLocations(array(
$this->getOtherLocation($declarations[$concrete]),
$this->getOtherLocation(max($uses[$concrete])),
));
}
}
}
// This is a declaration, exclude it from the "declare variables prior
// to use" check below.
unset($all[$var->getOffset()]);
$vars[] = $var;
}
// Now rebuild declarations to include foreach().
foreach ($vars as $var) {
$concrete = $this->getConcreteVariableString($var);
$declarations[$concrete] = min(
idx($declarations, $concrete, PHP_INT_MAX),
$var->getOffset());
$declaration_tokens[$var->getID()] = true;
}
foreach (array('n_STRING_SCALAR', 'n_HEREDOC') as $type) {
foreach ($body->selectDescendantsOfType($type) as $string) {
foreach ($string->getStringVariables() as $offset => $var) {
$all[$string->getOffset() + $offset - 1] = '$'.$var;
}
}
}
// Issue a warning for every variable token, unless it appears in a
// declaration, we know about a prior declaration, we have explicitly
// exlcuded it, or scope has been made unknowable before it appears.
$issued_warnings = array();
foreach ($all as $offset => $concrete) {
if ($offset >= $scope_destroyed_at) {
// This appears after an extract() or $$var so we have no idea
// whether it's legitimate or not. We raised a harshly-worded warning
// when scope was made unknowable, so just ignore anything we can't
// figure out.
continue;
}
if ($offset >= idx($declarations, $concrete, PHP_INT_MAX)) {
// The use appears after the variable is declared, so it's fine.
continue;
}
if (!empty($issued_warnings[$concrete])) {
// We've already issued a warning for this variable so we don't need
// to issue another one.
continue;
}
$this->raiseLintAtOffset(
$offset,
self::LINT_UNDECLARED_VARIABLE,
'Declare variables prior to use (even if you are passing them '.
'as reference parameters). You may have misspelled this '.
'variable name.',
$concrete);
$issued_warnings[$concrete] = true;
}
}
}
private function getConcreteVariableString($var) {
$concrete = $var->getConcreteString();
// Strip off curly braces as in $obj->{$property}.
$concrete = trim($concrete, '{}');
return $concrete;
}
protected function lintPHPTagUse($root) {
$tokens = $root->getTokens();
foreach ($tokens as $token) {
if ($token->getTypeName() == 'T_OPEN_TAG') {
if (trim($token->getValue()) == '<?') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_SHORT_TAG,
'Use the full form of the PHP open tag, "<?php".',
"<?php\n");
}
break;
} else if ($token->getTypeName() == 'T_OPEN_TAG_WITH_ECHO') {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_ECHO_TAG,
'Avoid the PHP echo short form, "<?=".');
break;
} else {
if (!preg_match('/^#!/', $token->getValue())) {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_OPEN_TAG,
'PHP files should start with "<?php", which may be preceded by '.
'a "#!" line for scripts.');
}
break;
}
}
foreach ($root->selectTokensOfType('T_CLOSE_TAG') as $token) {
$this->raiseLintAtToken(
$token,
self::LINT_PHP_CLOSE_TAG,
'Do not use the PHP closing tag, "?>".');
}
}
protected function lintNamingConventions($root) {
// We're going to build up a list of <type, name, token, error> tuples
// and then try to instantiate a hook class which has the opportunity to
// override us.
$names = array();
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$name_token = $class->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
$names[] = array(
'class',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: classes should be named using '.
'UpperCamelCase.',
);
}
$ifaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($ifaces as $iface) {
$name_token = $iface->getChildByIndex(1);
$name_string = $name_token->getConcreteString();
$names[] = array(
'interface',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUpperCamelCase($name_string)
? null
: 'Follow naming conventions: interfaces should be named using '.
'UpperCamelCase.',
);
}
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name_token = $function->getChildByIndex(2);
if ($name_token->getTypeName() == 'n_EMPTY') {
// Unnamed closure.
continue;
}
$name_string = $name_token->getConcreteString();
$names[] = array(
'function',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
? null
: 'Follow naming conventions: functions should be named using '.
'lowercase_with_underscores.',
);
}
$methods = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$name_token = $method->getChildByIndex(2);
$name_string = $name_token->getConcreteString();
$names[] = array(
'method',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
ArcanistXHPASTLintNamingHook::stripPHPFunction($name_string))
? null
: 'Follow naming conventions: methods should be named using '.
'lowerCamelCase.',
);
}
$param_tokens = array();
$params = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
foreach ($params as $param_list) {
foreach ($param_list->getChildren() as $param) {
$name_token = $param->getChildByIndex(1);
if ($name_token->getTypeName() == 'n_VARIABLE_REFERENCE') {
$name_token = $name_token->getChildOfType(0, 'n_VARIABLE');
}
$param_tokens[$name_token->getID()] = true;
$name_string = $name_token->getConcreteString();
$names[] = array(
'parameter',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
? null
: 'Follow naming conventions: parameters should be named using '.
'lowercase_with_underscores.',
);
}
}
$constants = $root->selectDescendantsOfType(
'n_CLASS_CONSTANT_DECLARATION_LIST');
foreach ($constants as $constant_list) {
foreach ($constant_list->getChildren() as $constant) {
$name_token = $constant->getChildByIndex(0);
$name_string = $name_token->getConcreteString();
$names[] = array(
'constant',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isUppercaseWithUnderscores($name_string)
? null
: 'Follow naming conventions: class constants should be named '.
'using UPPERCASE_WITH_UNDERSCORES.',
);
}
}
$member_tokens = array();
$props = $root->selectDescendantsOfType('n_CLASS_MEMBER_DECLARATION_LIST');
foreach ($props as $prop_list) {
foreach ($prop_list->getChildren() as $token_id => $prop) {
if ($prop->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') {
continue;
}
$name_token = $prop->getChildByIndex(0);
$member_tokens[$name_token->getID()] = true;
$name_string = $name_token->getConcreteString();
$names[] = array(
'member',
$name_string,
$name_token,
ArcanistXHPASTLintNamingHook::isLowerCamelCase(
ArcanistXHPASTLintNamingHook::stripPHPVariable($name_string))
? null
: 'Follow naming conventions: class properties should be named '.
'using lowerCamelCase.',
);
}
}
$superglobal_map = array_fill_keys(
$this->getSuperGlobalNames(),
true);
$fdefs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$mdefs = $root->selectDescendantsOfType('n_METHOD_DECLARATION');
$defs = $fdefs->add($mdefs);
foreach ($defs as $def) {
$globals = $def->selectDescendantsOfType('n_GLOBAL_DECLARATION_LIST');
$globals = $globals->selectDescendantsOfType('n_VARIABLE');
$globals_map = array();
foreach ($globals as $global) {
$global_string = $global->getConcreteString();
$globals_map[$global_string] = true;
$names[] = array(
'user',
$global_string,
$global,
// No advice for globals, but hooks have an option to provide some.
null);
}
// Exclude access of static properties, since lint will be raised at
// their declaration if they're invalid and they may not conform to
// variable rules. This is slightly overbroad (includes the entire
// rhs of a "Class::..." token) to cover cases like "Class:$x[0]". These
// variables are simply made exempt from naming conventions.
$exclude_tokens = array();
$statics = $def->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($statics as $static) {
$rhs = $static->getChildByIndex(1);
$rhs_vars = $def->selectDescendantsOfType('n_VARIABLE');
foreach ($rhs_vars as $var) {
$exclude_tokens[$var->getID()] = true;
}
}
$vars = $def->selectDescendantsOfType('n_VARIABLE');
foreach ($vars as $token_id => $var) {
if (isset($member_tokens[$token_id])) {
continue;
}
if (isset($param_tokens[$token_id])) {
continue;
}
if (isset($exclude_tokens[$token_id])) {
continue;
}
$var_string = $var->getConcreteString();
// Awkward artifact of "$o->{$x}".
$var_string = trim($var_string, '{}');
if (isset($superglobal_map[$var_string])) {
continue;
}
if (isset($globals_map[$var_string])) {
continue;
}
$names[] = array(
'variable',
$var_string,
$var,
ArcanistXHPASTLintNamingHook::isLowercaseWithUnderscores(
ArcanistXHPASTLintNamingHook::stripPHPVariable($var_string))
? null
: 'Follow naming conventions: variables should be named using '.
'lowercase_with_underscores.',
);
}
}
$engine = $this->getEngine();
$working_copy = $engine->getWorkingCopy();
if ($working_copy) {
// If a naming hook is configured, give it a chance to override the
// default results for all the symbol names.
- $hook_class = $working_copy->getConfig('lint.xhpast.naminghook');
+ $hook_class = $working_copy->getProjectConfig('lint.xhpast.naminghook');
if ($hook_class) {
$hook_obj = newv($hook_class, array());
foreach ($names as $k => $name_attrs) {
list($type, $name, $token, $default) = $name_attrs;
$result = $hook_obj->lintSymbolName($type, $name, $default);
$names[$k][3] = $result;
}
}
}
// Raise anything we're left with.
foreach ($names as $k => $name_attrs) {
list($type, $name, $token, $result) = $name_attrs;
if ($result) {
$this->raiseLintAtNode(
$token,
self::LINT_NAMING_CONVENTIONS,
$result);
}
}
}
protected function lintSurpriseConstructors($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1)->getConcreteString();
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$method_name_token = $method->getChildByIndex(2);
$method_name = $method_name_token->getConcreteString();
if (strtolower($class_name) == strtolower($method_name)) {
$this->raiseLintAtNode(
$method_name_token,
self::LINT_IMPLICIT_CONSTRUCTOR,
'Name constructors __construct() explicitly. This method is a '.
'constructor because it has the same name as the class it is '.
'defined in.');
}
}
}
}
protected function lintParenthesesShouldHugExpressions($root) {
$calls = $root->selectDescendantsOfType('n_CALL_PARAMETER_LIST');
$controls = $root->selectDescendantsOfType('n_CONTROL_CONDITION');
$fors = $root->selectDescendantsOfType('n_FOR_EXPRESSION');
$foreach = $root->selectDescendantsOfType('n_FOREACH_EXPRESSION');
$decl = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER_LIST');
$all_paren_groups = $calls
->add($controls)
->add($fors)
->add($foreach)
->add($decl);
foreach ($all_paren_groups as $group) {
$tokens = $group->getTokens();
$token_o = array_shift($tokens);
$token_c = array_pop($tokens);
if ($token_o->getTypeName() != '(') {
throw new Exception('Expected open paren!');
}
if ($token_c->getTypeName() != ')') {
throw new Exception('Expected close paren!');
}
$nonsem_o = $token_o->getNonsemanticTokensAfter();
$nonsem_c = $token_c->getNonsemanticTokensBefore();
if (!$nonsem_o) {
continue;
}
$raise = array();
$string_o = implode('', mpull($nonsem_o, 'getValue'));
if (preg_match('/^[ ]+$/', $string_o)) {
$raise[] = array($nonsem_o, $string_o);
}
if ($nonsem_o !== $nonsem_c) {
$string_c = implode('', mpull($nonsem_c, 'getValue'));
if (preg_match('/^[ ]+$/', $string_c)) {
$raise[] = array($nonsem_c, $string_c);
}
}
foreach ($raise as $warning) {
list($tokens, $string) = $warning;
$this->raiseLintAtOffset(
reset($tokens)->getOffset(),
self::LINT_PARENTHESES_SPACING,
'Parentheses should hug their contents.',
$string,
'');
}
}
}
protected function lintSpaceAfterControlStatementKeywords($root) {
foreach ($root->getTokens() as $id => $token) {
switch ($token->getTypeName()) {
case 'T_IF':
case 'T_ELSE':
case 'T_FOR':
case 'T_FOREACH':
case 'T_WHILE':
case 'T_DO':
case 'T_SWITCH':
$after = $token->getNonsemanticTokensAfter();
if (empty($after)) {
$this->raiseLintAtToken(
$token,
self::LINT_CONTROL_STATEMENT_SPACING,
'Convention: put a space after control statements.',
$token->getValue().' ');
} else if (count($after) == 1) {
$space = head($after);
// If we have an else clause with braces, $space may not be
// a single white space. e.g.,
//
// if ($x)
// echo 'foo'
// else // <- $space is not " " but "\n ".
// echo 'bar'
//
// We just require it starts with either a whitespace or a newline.
if ($token->getTypeName() == 'T_ELSE' ||
$token->getTypeName() == 'T_DO') {
break;
}
if ($space->isAnyWhitespace() && $space->getValue() != ' ') {
$this->raiseLintAtToken(
$space,
self::LINT_CONTROL_STATEMENT_SPACING,
'Convention: put a single space after control statements.',
' ');
}
}
break;
}
}
}
protected function lintSpaceAroundBinaryOperators($root) {
// NOTE: '.' is parsed as n_CONCATENATION_LIST, not n_BINARY_EXPRESSION,
// so we don't select it here.
$expressions = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($expressions as $expression) {
$operator = $expression->getChildByIndex(1);
$operator_value = $operator->getConcreteString();
list($before, $after) = $operator->getSurroundingNonsemanticTokens();
$replace = null;
if (empty($before) && empty($after)) {
$replace = " {$operator_value} ";
} else if (empty($before)) {
$replace = " {$operator_value}";
} else if (empty($after)) {
$replace = "{$operator_value} ";
}
if ($replace !== null) {
$this->raiseLintAtNode(
$operator,
self::LINT_BINARY_EXPRESSION_SPACING,
'Convention: logical and arithmetic operators should be '.
'surrounded by whitespace.',
$replace);
}
}
$tokens = $root->selectTokensOfType(',');
foreach ($tokens as $token) {
$next = $token->getNextToken();
switch ($next->getTypeName()) {
case ')':
case 'T_WHITESPACE':
break;
break;
default:
$this->raiseLintAtToken(
$token,
self::LINT_BINARY_EXPRESSION_SPACING,
'Convention: comma should be followed by space.',
', ');
break;
}
}
// TODO: Spacing around ".".
// TODO: Spacing around default parameter assignment in function/method
// declarations (which is not n_BINARY_EXPRESSION).
}
protected function lintDynamicDefines($root) {
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = $call->getChildByIndex(0)->getConcreteString();
if (strtolower($name) == 'define') {
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
$defined = $parameter_list->getChildByIndex(0);
if (!$defined->isStaticScalar()) {
$this->raiseLintAtNode(
$defined,
self::LINT_DYNAMIC_DEFINE,
'First argument to define() must be a string literal.');
}
}
}
}
protected function lintUseOfThisInStaticMethods($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION');
foreach ($methods as $method) {
$attributes = $method
->getChildByIndex(0, 'n_METHOD_MODIFIER_LIST')
->selectDescendantsOfType('n_STRING');
$method_is_static = false;
$method_is_abstract = false;
foreach ($attributes as $attribute) {
if (strtolower($attribute->getConcreteString()) == 'static') {
$method_is_static = true;
}
if (strtolower($attribute->getConcreteString()) == 'abstract') {
$method_is_abstract = true;
}
}
if ($method_is_abstract) {
continue;
}
if (!$method_is_static) {
continue;
}
$body = $method->getChildOfType(5, 'n_STATEMENT_LIST');
$variables = $body->selectDescendantsOfType('n_VARIABLE');
foreach ($variables as $variable) {
if ($method_is_static &&
strtolower($variable->getConcreteString()) == '$this') {
$this->raiseLintAtNode(
$variable,
self::LINT_STATIC_THIS,
'You can not reference "$this" inside a static method.');
}
}
}
}
}
/**
* preg_quote() takes two arguments, but the second one is optional because
* it is possible to use (), [] or {} as regular expression delimiters. If
* you don't pass a second argument, you're probably going to get something
* wrong.
*/
protected function lintPregQuote($root) {
$function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($function_calls as $call) {
$name = $call->getChildByIndex(0)->getConcreteString();
if (strtolower($name) === 'preg_quote') {
$parameter_list = $call->getChildOfType(1, 'n_CALL_PARAMETER_LIST');
if (count($parameter_list->getChildren()) !== 2) {
$this->raiseLintAtNode(
$call,
self::LINT_PREG_QUOTE_MISUSE,
'If you use pattern delimiters that require escaping (such as //, '.
'but not ()) then you should pass two arguments to preg_quote(), '.
'so that preg_quote() knows which delimiter to escape.');
}
}
}
}
/**
* Exit is parsed as an expression, but using it as such is almost always
* wrong. That is, this is valid:
*
* strtoupper(33 * exit - 6);
*
* When exit is used as an expression, it causes the program to terminate with
* exit code 0. This is likely not what is intended; these statements have
* different effects:
*
* exit(-1);
* exit -1;
*
* The former exits with a failure code, the latter with a success code!
*/
protected function lintExitExpressions($root) {
$unaries = $root->selectDescendantsOfType('n_UNARY_PREFIX_EXPRESSION');
foreach ($unaries as $unary) {
$operator = $unary->getChildByIndex(0)->getConcreteString();
if (strtolower($operator) == 'exit') {
if ($unary->getParentNode()->getTypeName() != 'n_STATEMENT') {
$this->raiseLintAtNode(
$unary,
self::LINT_EXIT_EXPRESSION,
"Use exit as a statement, not an expression.");
}
}
}
}
private function lintArrayIndexWhitespace($root) {
$indexes = $root->selectDescendantsOfType('n_INDEX_ACCESS');
foreach ($indexes as $index) {
$tokens = $index->getChildByIndex(0)->getTokens();
$last = array_pop($tokens);
$trailing = $last->getNonsemanticTokensAfter();
$trailing_text = implode('', mpull($trailing, 'getValue'));
if (preg_match('/^ +$/', $trailing_text)) {
$this->raiseLintAtOffset(
$last->getOffset() + strlen($last->getValue()),
self::LINT_ARRAY_INDEX_SPACING,
'Convention: no spaces before index access.',
$trailing_text,
'');
}
}
}
protected function lintTODOComments($root) {
$comments = $root->selectTokensOfType('T_COMMENT') +
$root->selectTokensOfType('T_DOC_COMMENT');
foreach ($comments as $token) {
$value = $token->getValue();
$matches = null;
$preg = preg_match_all(
'/TODO/',
$value,
$matches,
PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $match) {
list($string, $offset) = $match;
$this->raiseLintAtOffset(
$token->getOffset() + $offset,
self::LINT_TODO_COMMENT,
'This comment has a TODO.',
$string);
}
}
}
/**
* Lint that if the file declares exactly one interface or class,
* the name of the file matches the name of the class,
* unless the classname is funky like an XHP element.
*/
private function lintPrimaryDeclarationFilenameMatch($root) {
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
if (count($classes) + count($interfaces) != 1) {
return;
}
$declarations = count($classes) ? $classes : $interfaces;
$declarations->rewind();
$declaration = $declarations->current();
$decl_name = $declaration->getChildByIndex(1);
$decl_string = $decl_name->getConcreteString();
// Exclude strangely named classes, e.g. XHP tags.
if (!preg_match('/^\w+$/', $decl_string)) {
return;
}
$rename = $decl_string.'.php';
$path = $this->getActivePath();
$filename = basename($path);
if ($rename == $filename) {
return;
}
$this->raiseLintAtNode(
$decl_name,
self::LINT_CLASS_FILENAME_MISMATCH,
"The name of this file differs from the name of the class or interface ".
"it declares. Rename the file to '{$rename}'.");
}
private function lintPlusOperatorOnStrings($root) {
$binops = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
foreach ($binops as $binop) {
$op = $binop->getChildByIndex(1);
if ($op->getConcreteString() != '+') {
continue;
}
$left = $binop->getChildByIndex(0);
$right = $binop->getChildByIndex(2);
if (($left->getTypeName() == 'n_STRING_SCALAR') ||
($right->getTypeName() == 'n_STRING_SCALAR')) {
$this->raiseLintAtNode(
$binop,
self::LINT_PLUS_OPERATOR_ON_STRINGS,
"In PHP, '.' is the string concatenation operator, not '+'. This ".
"expression uses '+' with a string literal as an operand.");
}
}
}
/**
* Finds duplicate keys in array initializers, as in
* array(1 => 'anything', 1 => 'foo'). Since the first entry is ignored,
* this is almost certainly an error.
*/
private function lintDuplicateKeysInArray($root) {
$array_literals = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
foreach ($array_literals as $array_literal) {
$nodes_by_key = array();
$keys_warn = array();
$list_node = $array_literal->getChildByIndex(0);
foreach ($list_node->getChildren() as $array_entry) {
$key_node = $array_entry->getChildByIndex(0);
switch ($key_node->getTypeName()) {
case 'n_STRING_SCALAR':
case 'n_NUMERIC_SCALAR':
// Scalars: array(1 => 'v1', '1' => 'v2');
$key = 'scalar:'.(string)$key_node->evalStatic();
break;
case 'n_SYMBOL_NAME':
case 'n_VARIABLE':
case 'n_CLASS_STATIC_ACCESS':
// Constants: array(CONST => 'v1', CONST => 'v2');
// Variables: array($a => 'v1', $a => 'v2');
// Class constants and vars: array(C::A => 'v1', C::A => 'v2');
$key = $key_node->getTypeName().':'.$key_node->getConcreteString();
break;
default:
$key = null;
break;
}
if ($key !== null) {
if (isset($nodes_by_key[$key])) {
$keys_warn[$key] = true;
}
$nodes_by_key[$key][] = $key_node;
}
}
foreach ($keys_warn as $key => $_) {
$node = array_pop($nodes_by_key[$key]);
$message = $this->raiseLintAtNode(
$node,
self::LINT_DUPLICATE_KEYS_IN_ARRAY,
"Duplicate key in array initializer. PHP will ignore all ".
"but the last entry.");
$locations = array();
foreach ($nodes_by_key[$key] as $node) {
$locations[] = $this->getOtherLocation($node->getOffset());
}
$message->setOtherLocations($locations);
}
}
}
private function lintRaggedClasstreeEdges($root) {
$parser = new PhutilDocblockParser();
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$is_final = false;
$is_abstract = false;
$is_concrete_extensible = false;
$attributes = $class->getChildOfType(0, 'n_CLASS_ATTRIBUTES');
foreach ($attributes->getChildren() as $child) {
if ($child->getConcreteString() == 'final') {
$is_final = true;
}
if ($child->getConcreteString() == 'abstract') {
$is_abstract = true;
}
}
$docblock = $class->getDocblockToken();
if ($docblock) {
list($text, $specials) = $parser->parse($docblock->getValue());
$is_concrete_extensible = idx($specials, 'concrete-extensible');
}
if (!$is_final && !$is_abstract && !$is_concrete_extensible) {
$this->raiseLintAtNode(
$class->getChildOfType(1, 'n_CLASS_NAME'),
self::LINT_RAGGED_CLASSTREE_EDGE,
"This class is neither 'final' nor 'abstract', and does not have ".
"a docblock marking it '@concrete-extensible'.");
}
}
}
private function lintClosingCallParen($root) {
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
$calls = $calls->add($root->selectDescendantsOfType('n_METHOD_CALL'));
foreach ($calls as $call) {
// If the last parameter of a call is a HEREDOC, don't apply this rule.
$params = $call
->getChildOfType(1, 'n_CALL_PARAMETER_LIST')
->getChildren();
if ($params) {
$last_param = last($params);
if ($last_param->getTypeName() == 'n_HEREDOC') {
continue;
}
}
$tokens = $call->getTokens();
$last = array_pop($tokens);
$trailing = $last->getNonsemanticTokensBefore();
$trailing_text = implode('', mpull($trailing, 'getValue'));
if (preg_match('/^\s+$/', $trailing_text)) {
$this->raiseLintAtOffset(
$last->getOffset() - strlen($trailing_text),
self::LINT_CLOSING_CALL_PAREN,
'Convention: no spaces before closing parenthesis in calls.',
$trailing_text,
'');
}
}
}
private function lintClosingDeclarationParen($root) {
$decs = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
$decs = $decs->add($root->selectDescendantsOfType('n_METHOD_DECLARATION'));
foreach ($decs as $dec) {
$params = $dec->getChildOfType(3, 'n_DECLARATION_PARAMETER_LIST');
$tokens = $params->getTokens();
$last = array_pop($tokens);
$trailing = $last->getNonsemanticTokensBefore();
$trailing_text = implode('', mpull($trailing, 'getValue'));
if (preg_match('/^\s+$/', $trailing_text)) {
$this->raiseLintAtOffset(
$last->getOffset() - strlen($trailing_text),
self::LINT_CLOSING_DECL_PAREN,
'Convention: no spaces before closing parenthesis in function and '.
'method declarations.',
$trailing_text,
'');
}
}
}
private function lintKeywordCasing($root) {
$keywords = array();
$symbols = $root->selectDescendantsOfType('n_SYMBOL_NAME');
foreach ($symbols as $symbol) {
$keywords[] = head($symbol->getTokens());
}
$arrays = $root->selectDescendantsOfType('n_ARRAY_LITERAL');
foreach ($arrays as $array) {
$keywords[] = head($array->getTokens());
}
$typehints = $root->selectDescendantsOfType('n_TYPE_NAME');
foreach ($typehints as $typehint) {
$keywords[] = head($typehint->getTokens());
}
$new_invocations = $root->selectDescendantsOfType('n_NEW');
foreach ($new_invocations as $invocation) {
$keywords[] = head($invocation->getTokens());
}
// NOTE: Although PHP generally allows arbitrary casing for all language
// keywords, it's exceedingly rare for anyone to type, e.g., "CLASS" or
// "cLaSs" in the wild. This list just attempts to cover unconventional
// spellings which see some level of use, not all keywords exhaustively.
// There is no token or node type which spans all keywords, so this is
// significantly simpler.
static $keyword_map = array(
'true' => 'true',
'false' => 'false',
'null' => 'null',
'array' => 'array',
'new' => 'new',
);
foreach ($keywords as $keyword) {
$value = $keyword->getValue();
$value_key = strtolower($value);
if (!isset($keyword_map[$value_key])) {
continue;
}
$expected_spelling = $keyword_map[$value_key];
if ($value !== $expected_spelling) {
$this->raiseLintAtToken(
$keyword,
self::LINT_KEYWORD_CASING,
"Convention: spell keyword '{$value}' as '{$expected_spelling}'.",
$expected_spelling);
}
}
}
public function getSuperGlobalNames() {
return array(
'$GLOBALS',
'$_SERVER',
'$_GET',
'$_POST',
'$_FILES',
'$_COOKIE',
'$_SESSION',
'$_REQUEST',
'$_ENV',
);
}
}
diff --git a/src/lint/linter/__tests__/ArcanistLinterTestCase.php b/src/lint/linter/__tests__/ArcanistLinterTestCase.php
index 8be89b88..3c500b3a 100644
--- a/src/lint/linter/__tests__/ArcanistLinterTestCase.php
+++ b/src/lint/linter/__tests__/ArcanistLinterTestCase.php
@@ -1,190 +1,194 @@
<?php
/**
* Facilitates implementation of test cases for @{class:ArcanistLinter}s.
*
* @group testcase
*/
abstract class ArcanistLinterTestCase extends ArcanistPhutilTestCase {
public function executeTestsInDirectory($root, ArcanistLinter $linter) {
foreach (Filesystem::listDirectory($root, $hidden = false) as $file) {
$this->lintFile($root.$file, $linter);
}
}
private function lintFile($file, $linter) {
$linter = clone $linter;
$contents = Filesystem::readFile($file);
$contents = explode("~~~~~~~~~~\n", $contents);
if (count($contents) < 2) {
throw new Exception(
"Expected '~~~~~~~~~~' separating test case and results.");
}
list ($data, $expect, $xform, $config) = array_merge(
$contents,
array(null, null));
$basename = basename($file);
if ($config) {
$config = json_decode($config, true);
if (!is_array($config)) {
throw new Exception(
"Invalid configuration in test '{$basename}', not valid JSON.");
}
} else {
$config = array();
}
PhutilTypeSpec::checkMap(
$config,
array(
'hook' => 'optional bool',
'config' => 'optional wild',
'path' => 'optional string',
'arcconfig' => 'optional map<string, string>',
));
$exception = null;
$after_lint = null;
$messages = null;
$exception_message = false;
$caught_exception = false;
try {
$tmp = new TempFile($basename);
Filesystem::writeFile($tmp, $data);
$full_path = (string)$tmp;
$dir = dirname($full_path);
$path = basename($full_path);
$config_file = null;
$arcconfig = idx($config, 'arcconfig');
if ($arcconfig) {
$config_file = json_encode($arcconfig);
}
$working_copy = ArcanistWorkingCopyIdentity::newFromRootAndConfigFile(
$dir,
$config_file,
'Unit Test');
+ $configuration_manager = new ArcanistConfigurationManager();
+ $configuration_manager->setWorkingCopyIdentity($working_copy);
+
$engine = new UnitTestableArcanistLintEngine();
$engine->setWorkingCopy($working_copy);
+ $engine->setConfigurationManager($configuration_manager);
$engine->setPaths(array($path));
$engine->setCommitHookMode(idx($config, 'hook', false));
$path_name = idx($config, 'path', $path);
$linter->addPath($path_name);
$linter->addData($path_name, $data);
$linter->setConfig(idx($config, 'config', array()));
$engine->addLinter($linter);
$engine->addFileData($path_name, $data);
$results = $engine->run();
$this->assertEqual(
1,
count($results),
'Expect one result returned by linter.');
$result = reset($results);
$patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
$after_lint = $patcher->getModifiedFileContent();
} catch (ArcanistPhutilTestTerminatedException $ex) {
throw $ex;
} catch (Exception $exception) {
$caught_exception = true;
if ($exception instanceof PhutilAggregateException) {
$caught_exception = false;
foreach ($exception->getExceptions() as $ex) {
if ($ex instanceof ArcanistUsageException) {
$this->assertSkipped($ex->getMessage());
} else {
$caught_exception = true;
}
}
}
$exception_message = $exception->getMessage()."\n\n".
$exception->getTraceAsString();
}
switch ($basename) {
default:
$this->assertEqual(false, $caught_exception, $exception_message);
$this->compareLint($basename, $expect, $result);
$this->compareTransform($xform, $after_lint);
break;
}
}
private function compareLint($file, $expect, $result) {
$seen = array();
$raised = array();
$message_map = array();
foreach ($result->getMessages() as $message) {
$sev = $message->getSeverity();
$line = $message->getLine();
$char = $message->getChar();
$code = $message->getCode();
$name = $message->getName();
$message_key = $sev.":".$line.":".$char;
$message_map[$message_key] = $message;
$seen[] = $message_key;
$raised[] = " {$sev} at line {$line}, char {$char}: {$code} {$name}";
}
$expect = trim($expect);
if ($expect) {
$expect = explode("\n", $expect);
} else {
$expect = array();
}
foreach ($expect as $key => $expected) {
$expect[$key] = head(explode(' ', $expected));
}
$expect = array_fill_keys($expect, true);
$seen = array_fill_keys($seen, true);
if (!$raised) {
$raised = array("No messages.");
}
$raised = "Actually raised:\n".implode("\n", $raised);
foreach (array_diff_key($expect, $seen) as $missing => $ignored) {
list($sev, $line, $char) = explode(':', $missing);
$this->assertFailure(
"In '{$file}', ".
"expected lint to raise {$sev} on line {$line} at char {$char}, ".
"but no {$sev} was raised. {$raised}");
}
foreach (array_diff_key($seen, $expect) as $surprising => $ignored) {
$message = $message_map[$surprising];
$message_info = $message->getDescription();
list($sev, $line, $char) = explode(':', $surprising);
$this->assertFailure(
"In '{$file}', ".
"lint raised {$sev} on line {$line} at char {$char}, ".
"but nothing was expected:\n\n{$message_info}\n\n{$raised}");
}
}
protected function compareTransform($expected, $actual) {
if (!strlen($expected)) {
return;
}
$this->assertEqual(
$expected,
$actual,
"File as patched by lint did not match the expected patched file.");
}
}
diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
index 3c5d8340..a15a96ac 100644
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1,1090 +1,1090 @@
<?php
/**
* Interfaces with Git working copies.
*
* @group workingcopy
*/
final class ArcanistGitAPI extends ArcanistRepositoryAPI {
private $repositoryHasNoCommits = false;
const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16;
/**
* For the repository's initial commit, 'git diff HEAD^' and similar do
* not work. Using this instead does work; it is the hash of the empty tree.
*/
const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
public static function newHookAPI($root) {
return new ArcanistGitAPI($root);
}
protected function buildLocalFuture(array $argv) {
$argv[0] = 'git '.$argv[0];
$future = newv('ExecFuture', $argv);
$future->setCWD($this->getPath());
return $future;
}
public function execPassthru($pattern /* , ... */) {
$args = func_get_args();
static $git = null;
if ($git === null) {
if (phutil_is_windows()) {
// NOTE: On Windows, phutil_passthru() uses 'bypass_shell' because
// everything goes to hell if we don't. We must provide an absolute
// path to Git for this to work properly.
$git = Filesystem::resolveBinary('git');
$git = csprintf('%s', $git);
} else {
$git = 'git';
}
}
$args[0] = $git.' '.$args[0];
return call_user_func_array('phutil_passthru', $args);
}
public function getSourceControlSystemName() {
return 'git';
}
public function getMetadataPath() {
static $path = null;
if ($path === null) {
list($stdout) = $this->execxLocal('rev-parse --git-dir');
$path = rtrim($stdout, "\n");
// the output of git rev-parse --git-dir is an absolute path, unless
// the cwd is the root of the repository, in which case it uses the
// relative path of .git. If we get this relative path, turn it into
// an absolute path.
if ($path === '.git') {
$path = $this->getPath('.git');
}
}
return $path;
}
public function getHasCommits() {
return !$this->repositoryHasNoCommits;
}
public function getLocalCommitInformation() {
if ($this->repositoryHasNoCommits) {
// Zero commits.
throw new Exception(
"You can't get local commit information for a repository with no ".
"commits.");
} else if ($this->getBaseCommit() == self::GIT_MAGIC_ROOT_COMMIT) {
// One commit.
$against = 'HEAD';
} else {
// 2..N commits. We include commits reachable from HEAD which are
// not reachable from the relative commit; this is consistent with
// user expectations even though it is not actually the diff range.
// Particularly:
//
// |
// D <----- master branch
// |
// C Y <- feature branch
// | /|
// B X
// | /
// A
// |
//
// If "A, B, C, D" are master, and the user is at Y, when they run
// "arc diff B" they want (and get) a diff of B vs Y, but they think about
// this as being the commits X and Y. If we log "B..Y", we only show
// Y. With "Y --not B", we show X and Y.
$against = csprintf('%s --not %s', 'HEAD', $this->getBaseCommit());
}
// NOTE: Windows escaping of "%" symbols apparently is inherently broken;
// when passed throuhgh escapeshellarg() they are replaced with spaces.
// TODO: Learn how cmd.exe works and find some clever workaround?
// NOTE: If we use "%x00", output is truncated in Windows.
list($info) = $this->execxLocal(
phutil_is_windows()
? 'log %C --format=%C --'
: 'log %C --format=%s --',
$against,
// NOTE: "%B" is somewhat new, use "%s%n%n%b" instead.
'%H%x01%T%x01%P%x01%at%x01%an%x01%aE%x01%s%x01%s%n%n%b%x02');
$commits = array();
$info = trim($info, " \n\2");
if (!strlen($info)) {
return array();
}
$info = explode("\2", $info);
foreach ($info as $line) {
list($commit, $tree, $parents, $time, $author, $author_email,
$title, $message) = explode("\1", trim($line), 8);
$message = rtrim($message);
$commits[$commit] = array(
'commit' => $commit,
'tree' => $tree,
'parents' => array_filter(explode(' ', $parents)),
'time' => $time,
'author' => $author,
'summary' => $title,
'message' => $message,
'authorEmail' => $author_email,
);
}
return $commits;
}
protected function buildBaseCommit($symbolic_commit) {
if ($symbolic_commit !== null) {
if ($symbolic_commit == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$this->setBaseCommitExplanation(
"you explicitly specified the empty tree.");
return $symbolic_commit;
}
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$symbolic_commit);
if ($err) {
throw new ArcanistUsageException(
"Unable to find any git commit named '{$symbolic_commit}' in ".
"this repository.");
}
$this->setBaseCommitExplanation(
"it is the merge-base of '{$symbolic_commit}' and HEAD, as you ".
"explicitly specified.");
return trim($merge_base);
}
// Detect zero-commit or one-commit repositories. There is only one
// relative-commit value that makes any sense in these repositories: the
// empty tree.
list($err) = $this->execManualLocal('rev-parse --verify HEAD^');
if ($err) {
list($err) = $this->execManualLocal('rev-parse --verify HEAD');
if ($err) {
$this->repositoryHasNoCommits = true;
}
if ($this->repositoryHasNoCommits) {
$this->setBaseCommitExplanation(
"the repository has no commits.");
} else {
$this->setBaseCommitExplanation(
"the repository has only one commit.");
}
return self::GIT_MAGIC_ROOT_COMMIT;
}
if ($this->getBaseCommitArgumentRules() ||
$this->getConfigurationManager()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
return $base;
}
$do_write = false;
$default_relative = null;
$working_copy = $this->getWorkingCopyIdentity();
if ($working_copy) {
- $default_relative = $working_copy->getConfig(
+ $default_relative = $working_copy->getProjectConfig(
'git.default-relative-commit');
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in 'git.default-relative-commit' in '.arcconfig'. This ".
"setting overrides other settings.");
}
if (!$default_relative) {
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$default_relative = trim($upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' (the Git upstream ".
"of the current branch) HEAD.");
}
}
if (!$default_relative) {
$default_relative = $this->readScratchFile('default-relative-commit');
$default_relative = trim($default_relative);
if ($default_relative) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in '.git/arc/default-relative-commit'.");
}
}
if (!$default_relative) {
// TODO: Remove the history lesson soon.
echo phutil_console_format(
"<bg:green>** Select a Default Commit Range **</bg>\n\n");
echo phutil_console_wrap(
"You're running a command which operates on a range of revisions ".
"(usually, from some revision to HEAD) but have not specified the ".
"revision that should determine the start of the range.\n\n".
"Previously, arc assumed you meant 'HEAD^' when you did not specify ".
"a start revision, but this behavior does not make much sense in ".
"most workflows outside of Facebook's historic git-svn workflow.\n\n".
"arc no longer assumes 'HEAD^'. You must specify a relative commit ".
"explicitly when you invoke a command (e.g., `arc diff HEAD^`, not ".
"just `arc diff`) or select a default for this working copy.\n\n".
"In most cases, the best default is 'origin/master'. You can also ".
"select 'HEAD^' to preserve the old behavior, or some other remote ".
"or branch. But you almost certainly want to select ".
"'origin/master'.\n\n".
"(Technically: the merge-base of the selected revision and HEAD is ".
"used to determine the start of the commit range.)");
$prompt = "What default do you want to use? [origin/master]";
$default = phutil_console_prompt($prompt);
if (!strlen(trim($default))) {
$default = 'origin/master';
}
$default_relative = $default;
$do_write = true;
}
list($object_type) = $this->execxLocal(
'cat-file -t %s',
$default_relative);
if (trim($object_type) !== 'commit') {
throw new Exception(
"Relative commit '{$default_relative}' is not the name of a commit!");
}
if ($do_write) {
// Don't perform this write until we've verified that the object is a
// valid commit name.
$this->writeScratchFile('default-relative-commit', $default_relative);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as you ".
"just specified.");
}
list($merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$default_relative);
return trim($merge_base);
}
private function getDiffFullOptions($detect_moves_and_renames = true) {
$options = array(
self::getDiffBaseOptions(),
'--no-color',
'--src-prefix=a/',
'--dst-prefix=b/',
'-U'.$this->getDiffLinesOfContext(),
);
if ($detect_moves_and_renames) {
$options[] = '-M';
$options[] = '-C';
}
return implode(' ', $options);
}
private function getDiffBaseOptions() {
$options = array(
// Disable external diff drivers, like graphical differs, since Arcanist
// needs to capture the diff text.
'--no-ext-diff',
// Disable textconv so we treat binary files as binary, even if they have
// an alternative textual representation. TODO: Ideally, Differential
// would ship up the binaries for 'arc patch' but display the textconv
// output in the visual diff.
'--no-textconv',
);
return implode(' ', $options);
}
public function getFullGitDiff() {
$options = $this->getDiffFullOptions();
list($stdout) = $this->execxLocal(
"diff {$options} %s --",
$this->getBaseCommit());
return $stdout;
}
/**
* @param string Path to generate a diff for.
* @param bool If true, detect moves and renames. Otherwise, ignore
* moves/renames; this is useful because it prompts git to
* generate real diff text.
*/
public function getRawDiffText($path, $detect_moves_and_renames = true) {
$options = $this->getDiffFullOptions($detect_moves_and_renames);
list($stdout) = $this->execxLocal(
"diff {$options} %s -- %s",
$this->getBaseCommit(),
$path);
return $stdout;
}
public function getBranchName() {
// TODO: consider:
//
// $ git rev-parse --abbrev-ref `git symbolic-ref HEAD`
//
// But that may fail if you're not on a branch.
list($stdout) = $this->execxLocal('branch --no-color');
// Assume that any branch beginning with '(' means 'no branch', or whatever
// 'no branch' is in the current locale.
$matches = null;
if (preg_match('/^\* ([^\(].*)$/m', $stdout, $matches)) {
return $matches[1];
}
return null;
}
public function getSourceControlPath() {
// TODO: Try to get something useful here.
return null;
}
public function getGitCommitLog() {
$relative = $this->getBaseCommit();
if ($this->repositoryHasNoCommits) {
// No commits yet.
return '';
} else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) {
// First commit.
list($stdout) = $this->execxLocal(
'log --format=medium HEAD');
} else {
// 2..N commits.
list($stdout) = $this->execxLocal(
'log --first-parent --format=medium %s..HEAD',
$this->getBaseCommit());
}
return $stdout;
}
public function getGitHistoryLog() {
list($stdout) = $this->execxLocal(
'log --format=medium -n%d %s',
self::SEARCH_LENGTH_FOR_PARENT_REVISIONS,
$this->getBaseCommit());
return $stdout;
}
public function getSourceControlBaseRevision() {
list($stdout) = $this->execxLocal(
'rev-parse %s',
$this->getBaseCommit());
return rtrim($stdout, "\n");
}
public function getCanonicalRevisionName($string) {
$match = null;
if (preg_match('/@([0-9]+)$/', $string, $match)) {
$stdout = $this->getHashFromFromSVNRevisionNumber($match[1]);
} else {
list($stdout) = $this->execxLocal(
'show -s --format=%C %s',
'%H',
$string);
}
return rtrim($stdout);
}
private function executeSVNFindRev($input, $vcs) {
$match = array();
list($stdout) = $this->execxLocal(
'svn find-rev %s',
$input);
if (!$stdout) {
throw new ArcanistUsageException("Cannot find the {$vcs} equivalent "
."of {$input}.");
}
// When git performs a partial-rebuild during svn
// look-up, we need to parse the final line
$lines = explode("\n", $stdout);
$stdout = $lines[count($lines) - 2];
return rtrim($stdout);
}
// Convert svn revision number to git hash
public function getHashFromFromSVNRevisionNumber($revision_id) {
return $this->executeSVNFindRev("r".$revision_id, "Git");
}
// Convert a git hash to svn revision number
public function getSVNRevisionNumberFromHash($hash) {
return $this->executeSVNFindRev($hash, "SVN");
}
protected function buildUncommittedStatus() {
$diff_options = $this->getDiffBaseOptions();
if ($this->repositoryHasNoCommits) {
$diff_base = self::GIT_MAGIC_ROOT_COMMIT;
} else {
$diff_base = 'HEAD';
}
// Find uncommitted changes.
$uncommitted_future = $this->buildLocalFuture(
array(
'diff %C --raw %s --',
$diff_options,
$diff_base,
));
$untracked_future = $this->buildLocalFuture(
array(
'ls-files --others --exclude-standard',
));
// TODO: This doesn't list unstaged adds. It's not clear how to get that
// list other than "git status --porcelain" and then parsing it. :/
// Unstaged changes
$unstaged_future = $this->buildLocalFuture(
array(
'ls-files -m',
));
$futures = array(
$uncommitted_future,
$untracked_future,
$unstaged_future,
);
Futures($futures)->resolveAll();
$result = new PhutilArrayWithDefaultValue();
list($stdout) = $uncommitted_future->resolvex();
$uncommitted_files = $this->parseGitStatus($stdout);
foreach ($uncommitted_files as $path => $mask) {
$result[$path] |= ($mask | self::FLAG_UNCOMMITTED);
}
list($stdout) = $untracked_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $path) {
$result[$path] |= self::FLAG_UNTRACKED;
}
}
list($stdout, $stderr) = $unstaged_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $path) {
$result[$path] |= self::FLAG_UNSTAGED;
}
}
return $result->toArray();
}
protected function buildCommitRangeStatus() {
list($stdout, $stderr) = $this->execxLocal(
'diff %C --raw %s --',
$this->getDiffBaseOptions(),
$this->getBaseCommit());
return $this->parseGitStatus($stdout);
}
public function getGitConfig($key, $default = null) {
list($err, $stdout) = $this->execManualLocal('config %s', $key);
if ($err) {
return $default;
}
return rtrim($stdout);
}
public function getAuthor() {
list($stdout) = $this->execxLocal('var GIT_AUTHOR_IDENT');
return preg_replace('/\s+<.*/', '', rtrim($stdout, "\n"));
}
public function addToCommit(array $paths) {
$this->execxLocal(
'add -A -- %Ls',
$paths);
$this->reloadWorkingCopy();
return $this;
}
public function doCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
// NOTE: "--allow-empty-message" was introduced some time after 1.7.0.4,
// so we do not provide it and thus require a message.
$this->execxLocal(
'commit -F %s',
$tmp_file);
$this->reloadWorkingCopy();
return $this;
}
public function amendCommit($message = null) {
if ($message === null) {
$this->execxLocal('commit --amend --allow-empty -C HEAD');
} else {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal(
'commit --amend --allow-empty -F %s',
$tmp_file);
}
$this->reloadWorkingCopy();
return $this;
}
public function getPreReceiveHookStatus($old_ref, $new_ref) {
$options = $this->getDiffBaseOptions();
list($stdout) = $this->execxLocal(
"diff {$options} --raw %s %s --",
$old_ref,
$new_ref);
return $this->parseGitStatus($stdout, $full = true);
}
private function parseGitStatus($status, $full = false) {
static $flags = array(
'A' => self::FLAG_ADDED,
'M' => self::FLAG_MODIFIED,
'D' => self::FLAG_DELETED,
);
$status = trim($status);
$lines = array();
foreach (explode("\n", $status) as $line) {
if ($line) {
$lines[] = preg_split("/[ \t]/", $line, 6);
}
}
$files = array();
foreach ($lines as $line) {
$mask = 0;
$flag = $line[4];
$file = $line[5];
foreach ($flags as $key => $bits) {
if ($flag == $key) {
$mask |= $bits;
}
}
if ($full) {
$files[$file] = array(
'mask' => $mask,
'ref' => rtrim($line[3], '.'),
);
} else {
$files[$file] = $mask;
}
}
return $files;
}
public function getAllFiles() {
$future = $this->buildLocalFuture(array('ls-files -z'));
return id(new LinesOfALargeExecFuture($future))
->setDelimiter("\0");
}
public function getChangedFiles($since_commit) {
list($stdout) = $this->execxLocal(
'diff --raw %s',
$since_commit);
return $this->parseGitStatus($stdout);
}
public function getBlame($path) {
// TODO: 'git blame' supports --porcelain and we should probably use it.
list($stdout) = $this->execxLocal(
'blame --date=iso -w -M %s -- %s',
$this->getBaseCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
// lines predating a git repo's history are blamed to the oldest revision,
// with the commit hash prepended by a ^. we shouldn't count these lines
// as blaming to the oldest diff's unfortunate author
if ($line[0] == '^') {
continue;
}
$matches = null;
$ok = preg_match(
'/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/',
$line,
$matches);
if (!$ok) {
throw new Exception("Bad blame? `{$line}'");
}
$revision = $matches[1];
$author = $matches[2];
$blame[] = array($author, $revision);
}
return $blame;
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getBaseCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision($path, 'HEAD');
}
private function parseGitTree($stdout) {
$result = array();
$stdout = trim($stdout);
if (!strlen($stdout)) {
return $result;
}
$lines = explode("\n", $stdout);
foreach ($lines as $line) {
$matches = array();
$ok = preg_match(
'/^(\d{6}) (blob|tree|commit) ([a-z0-9]{40})[\t](.*)$/',
$line,
$matches);
if (!$ok) {
throw new Exception("Failed to parse git ls-tree output!");
}
$result[$matches[4]] = array(
'mode' => $matches[1],
'type' => $matches[2],
'ref' => $matches[3],
);
}
return $result;
}
private function getFileDataAtRevision($path, $revision) {
// NOTE: We don't want to just "git show {$revision}:{$path}" since if the
// path was a directory at the given revision we'll get a list of its files
// and treat it as though it as a file containing a list of other files,
// which is silly.
list($stdout) = $this->execxLocal(
'ls-tree %s -- %s',
$revision,
$path);
$info = $this->parseGitTree($stdout);
if (empty($info[$path])) {
// No such path, or the path is a directory and we executed 'ls-tree dir/'
// and got a list of its contents back.
return null;
}
if ($info[$path]['type'] != 'blob') {
// Path is or was a directory, not a file.
return null;
}
list($stdout) = $this->execxLocal(
'cat-file blob %s',
$info[$path]['ref']);
return $stdout;
}
/**
* Returns names of all the branches in the current repository.
*
* @return list<dict<string, string>> Dictionary of branch information.
*/
public function getAllBranches() {
list($branch_info) = $this->execxLocal(
'branch --no-color');
$lines = explode("\n", rtrim($branch_info));
$result = array();
foreach ($lines as $line) {
if (preg_match('/^[* ]+\(no branch\)/', $line)) {
// This is indicating that the working copy is in a detached state;
// just ignore it.
continue;
}
list($current, $name) = preg_split('/\s+/', $line, 2);
$result[] = array(
'current' => !empty($current),
'name' => $name,
);
}
return $result;
}
public function getWorkingCopyRevision() {
list($stdout) = $this->execxLocal('rev-parse HEAD');
return rtrim($stdout, "\n");
}
public function getUnderlyingWorkingCopyRevision() {
list($err, $stdout) = $this->execManualLocal('svn find-rev HEAD');
if (!$err && $stdout) {
return rtrim($stdout, "\n");
}
return $this->getWorkingCopyRevision();
}
public function isHistoryDefaultImmutable() {
return false;
}
public function supportsAmend() {
return true;
}
public function supportsCommitRanges() {
return true;
}
public function supportsLocalCommits() {
return true;
}
public function hasLocalCommit($commit) {
try {
if (!$this->getCanonicalRevisionName($commit)) {
return false;
}
} catch (CommandException $exception) {
return false;
}
return true;
}
public function getAllLocalChanges() {
$diff = $this->getFullGitDiff();
if (!strlen(trim($diff))) {
return array();
}
$parser = new ArcanistDiffParser();
return $parser->parseDiff($diff);
}
public function supportsLocalBranchMerge() {
return true;
}
public function performLocalBranchMerge($branch, $message) {
if (!$branch) {
throw new ArcanistUsageException(
"Under git, you must specify the branch you want to merge.");
}
$err = phutil_passthru(
'(cd %s && git merge --no-ff -m %s %s)',
$this->getPath(),
$message,
$branch);
if ($err) {
throw new ArcanistUsageException("Merge failed!");
}
}
public function getFinalizedRevisionMessage() {
return "You may now push this commit upstream, as appropriate (e.g. with ".
"'git push', or 'git svn dcommit', or by printing and faxing it).";
}
public function getCommitMessage($commit) {
list($message) = $this->execxLocal(
'log -n1 --format=%C %s --',
'%s%n%n%b',
$commit);
return $message;
}
public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query) {
$messages = $this->getGitCommitLog();
if (!strlen($messages)) {
return array();
}
$parser = new ArcanistDiffParser();
$messages = $parser->parseDiff($messages);
// First, try to find revisions by explicit revision IDs in commit messages.
$reason_map = array();
$revision_ids = array();
foreach ($messages as $message) {
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$message->getMetadata('message'));
if ($object->getRevisionID()) {
$revision_ids[] = $object->getRevisionID();
$reason_map[$object->getRevisionID()] = $message->getCommitHash();
}
}
if ($revision_ids) {
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'ids' => $revision_ids,
));
foreach ($results as $key => $result) {
$hash = substr($reason_map[$result['id']], 0, 16);
$results[$key]['why'] =
"Commit message for '{$hash}' has explicit 'Differential Revision'.";
}
return $results;
}
// If we didn't succeed, try to find revisions by hash.
$hashes = array();
foreach ($this->getLocalCommitInformation() as $commit) {
$hashes[] = array('gtcm', $commit['commit']);
$hashes[] = array('gttr', $commit['tree']);
}
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'commitHashes' => $hashes,
));
foreach ($results as $key => $result) {
$results[$key]['why'] =
"A git commit or tree hash in the commit range is already attached ".
"to the Differential revision.";
}
return $results;
}
public function updateWorkingCopy() {
$this->execxLocal('pull');
$this->reloadWorkingCopy();
}
public function getCommitSummary($commit) {
if ($commit == self::GIT_MAGIC_ROOT_COMMIT) {
return '(The Empty Tree)';
}
list($summary) = $this->execxLocal(
'log -n 1 --format=%C %s',
'%s',
$commit);
return trim($summary);
}
public function backoutCommit($commit_hash) {
$this->execxLocal(
'revert %s -n --no-edit', $commit_hash);
$this->reloadWorkingCopy();
if (!$this->getUncommittedStatus()) {
throw new ArcanistUsageException(
"{$commit_hash} has already been reverted.");
}
}
public function getBackoutMessage($commit_hash) {
return "This reverts commit ".$commit_hash.".";
}
public function isGitSubversionRepo() {
return Filesystem::pathExists($this->getPath('.git/svn'));
}
public function resolveBaseCommitRule($rule, $source) {
list($type, $name) = explode(':', $rule, 2);
switch ($type) {
case 'git':
$matches = null;
if (preg_match('/^merge-base\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$matches[1]);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$matches[1]}' and HEAD, as ".
"specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return trim($merge_base);
}
} else if (preg_match('/^branch-unique\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$matches[1]);
if ($err) {
return null;
}
$merge_base = trim($merge_base);
list($commits) = $this->execxLocal(
'log --format=%C %s..HEAD --',
'%H',
$merge_base);
$commits = array_filter(explode("\n", $commits));
if (!$commits) {
return null;
}
$commits[] = $merge_base;
$head_branch_count = null;
foreach ($commits as $commit) {
list($branches) = $this->execxLocal(
'branch --contains %s',
$commit);
$branches = array_filter(explode("\n", $branches));
if ($head_branch_count === null) {
// If this is the first commit, it's HEAD. Count how many
// branches it is on; we want to include commits on the same
// number of branches. This covers a case where this branch
// has sub-branches and we're running "arc diff" here again
// for whatever reason.
$head_branch_count = count($branches);
} else if (count($branches) > $head_branch_count) {
foreach ($branches as $key => $branch) {
$branches[$key] = trim($branch, ' *');
}
$branches = implode(', ', $branches);
$this->setBaseCommitExplanation(
"it is the first commit between '{$merge_base}' (the ".
"merge-base of '{$matches[1]}' and HEAD) which is also ".
"contained by another branch ({$branches}).");
return $commit;
}
}
} else {
list($err) = $this->execManualLocal(
'cat-file -t %s',
$name);
if (!$err) {
$this->setBaseCommitExplanation(
"it is specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return $name;
}
}
break;
case 'arc':
switch ($name) {
case 'empty':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return self::GIT_MAGIC_ROOT_COMMIT;
case 'amended':
$text = $this->getCommitMessage('HEAD');
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$text);
if ($message->getRevisionID()) {
$this->setBaseCommitExplanation(
"HEAD has been amended with 'Differential Revision:', ".
"as specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'HEAD^';
}
break;
case 'upstream':
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$upstream = rtrim($upstream);
list($upstream_merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$upstream);
$upstream_merge_base = rtrim($upstream_merge_base);
$this->setBaseCommitExplanation(
"it is the merge-base of the upstream of the current branch ".
"and HEAD, and matched the rule '{$rule}' in your {$source} ".
"'base' configuration.");
return $upstream_merge_base;
}
break;
case 'this':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return 'HEAD^';
}
default:
return null;
}
return null;
}
public function canStashChanges() {
return true;
}
public function stashChanges() {
$this->execxLocal('stash');
$this->reloadWorkingCopy();
}
public function unstashChanges() {
$this->execxLocal('stash pop');
}
}
diff --git a/src/workflow/ArcanistGetConfigWorkflow.php b/src/workflow/ArcanistGetConfigWorkflow.php
index bb37f5a2..fd348490 100644
--- a/src/workflow/ArcanistGetConfigWorkflow.php
+++ b/src/workflow/ArcanistGetConfigWorkflow.php
@@ -1,95 +1,95 @@
<?php
/**
* Read configuration settings.
*
* @group workflow
*/
final class ArcanistGetConfigWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'get-config';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**get-config** -- [__name__ ...]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Reads an arc configuration option. With no argument, reads all
options.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'argv',
);
}
public function desiresRepositoryAPI() {
return true;
}
public function run() {
$argv = $this->getArgument('argv');
$settings = new ArcanistSettings();
$configuration_manager = $this->getConfigurationManager();
$configs = array(
ArcanistConfigurationManager::CONFIG_SOURCE_SYSTEM =>
$configuration_manager->readSystemArcConfig(),
ArcanistConfigurationManager::CONFIG_SOURCE_USER =>
$configuration_manager->readUserArcConfig(),
ArcanistConfigurationManager::CONFIG_SOURCE_PROJECT =>
$this->getWorkingCopy()->readProjectConfig(),
ArcanistConfigurationManager::CONFIG_SOURCE_LOCAL =>
$configuration_manager->readLocalArcConfig(),
);
if ($argv) {
$keys = $argv;
} else {
$keys = array_mergev(array_map('array_keys', $configs));
$keys = array_unique($keys);
sort($keys);
}
$multi = (count($keys) > 1);
foreach ($keys as $key) {
if ($multi) {
echo "{$key}\n";
}
foreach ($configs as $name => $config) {
switch ($name) {
case ArcanistConfigurationManager::CONFIG_SOURCE_PROJECT:
// Respect older names in project config.
- $val = $this->getWorkingCopy()->getConfig($key);
+ $val = $this->getWorkingCopy()->getProjectConfig($key);
break;
default:
$val = idx($config, $key);
break;
}
if ($val === null) {
continue;
}
$val = $settings->formatConfigValueForDisplay($key, $val);
printf("% 10.10s: %s\n", $name, $val);
}
if ($multi) {
echo "\n";
}
}
return 0;
}
}
diff --git a/src/workflow/ArcanistSvnHookPreCommitWorkflow.php b/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
index f4ac5bb9..a444aafb 100644
--- a/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
+++ b/src/workflow/ArcanistSvnHookPreCommitWorkflow.php
@@ -1,232 +1,232 @@
<?php
/**
* Installable as an SVN "pre-commit" hook.
*
* @group workflow
*/
final class ArcanistSvnHookPreCommitWorkflow extends ArcanistBaseWorkflow {
public function getWorkflowName() {
return 'svn-hook-pre-commit';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**svn-hook-pre-commit** __repository__ __transaction__
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: svn
You can install this as an SVN pre-commit hook. For more information,
see the article "Installing Arcanist SVN Hooks" in the Arcanist
documentation.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'svnargs',
);
}
public function shouldShellComplete() {
return false;
}
public function run() {
$svnargs = $this->getArgument('svnargs');
$repository = $svnargs[0];
$transaction = $svnargs[1];
list($commit_message) = execx(
'svnlook log --transaction %s %s',
$transaction,
$repository);
if (strpos($commit_message, '@bypass-lint') !== false) {
return 0;
}
// TODO: Do stuff with commit message.
list($changed) = execx(
'svnlook changed --transaction %s %s',
$transaction,
$repository);
$paths = array();
$changed = explode("\n", trim($changed));
foreach ($changed as $line) {
$matches = null;
preg_match('/^..\s*(.*)$/', $line, $matches);
$paths[$matches[1]] = strlen($matches[1]);
}
$resolved = array();
$failed = array();
$missing = array();
$found = array();
asort($paths);
foreach ($paths as $path => $length) {
foreach ($resolved as $rpath => $root) {
if (!strncmp($path, $rpath, strlen($rpath))) {
$resolved[$path] = $root;
continue 2;
}
}
$config = $path;
if (basename($config) == '.arcconfig') {
$resolved[$config] = $config;
continue;
}
$config = rtrim($config, '/');
$last_config = $config;
do {
if (!empty($missing[$config])) {
break;
} else if (!empty($found[$config])) {
$resolved[$path] = $found[$config];
break;
}
list($err) = exec_manual(
'svnlook cat --transaction %s %s %s',
$transaction,
$repository,
$config ? $config.'/.arcconfig' : '.arcconfig');
if ($err) {
$missing[$path] = true;
} else {
$resolved[$path] = $config ? $config.'/.arcconfig' : '.arcconfig';
$found[$config] = $resolved[$path];
break;
}
$config = dirname($config);
if ($config == '.') {
$config = '';
}
if ($config == $last_config) {
break;
}
$last_config = $config;
} while (true);
if (empty($resolved[$path])) {
$failed[] = $path;
}
}
if ($failed && $resolved) {
$failed_paths = ' '.implode("\n ", $failed);
$resolved_paths = ' '.implode("\n ", array_keys($resolved));
throw new ArcanistUsageException(
"This commit includes a mixture of files in Arcanist projects and ".
"outside of Arcanist projects. A commit which affects an Arcanist ".
"project must affect only that project.\n\n".
"Files in projects:\n\n".
$resolved_paths."\n\n".
"Files not in projects:\n\n".
$failed_paths);
}
if (!$resolved) {
// None of the affected paths are beneath a .arcconfig file.
return 0;
}
$groups = array();
foreach ($resolved as $path => $project) {
$groups[$project][] = $path;
}
if (count($groups) > 1) {
$message = array();
foreach ($groups as $project => $group) {
$message[] = "Files underneath '{$project}':\n\n";
$message[] = " ".implode("\n ", $group)."\n\n";
}
$message = implode('', $message);
throw new ArcanistUsageException(
"This commit includes a mixture of files from different Arcanist ".
"projects. A commit which affects an Arcanist project must affect ".
"only that project.\n\n".
$message);
}
$config_file = key($groups);
$project_root = dirname($config_file);
$paths = reset($groups);
list($config) = execx(
'svnlook cat --transaction %s %s %s',
$transaction,
$repository,
$config_file);
$working_copy = ArcanistWorkingCopyIdentity::newFromRootAndConfigFile(
$project_root,
$this->getConfigurationManager(),
$config,
$config_file." (svnlook: {$transaction} {$repository})");
$repository_api = new ArcanistSubversionHookAPI(
$project_root,
$transaction,
$repository);
- $lint_engine = $working_copy->getConfig('lint.engine');
+ $lint_engine = $working_copy->getProjectConfig('lint.engine');
if (!$lint_engine) {
return 0;
}
$engine = newv($lint_engine, array());
$engine->setWorkingCopy($working_copy);
$engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ERROR);
$engine->setPaths($paths);
$engine->setCommitHookMode(true);
$engine->setHookAPI($repository_api);
try {
$results = $engine->run();
} catch (ArcanistNoEffectException $no_effect) {
// Nothing to do, bail out.
return 0;
}
$failures = array();
foreach ($results as $result) {
if (!$result->getMessages()) {
continue;
}
$failures[] = $result;
}
if ($failures) {
$at = "@";
$msg = phutil_console_format(
"\n**LINT ERRORS**\n\n".
"This changeset has lint errors. You must fix all lint errors before ".
"you can commit.\n\n".
"You can add '{$at}bypass-lint' to your commit message to disable ".
"lint checks for this commit, or '{$at}nolint' to the file with ".
"errors to disable lint for that file.\n\n");
echo phutil_console_wrap($msg);
$renderer = new ArcanistLintConsoleRenderer();
foreach ($failures as $result) {
echo $renderer->renderLintResult($result);
}
return 1;
}
return 0;
}
}
diff --git a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
index bf3fbd59..c86f1def 100644
--- a/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
+++ b/src/workingcopyidentity/ArcanistWorkingCopyIdentity.php
@@ -1,247 +1,255 @@
<?php
/**
* Interfaces with basic information about the working copy.
*
*
* @task config
*
* @group workingcopy
*/
final class ArcanistWorkingCopyIdentity {
protected $localConfig;
protected $projectConfig;
protected $projectRoot;
protected $localMetaDir;
public static function newDummyWorkingCopy() {
return new ArcanistWorkingCopyIdentity('/', array());
}
public static function newFromPath($path) {
$project_id = null;
$project_root = null;
$config = array();
foreach (Filesystem::walkToRoot($path) as $dir) {
$config_file = $dir.'/.arcconfig';
if (!Filesystem::pathExists($config_file)) {
continue;
}
$proj_raw = Filesystem::readFile($config_file);
$config = self::parseRawConfigFile($proj_raw, $config_file);
$project_root = $dir;
break;
}
if (!$project_root) {
foreach (Filesystem::walkToRoot($path) as $dir) {
$try = array(
$dir.'/.svn',
$dir.'/.hg',
$dir.'/.git',
);
foreach ($try as $trydir) {
if (Filesystem::pathExists($trydir)) {
$project_root = $dir;
break 2;
}
}
}
}
return new ArcanistWorkingCopyIdentity($project_root, $config);
}
public static function newFromRootAndConfigFile(
$root,
$config_raw,
$from_where) {
if ($config_raw === null) {
$config = array();
} else {
$config = self::parseRawConfigFile($config_raw, $from_where);
}
return new ArcanistWorkingCopyIdentity($root, $config);
}
private static function parseRawConfigFile($raw_config, $from_where) {
$proj = json_decode($raw_config, true);
if (!is_array($proj)) {
throw new Exception(
"Unable to parse '.arcconfig' file '{$from_where}'. The file contents ".
"should be valid JSON.\n\n".
"FILE CONTENTS\n".
substr($raw_config, 0, 2048));
}
$required_keys = array(
'project_id',
);
foreach ($required_keys as $key) {
if (!array_key_exists($key, $proj)) {
throw new Exception(
"Required key '{$key}' is missing from '.arcconfig' file ".
"'{$from_where}'.");
}
}
return $proj;
}
protected function __construct($root, array $config) {
$this->projectRoot = $root;
$this->projectConfig = $config;
$this->localConfig = array();
$this->localMetaDir = null;
$vc_dirs = array(
'.git',
'.hg',
'.svn',
);
$found_meta_dir = false;
foreach ($vc_dirs as $dir) {
$meta_path = Filesystem::resolvePath(
$dir,
$this->projectRoot);
if (Filesystem::pathExists($meta_path)) {
$found_meta_dir = true;
$this->localMetaDir = $meta_path;
$local_path = Filesystem::resolvePath(
'arc/config',
$meta_path);
$this->localConfig = $this->readLocalArcConfig();
break;
}
}
if (!$found_meta_dir) {
// Try for a single higher-level .svn directory as used by svn 1.7+
foreach (Filesystem::walkToRoot($this->projectRoot) as $parent_path) {
$meta_path = Filesystem::resolvePath(
'.svn',
$parent_path);
$local_path = Filesystem::resolvePath(
'.svn/arc/config',
$parent_path);
if (Filesystem::pathExists($local_path)) {
$this->localMetaDir = $meta_path;
$this->localConfig = $this->readLocalArcConfig();
}
}
}
}
public function getProjectID() {
- return $this->getConfig('project_id');
+ return $this->getProjectConfig('project_id');
}
public function getProjectRoot() {
return $this->projectRoot;
}
public function getProjectPath($to_file) {
return $this->projectRoot.'/'.$to_file;
}
/* -( Config )------------------------------------------------------------- */
public function readProjectConfig() {
return $this->projectConfig;
}
+ /**
+ * Deprecated; use @{method:getProjectConfig}.
+ */
+ public function getConfig($key, $default = null) {
+ return $this->getProjectConfig($key, $default);
+ }
+
+
/**
* Read a configuration directive from project configuration. This reads ONLY
* permanent project configuration (i.e., ".arcconfig"), not other
* configuration sources. See @{method:getConfigFromAnySource} to read from
* user configuration.
*
* @param key Key to read.
* @param wild Default value if key is not found.
* @return wild Value, or default value if not found.
*
* @task config
*/
- public function getConfig($key, $default = null) {
+ public function getProjectConfig($key, $default = null) {
$settings = new ArcanistSettings();
$pval = idx($this->projectConfig, $key);
// Test for older names in the per-project config only, since
// they've only been used there.
if ($pval === null) {
$legacy = $settings->getLegacyName($key);
if ($legacy) {
- $pval = $this->getConfig($legacy);
+ $pval = $this->getProjectConfig($legacy);
}
}
if ($pval === null) {
$pval = $default;
} else {
$pval = $settings->willReadValue($key, $pval);
}
return $pval;
}
/**
* Read a configuration directive from local configuration. This
* reads ONLY the per-working copy configuration,
* i.e. .(git|hg|svn)/arc/config, and not other configuration
* sources. See @{method:getConfigFromAnySource} to read from any
- * config source or @{method:getConfig} to read permanent
+ * config source or @{method:getProjectConfig} to read permanent
* project-level config.
*
* @task config
*/
public function getLocalConfig($key, $default=null) {
return idx($this->localConfig, $key, $default);
}
public function readLocalArcConfig() {
if (strlen($this->localMetaDir)) {
$local_path = Filesystem::resolvePath(
'arc/config',
$this->localMetaDir);
if (Filesystem::pathExists($local_path)) {
$file = Filesystem::readFile($local_path);
if ($file) {
return json_decode($file, true);
}
}
}
return array();
}
public function writeLocalArcConfig(array $config) {
$dir = $this->localMetaDir;
if (!strlen($dir)) {
return false;
}
if (!Filesystem::pathExists($dir)) {
try {
Filesystem::createDirectory($dir);
} catch (Exception $ex) {
return false;
}
}
$json_encoder = new PhutilJSON();
$json = $json_encoder->encodeFormatted($config);
$config_file = Filesystem::resolvePath('arc/config', $dir);
try {
Filesystem::writeFile($config_file, $json);
} catch (FilesystemException $ex) {
return false;
}
return true;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Dec 24, 07:44 (22 h, 59 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
557043
Default Alt Text
(214 KB)

Event Timeline