Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F995693
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
67 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/scripts/phutil_analyzer.php b/scripts/phutil_analyzer.php
index e519ae13..80f5201f 100755
--- a/scripts/phutil_analyzer.php
+++ b/scripts/phutil_analyzer.php
@@ -1,361 +1,362 @@
#!/usr/bin/env php
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$builtin_classes = get_declared_classes();
$builtin_interfaces = get_declared_interfaces();
$builtin_functions = get_defined_functions();
$builtin_functions = $builtin_functions['internal'];
$builtin = array(
'class' => array_fill_keys($builtin_classes, true) + array(
'PhutilBootloader' => true,
),
'function' => array_fill_keys($builtin_functions, true) + array(
'empty' => true,
'isset' => true,
'echo' => true,
'print' => true,
'exit' => true,
'die' => true,
+ 'phutil_load_library' => true,
),
'interface' => array_fill_keys($builtin_interfaces, true),
);
require_once dirname(__FILE__).'/__init_script__.php';
if ($argc != 2) {
$self = basename($argv[0]);
echo "usage: {$self} <module>\n";
exit(1);
}
phutil_require_module('phutil', 'filesystem');
$dir = Filesystem::resolvePath($argv[1]);
phutil_require_module('phutil', 'parser/xhpast/bin');
phutil_require_module('phutil', 'parser/xhpast/api/tree');
phutil_require_module('arcanist', 'lint/linter/phutilmodule');
phutil_require_module('arcanist', 'lint/message');
phutil_require_module('arcanist', 'parser/phutilmodule');
$data = array();
$futures = array();
foreach (Filesystem::listDirectory($dir, $hidden_files = false) as $file) {
if (!preg_match('/.php$/', $file)) {
continue;
}
$data[$file] = Filesystem::readFile($dir.'/'.$file);
$futures[$file] = xhpast_get_parser_future($data[$file]);
}
$requirements = new PhutilModuleRequirements();
$requirements->addBuiltins($builtin);
$has_init = false;
$has_files = false;
foreach (Futures($futures) as $file => $future) {
try {
$tree = XHPASTTree::newFromDataAndResolvedExecFuture(
$data[$file],
$future->resolve());
} catch (XHPASTSyntaxErrorException $ex) {
echo "Syntax Error! In '{$file}': ".$ex->getMessage()."\n";
exit(1);
}
$root = $tree->getRootNode();
$requirements->setCurrentFile($file);
if ($file == '__init__.php') {
$has_init = true;
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = $call->getChildByIndex(0);
$call_name = $name->getConcreteString();
if ($call_name == 'phutil_require_source') {
$params = $call->getChildByIndex(1)->getChildren();
if (count($params) !== 1) {
$requirements->addLint(
$call,
$call->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE,
"Call to phutil_require_source() must have exactly one argument.");
continue;
}
$param = reset($params);
$value = $param->getStringLiteralValue();
if ($value === null) {
$requirements->addLint(
$param,
$param->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE,
"phutil_require_source() parameter must be a string literal.");
continue;
}
$requirements->addSourceDependency($name, $value);
} else if ($call_name == 'phutil_require_module') {
analyze_phutil_require_module($call, $requirements);
}
}
} else {
$has_files = true;
$requirements->addSourceDeclaration(basename($file));
// Function uses:
// - Explicit call
// TODO?: String literal in ReflectionFunction().
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($calls as $call) {
$name = $call->getChildByIndex(0);
if ($name->getTypeName() == 'n_VARIABLE' ||
$name->getTypeName() == 'n_VARIABLE_VARIABLE') {
$requirements->addLint(
$name,
$name->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC,
"Use of variable function calls prevents dependencies from being ".
"checked statically. This module may have undetectable errors.");
continue;
}
if ($name->getTypeName() == 'n_CLASS_STATIC_ACCESS') {
// We'll pick this up later.
continue;
}
$call_name = $name->getConcreteString();
if ($call_name == 'phutil_require_module') {
analyze_phutil_require_module($call, $requirements);
} else if ($call_name == 'call_user_func' ||
$call_name == 'call_user_func_array') {
$params = $call->getChildByIndex(1)->getChildren();
if (count($params) == 0) {
$requirements->addLint(
$call,
$call->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE,
"Call to {$call_name}() must have at least one argument.");
}
$symbol = array_shift($params);
$symbol_value = $symbol->getStringLiteralValue();
if ($symbol_value) {
$requirements->addFunctionDependency(
$symbol,
$symbol_value);
} else {
$requirements->addLint(
$symbol,
$symbol->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC,
"Use of variable arguments to {$call_name} prevents dependencies ".
"from being checked statically. This module may have undetectable ".
"errors.");
}
} else {
$requirements->addFunctionDependency(
$name,
$name->getConcreteString());
}
}
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name = $function->getChildByIndex(2);
$requirements->addFunctionDeclaration(
$name,
$name->getConcreteString());
}
// Class uses:
// - new
// - extends (in class declaration)
// - Static method call
// - Static property access
// - Constant use
// TODO?: String literal in ReflectionClass().
// TODO?: String literal in array literal in call_user_func /
// call_user_func_array().
// TODO: Raise a soft warning for use of an unknown class in:
// - Typehints
// - instanceof
// - catch
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1);
$requirements->addClassDeclaration(
$class_name,
$class_name->getConcreteString());
$extends = $class->getChildByIndex(2);
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) {
$requirements->addClassDependency(
$class_name->getConcreteString(),
$parent,
$parent->getConcreteString());
}
$implements = $class->getChildByIndex(3);
$interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME');
foreach ($interfaces as $interface) {
$requirements->addInterfaceDependency(
$class_name->getConcreteString(),
$interface,
$interface->getConcreteString());
}
}
if (count($classes) > 1) {
foreach ($classes as $class) {
$class_name = $class->getChildByIndex(1);
$class_string = $class_name->getConcreteString();
$requirements->addLint(
$class_name,
$class_string,
ArcanistPhutilModuleLinter::LINT_ANALYZER_MULTIPLE_CLASSES,
"This file declares more than one class. Declare only one class per ".
"file.");
break;
}
}
$uses_of_new = $root->selectDescendantsOfType('n_NEW');
foreach ($uses_of_new as $new_operator) {
$name = $new_operator->getChildByIndex(0);
if ($name->getTypeName() == 'n_VARIABLE' ||
$name->getTypeName() == 'n_VARIABLE_VARIABLE') {
$requirements->addLint(
$name,
$name->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_DYNAMIC,
"Use of variable class instantiation prevents dependencies from ".
"being checked statically. This module may have undetectable ".
"errors.");
continue;
}
$requirements->addClassDependency(
null,
$name,
$name->getConcreteString());
}
$static_uses = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($static_uses as $static_use) {
$name = $static_use->getChildByIndex(0);
if ($name->getTypeName() != 'n_CLASS_NAME') {
echo "WARNING UNLINTABLE\n";
continue;
}
$name_concrete = $name->getConcreteString();
$magic_names = array(
'static' => true,
'parent' => true,
'self' => true,
);
if (isset($magic_names[$name_concrete])) {
continue;
}
$requirements->addClassDependency(
null,
$name,
$name_concrete);
}
// Interface uses:
// - implements
// - extends (in interface declaration)
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
foreach ($interfaces as $interface) {
$interface_name = $interface->getChildByIndex(1);
$requirements->addInterfaceDeclaration(
$interface_name,
$interface_name->getConcreteString());
$extends = $interface->getChildByIndex(2);
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) {
$requirements->addInterfaceDependency(
$class_name->getConcreteString(),
$parent,
$parent->getConcreteString());
}
}
}
}
if (!$has_init && $has_files) {
$requirements->addRawLint(
ArcanistPhutilModuleLinter::LINT_ANALYZER_NO_INIT,
"Create an __init__.php file in this module.");
}
echo json_encode($requirements->toDictionary());
/**
* Parses meaning from calls to phutil_require_module() in __init__.php files.
*
* @group module
*/
function analyze_phutil_require_module(
XHPASTNode $call,
PhutilModuleRequirements $requirements) {
$name = $call->getChildByIndex(0);
$params = $call->getChildByIndex(1)->getChildren();
if (count($params) !== 2) {
$requirements->addLint(
$call,
$call->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE,
"Call to phutil_require_module() must have exactly two arguments.");
return;
}
$module_param = array_pop($params);
$library_param = array_pop($params);
$library_value = $library_param->getStringLiteralValue();
if ($library_value === null) {
$requirements->addLint(
$library_param,
$library_param->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE,
"phutil_require_module() parameters must be string literals.");
return;
}
$module_value = $module_param->getStringLiteralValue();
if ($module_value === null) {
$requirements->addLint(
$module_param,
$module_param->getConcreteString(),
ArcanistPhutilModuleLinter::LINT_ANALYZER_SIGNATURE,
"phutil_require_module() parameters must be string literals.");
return;
}
$requirements->addModuleDependency(
$name,
$library_value.':'.$module_value);
}
diff --git a/scripts/phutil_mapper.php b/scripts/phutil_mapper.php
index 7edf1133..7ad38d3f 100755
--- a/scripts/phutil_mapper.php
+++ b/scripts/phutil_mapper.php
@@ -1,189 +1,214 @@
#!/usr/bin/env php
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
require_once dirname(__FILE__).'/__init_script__.php';
+$liberate_mode = false;
+for ($ii = 0; $ii < $argc; $ii++) {
+ if ($argv[$ii] == '--find-paths-for-liberate') {
+ $liberate_mode = true;
+ unset($argv[$ii]);
+ }
+}
+$argv = array_values($argv);
+$argc = count($argv);
+
if ($argc != 2) {
$self = basename($argv[0]);
echo "usage: {$self} <phutil_library_root>\n";
exit(1);
}
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'filesystem/filefinder');
phutil_require_module('phutil', 'future/exec');
$root = Filesystem::resolvePath($argv[1]);
if (!@file_exists($root.'/__phutil_library_init__.php')) {
throw new Exception("Provided path is not a phutil library.");
}
+if ($liberate_mode) {
+ ob_start();
+}
+
echo "Finding phutil modules...\n";
$files = id(new FileFinder($root))
->withType('f')
->withSuffix('php')
->excludePath('*/.*')
->setGenerateChecksums(true)
->find();
// NOTE: Sorting by filename ensures that hash computation is stable; it is
// important we sort by name instead of by hash because sorting by hash could
// create a bad cache hit if the user swaps the contents of two files.
ksort($files);
$modules = array();
foreach ($files as $file => $hash) {
if (dirname($file) == $root) {
continue;
}
$modules[Filesystem::readablePath(dirname($file), $root)][] = $hash;
}
echo "Found ".count($files)." files in ".count($modules)." modules.\n";
$signatures = array();
foreach ($modules as $module => $hashes) {
$hashes = implode(' ', $hashes);
$signature = md5($hashes);
$signatures[$module] = $signature;
}
try {
$cache = Filesystem::readFile($root.'/.phutil_module_cache');
} catch (Exception $ex) {
$cache = null;
}
$signature_cache = array();
if ($cache) {
$signature_cache = json_decode($cache, true);
if (!is_array($signature_cache)) {
$signature_cache = array();
}
}
$specs = array();
-$futures = array();
+$need_update = array();
foreach ($signatures as $module => $signature) {
if (isset($signature_cache[$module]) &&
$signature_cache[$module]['signature'] == $signature) {
$specs[$module] = $signature_cache[$module];
} else {
- $futures[$module] = new ExecFuture(
- '%s %s',
- dirname(__FILE__).'/phutil_analyzer.php',
- $root.'/'.$module);
+ $need_update[$module] = true;
}
}
+$futures = array();
+foreach ($need_update as $module => $ignored) {
+ $futures[$module] = new ExecFuture(
+ '%s %s',
+ dirname(__FILE__).'/phutil_analyzer.php',
+ $root.'/'.$module);
+}
+
if ($futures) {
echo "Found ".count($specs)." modules in cache; ".
"analyzing ".count($futures)." modified modules";
foreach (Futures($futures)->limit(8) as $module => $future) {
echo ".";
$specs[$module] = array(
'signature' => $signatures[$module],
'spec' => $future->resolveJSON(),
);
}
echo "\n";
} else {
echo "All modules were found in cache.\n";
}
$class_map = array();
$requires_class_map = array();
$requires_interface_map = array();
$function_map = array();
foreach ($specs as $module => $info) {
$spec = $info['spec'];
foreach (array('class', 'interface') as $type) {
foreach ($spec['declares'][$type] as $class => $where) {
if (!empty($class_map[$class])) {
$prior = $class_map[$class];
echo "\n";
echo "Error: definition of {$type} '{$class}' in module '{$module}' ".
"duplicates prior definition in module '{$prior}'.";
echo "\n";
exit(1);
}
$class_map[$class] = $module;
}
}
if (!empty($spec['chain']['class'])) {
$requires_class_map += $spec['chain']['class'];
}
if (!empty($spec['chain']['interface'])) {
$requires_interface_map += $spec['chain']['interface'];
}
foreach ($spec['declares']['function'] as $function => $where) {
if (!empty($function_map[$function])) {
$prior = $function_map[$function];
echo "\n";
echo "Error: definition of function '{$function}' in module '{$module}' ".
"duplicates prior definition in module '{$prior}'.";
echo "\n";
exit(1);
}
$function_map[$function] = $module;
}
}
echo "\n";
ksort($class_map);
ksort($requires_class_map);
ksort($requires_interface_map);
ksort($function_map);
$library_map = array(
'class' => $class_map,
'function' => $function_map,
'requires_class' => $requires_class_map,
'requires_interface' => $requires_interface_map,
);
$library_map = var_export($library_map, $return_string = true);
$library_map = preg_replace('/\s+$/m', '', $library_map);
$library_map = preg_replace('/array \(/', 'array(', $library_map);
$at = '@';
$map_file = <<<EOPHP
<?php
/**
* This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
* {$at}generated
*/
phutil_register_library_map({$library_map});
EOPHP;
echo "Writing library map file...\n";
Filesystem::writeFile($root.'/__phutil_library_map__.php', $map_file);
+if ($liberate_mode) {
+ ob_get_clean();
+ echo json_encode(array_keys($need_update))."\n";
+ return;
+}
+
echo "Writing module cache...\n";
Filesystem::writeFile(
$root.'/.phutil_module_cache',
json_encode($specs));
echo "Done.\n";
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 848a4575..cf5667c8 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,129 +1,133 @@
<?php
/**
* This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
* @generated
*/
phutil_register_library_map(array(
'class' =>
array(
'ArcanistAmendWorkflow' => 'workflow/amend',
'ArcanistApacheLicenseLinter' => 'lint/linter/apachelicense',
'ArcanistApacheLicenseLinterTestCase' => 'lint/linter/apachelicense/__tests__',
'ArcanistBaseUnitTestEngine' => 'unit/engine/base',
'ArcanistBaseWorkflow' => 'workflow/base',
'ArcanistBundle' => 'parser/bundle',
'ArcanistCallConduitWorkflow' => 'workflow/call-conduit',
'ArcanistChooseInvalidRevisionException' => 'exception',
'ArcanistChooseNoRevisionsException' => 'exception',
'ArcanistCommitWorkflow' => 'workflow/commit',
'ArcanistConfiguration' => 'configuration',
'ArcanistCoverWorkflow' => 'workflow/cover',
'ArcanistDiffChange' => 'parser/diff/change',
'ArcanistDiffChangeType' => 'parser/diff/changetype',
'ArcanistDiffHunk' => 'parser/diff/hunk',
'ArcanistDiffParser' => 'parser/diff',
'ArcanistDiffParserTestCase' => 'parser/diff/__tests__',
'ArcanistDiffUtils' => 'difference',
'ArcanistDiffWorkflow' => 'workflow/diff',
'ArcanistDifferentialCommitMessage' => 'differential/commitmessage',
'ArcanistDifferentialCommitMessageParserException' => 'differential/commitmessage',
'ArcanistDifferentialRevisionRef' => 'differential/revision',
'ArcanistExportWorkflow' => 'workflow/export',
'ArcanistFilenameLinter' => 'lint/linter/filename',
'ArcanistGeneratedLinter' => 'lint/linter/generated',
'ArcanistGitAPI' => 'repository/api/git',
'ArcanistGitHookPreReceiveWorkflow' => 'workflow/git-hook-pre-receive',
'ArcanistHelpWorkflow' => 'workflow/help',
+ 'ArcanistLiberateLintEngine' => 'lint/engine/liberate',
+ 'ArcanistLiberateWorkflow' => 'workflow/liberate',
'ArcanistLicenseLinter' => 'lint/linter/license',
'ArcanistLintEngine' => 'lint/engine/base',
'ArcanistLintJSONRenderer' => 'lint/renderer',
'ArcanistLintMessage' => 'lint/message',
'ArcanistLintPatcher' => 'lint/patcher',
'ArcanistLintRenderer' => 'lint/renderer',
'ArcanistLintResult' => 'lint/result',
'ArcanistLintSeverity' => 'lint/severity',
'ArcanistLintSummaryRenderer' => 'lint/renderer',
'ArcanistLintWorkflow' => 'workflow/lint',
'ArcanistLinter' => 'lint/linter/base',
'ArcanistLinterTestCase' => 'lint/linter/base/test',
'ArcanistListWorkflow' => 'workflow/list',
'ArcanistMarkCommittedWorkflow' => 'workflow/mark-committed',
'ArcanistNoEffectException' => 'exception/usage/noeffect',
'ArcanistNoEngineException' => 'exception/usage/noengine',
'ArcanistNoLintLinter' => 'lint/linter/nolint',
'ArcanistNoLintTestCaseMisnamed' => 'lint/linter/nolint/__tests__',
'ArcanistPEP8Linter' => 'lint/linter/pep8',
'ArcanistPatchWorkflow' => 'workflow/patch',
'ArcanistPhutilModuleLinter' => 'lint/linter/phutilmodule',
'ArcanistPhutilTestCase' => 'unit/engine/phutil/testcase',
'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/exception',
'ArcanistRepositoryAPI' => 'repository/api/base',
'ArcanistShellCompleteWorkflow' => 'workflow/shell-complete',
'ArcanistSubversionAPI' => 'repository/api/subversion',
'ArcanistSvnHookPreCommitWorkflow' => 'workflow/svn-hook-pre-commit',
'ArcanistTextLinter' => 'lint/linter/text',
'ArcanistTextLinterTestCase' => 'lint/linter/text/__tests__',
'ArcanistUnitTestResult' => 'unit/result',
'ArcanistUnitWorkflow' => 'workflow/unit',
'ArcanistUsageException' => 'exception/usage',
'ArcanistUserAbortException' => 'exception/usage/userabort',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity',
'ArcanistXHPASTLinter' => 'lint/linter/xhpast',
'ArcanistXHPASTLinterTestCase' => 'lint/linter/xhpast/__tests__',
'PhutilLintEngine' => 'lint/engine/phutil',
'PhutilModuleRequirements' => 'parser/phutilmodule',
'PhutilUnitTestEngine' => 'unit/engine/phutil',
'PhutilUnitTestEngineTestCase' => 'unit/engine/phutil/__tests__',
'UnitTestableArcanistLintEngine' => 'lint/engine/test',
),
'function' =>
array(
),
'requires_class' =>
array(
'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter',
'ArcanistApacheLicenseLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDiffParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinter' => 'ArcanistLinter',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHelpWorkflow' => 'ArcanistBaseWorkflow',
+ 'ArcanistLiberateLintEngine' => 'ArcanistLintEngine',
+ 'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLicenseLinter' => 'ArcanistLinter',
'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException',
'ArcanistNoLintLinter' => 'ArcanistLinter',
'ArcanistNoLintTestCaseMisnamed' => 'ArcanistLinterTestCase',
'ArcanistPEP8Linter' => 'ArcanistLinter',
'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPhutilModuleLinter' => 'ArcanistLinter',
'ArcanistShellCompleteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI',
'ArcanistSvnHookPreCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTextLinter' => 'ArcanistLinter',
'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUserAbortException' => 'ArcanistUsageException',
'ArcanistXHPASTLinter' => 'ArcanistLinter',
'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase',
'PhutilLintEngine' => 'ArcanistLintEngine',
'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilUnitTestEngineTestCase' => 'ArcanistPhutilTestCase',
'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine',
),
'requires_interface' =>
array(
),
));
diff --git a/src/lint/engine/base/ArcanistLintEngine.php b/src/lint/engine/base/ArcanistLintEngine.php
index 472d4350..6cae48f6 100644
--- a/src/lint/engine/base/ArcanistLintEngine.php
+++ b/src/lint/engine/base/ArcanistLintEngine.php
@@ -1,237 +1,237 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Manages lint execution.
*
* @group lint
*/
abstract class ArcanistLintEngine {
protected $workingCopy;
protected $fileData = array();
protected $charToLine = array();
protected $lineToFirstChar = array();
private $results = array();
private $minimumSeverity = null;
private $changedLines = array();
private $commitHookMode = false;
public function __construct() {
}
public function setWorkingCopy(ArcanistWorkingCopyIdentity $working_copy) {
$this->workingCopy = $working_copy;
return $this;
}
public function getWorkingCopy() {
return $this->workingCopy;
}
public function setPaths($paths) {
$this->paths = $paths;
return $this;
}
public function getPaths() {
return $this->paths;
}
public function setPathChangedLines($path, array $changed) {
$this->changedLines[$path] = array_fill_keys($changed, true);
return $this;
}
public function getPathChangedLines($path) {
return idx($this->changedLines, $path);
}
public function setFileData($data) {
$this->fileData = $data + $this->fileData;
return $this;
}
public function setCommitHookMode($mode) {
$this->commitHookMode = $mode;
return $this;
}
protected function loadData($path) {
if (!isset($this->fileData[$path])) {
$disk_path = $this->getFilePathOnDisk($path);
$this->fileData[$path] = Filesystem::readFile($disk_path);
}
return $this->fileData[$path];
}
public function pathExists($path) {
if ($this->getCommitHookMode()) {
return (idx($this->fileData, $path) !== null);
} else {
$disk_path = $this->getFilePathOnDisk($path);
return Filesystem::pathExists($disk_path);
}
}
public function getFilePathOnDisk($path) {
return Filesystem::resolvePath(
$path,
$this->getWorkingCopy()->getProjectRoot());
}
public function setMinimumSeverity($severity) {
$this->minimumSeverity = $severity;
return $this;
}
public function getCommitHookMode() {
return $this->commitHookMode;
}
public function run() {
$stopped = array();
$linters = $this->buildLinters();
if (!$linters) {
throw new ArcanistNoEffectException("No linters to run.");
}
$have_paths = false;
foreach ($linters as $linter) {
if ($linter->getPaths()) {
$have_paths = true;
break;
}
}
if (!$have_paths) {
throw new ArcanistNoEffectException("No paths are lintable.");
}
foreach ($linters as $linter) {
$linter->setEngine($this);
$paths = $linter->getPaths();
foreach ($paths as $key => $path) {
// Make sure each path has a result generated, even if it is empty
// (i.e., the file has no lint messages).
$result = $this->getResultForPath($path);
if (isset($stopped[$path])) {
unset($paths[$key]);
}
}
$paths = array_values($paths);
if ($paths) {
$linter->willLintPaths($paths);
foreach ($paths as $path) {
$linter->willLintPath($path);
$linter->lintPath($path);
if ($linter->didStopAllLinters()) {
$stopped[$path] = true;
}
}
}
$minimum = $this->minimumSeverity;
foreach ($linter->getLintMessages() as $message) {
if (!ArcanistLintSeverity::isAtLeastAsSevere($message, $minimum)) {
continue;
}
// When a user runs "arc diff", we default to raising only warnings on
// lines they have changed (errors are still raised anywhere in the
// file).
$changed = $this->getPathChangedLines($message->getPath());
if ($changed !== null && !$message->isError()) {
if (empty($changed[$message->getLine()])) {
continue;
}
}
$result = $this->getResultForPath($message->getPath());
$result->addMessage($message);
}
}
foreach ($this->results as $path => $result) {
$disk_path = $this->getFilePathOnDisk($path);
$result->setFilePathOnDisk($disk_path);
if (isset($this->fileData[$path])) {
$result->setData($this->fileData[$path]);
} else if ($disk_path && Filesystem::pathExists($disk_path)) {
// TODO: this may cause us to, e.g., load a large binary when we only
// raised an error about its filename. We could refine this by looking
// through the lint messages and doing this load only if any of them
// have original/replacement text or something like that.
try {
- $this->fileData[$path] = Filesystem::readFile($path);
+ $this->fileData[$path] = Filesystem::readFile($disk_path);
$result->setData($this->fileData[$path]);
} catch (FilesystemException $ex) {
// Ignore this, it's noncritical that we access this data and it
// might be unreadable or a directory or whatever else for plenty
// of legitimate reasons.
}
}
}
return $this->results;
}
abstract protected function buildLinters();
private function getResultForPath($path) {
if (empty($this->results[$path])) {
$result = new ArcanistLintResult();
$result->setPath($path);
$this->results[$path] = $result;
}
return $this->results[$path];
}
public function getLineAndCharFromOffset($path, $offset) {
if (!isset($this->charToLine[$path])) {
$char_to_line = array();
$line_to_first_char = array();
$lines = explode("\n", $this->loadData($path));
$line_number = 0;
$line_start = 0;
foreach ($lines as $line) {
$len = strlen($line) + 1; // Account for "\n".
$line_to_first_char[] = $line_start;
$line_start += $len;
for ($ii = 0; $ii < $len; $ii++) {
$char_to_line[] = $line_number;
}
$line_number++;
}
$this->charToLine[$path] = $char_to_line;
$this->lineToFirstChar[$path] = $line_to_first_char;
}
$line = $this->charToLine[$path][$offset];
$char = $offset - $this->lineToFirstChar[$path][$line];
return array($line, $char);
}
}
diff --git a/src/lint/engine/liberate/ArcanistLiberateLintEngine.php b/src/lint/engine/liberate/ArcanistLiberateLintEngine.php
new file mode 100644
index 00000000..111ad43a
--- /dev/null
+++ b/src/lint/engine/liberate/ArcanistLiberateLintEngine.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Lint engine which powers 'arc liberate'.
+ *
+ * @group linter
+ */
+class ArcanistLiberateLintEngine extends ArcanistLintEngine {
+
+ public function buildLinters() {
+ // We just run the module linter, 'arc liberate' is only interested in
+ // building __init__.php files.
+
+ $module_linter = new ArcanistPhutilModuleLinter();
+ foreach ($this->getPaths() as $path) {
+ $module_linter->addPath($path);
+ }
+
+ return array($module_linter);
+ }
+
+}
diff --git a/src/lint/engine/liberate/__init__.php b/src/lint/engine/liberate/__init__.php
new file mode 100644
index 00000000..0fe14a7b
--- /dev/null
+++ b/src/lint/engine/liberate/__init__.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('arcanist', 'lint/engine/base');
+phutil_require_module('arcanist', 'lint/linter/phutilmodule');
+
+
+phutil_require_source('ArcanistLiberateLintEngine.php');
diff --git a/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php b/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
index 62f469e0..b8d56980 100644
--- a/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
+++ b/src/lint/linter/phutilmodule/ArcanistPhutilModuleLinter.php
@@ -1,525 +1,529 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Applies rules for modules in Phutil libraries.
*
* @group linter
*/
class ArcanistPhutilModuleLinter extends ArcanistLinter {
const LINT_UNDECLARED_CLASS = 1;
const LINT_UNDECLARED_FUNCTION = 2;
const LINT_UNDECLARED_INTERFACE = 3;
const LINT_UNDECLARED_SOURCE = 4;
const LINT_UNUSED_MODULE = 5;
const LINT_UNUSED_SOURCE = 6;
const LINT_INIT_REBUILD = 7;
const LINT_UNKNOWN_CLASS = 8;
const LINT_UNKNOWN_FUNCTION = 9;
const LINT_ANALYZER_SIGNATURE = 100;
const LINT_ANALYZER_DYNAMIC = 101;
const LINT_ANALYZER_NO_INIT = 102;
const LINT_ANALYZER_MULTIPLE_CLASSES = 103;
public function getLintNameMap() {
return array(
self::LINT_UNDECLARED_CLASS => 'Use of Undeclared Class',
self::LINT_UNDECLARED_FUNCTION => 'Use of Undeclared Function',
self::LINT_UNDECLARED_INTERFACE => 'Use of Undeclared Interface',
self::LINT_UNDECLARED_SOURCE => 'Use of Nonexistent File',
self::LINT_UNUSED_SOURCE => 'Unused Source',
self::LINT_UNUSED_MODULE => 'Unused Module',
self::LINT_INIT_REBUILD => 'Rebuilt __init__.php File',
self::LINT_UNKNOWN_CLASS => 'Unknown Class',
self::LINT_UNKNOWN_FUNCTION => 'Unknown Function',
self::LINT_ANALYZER_SIGNATURE => 'Analyzer: Bad Call Signature',
self::LINT_ANALYZER_DYNAMIC => 'Analyzer: Dynamic Dependency',
self::LINT_ANALYZER_NO_INIT => 'Analyzer: No __init__.php File',
self::LINT_ANALYZER_MULTIPLE_CLASSES
=> 'Analyzer: File Declares Multiple Classes',
);
}
public function getLinterName() {
return 'PHU';
}
public function getLintSeverityMap() {
return array(
self::LINT_ANALYZER_DYNAMIC => ArcanistLintSeverity::SEVERITY_WARNING,
);
}
private $moduleInfo = array();
private $unknownClasses = array();
private $unknownFunctions = array();
private function setModuleInfo($key, array $info) {
$this->moduleInfo[$key] = $info;
}
private function getModulePathOnDisk($key) {
$info = $this->moduleInfo[$key];
return $info['root'].'/'.$info['module'];
}
private function getModuleDisplayName($key) {
$info = $this->moduleInfo[$key];
return $info['module'];
}
private function isPhutilLibraryMetadata($path) {
$file = basename($path);
return !strncmp('__phutil_library_', $file, strlen('__phutil_library_'));
}
public function willLintPaths(array $paths) {
if ($paths) {
if (!xhpast_is_available()) {
throw new Exception(xhpast_get_build_instructions());
}
}
$modules = array();
$moduleinfo = array();
$project_root = $this->getEngine()->getWorkingCopy()->getProjectRoot();
foreach ($paths as $path) {
$absolute_path = $project_root.'/'.$path;
$library_root = phutil_get_library_root_for_path($absolute_path);
if (!$library_root) {
continue;
}
if ($this->isPhutilLibraryMetadata($path)) {
continue;
}
$library_name = phutil_get_library_name_for_root($library_root);
if (!is_dir($path)) {
$path = dirname($path);
}
$path = Filesystem::resolvePath(
$path,
$project_root);
if ($path == $library_root) {
continue;
}
$module_name = Filesystem::readablePath($path, $library_root);
$module_key = $library_name.':'.$module_name;
if (empty($modules[$module_key])) {
$modules[$module_key] = $module_key;
$this->setModuleInfo($module_key, array(
'library' => $library_name,
'root' => $library_root,
'module' => $module_name,
));
}
}
if (!$modules) {
return;
}
$modules = array_keys($modules);
$arc_root = phutil_get_library_root('arcanist');
$bin = dirname($arc_root).'/scripts/phutil_analyzer.php';
$futures = array();
foreach ($modules as $mkey => $key) {
$disk_path = $this->getModulePathOnDisk($key);
if (Filesystem::pathExists($disk_path)) {
$futures[$key] = new ExecFuture(
'%s %s',
$bin,
$disk_path);
} else {
// This can occur in git when you add a module in HEAD and then remove
// it in unstaged changes in the working copy. Just ignore it.
unset($modules[$mkey]);
}
}
$requirements = array();
- foreach (Futures($futures) as $key => $future) {
+ foreach (Futures($futures)->limit(16) as $key => $future) {
$requirements[$key] = $future->resolveJSON();
}
$dependencies = array();
$futures = array();
foreach ($requirements as $key => $requirement) {
foreach ($requirement['messages'] as $message) {
list($where, $text, $code, $description) = $message;
if ($where) {
$where = array($where);
}
$this->raiseLintInModule(
$key,
$code,
$description,
$where,
$text);
}
foreach ($requirement['requires']['module'] as $req_module => $where) {
if (isset($requirements[$req_module])) {
$dependencies[$req_module] = $requirements[$req_module];
} else {
list($library_name, $module_name) = explode(':', $req_module);
$library_root = phutil_get_library_root($library_name);
$this->setModuleInfo($req_module, array(
'library' => $library_name,
'root' => $library_root,
'module' => $module_name,
));
$disk_path = $this->getModulePathOnDisk($req_module);
if (Filesystem::pathExists($disk_path)) {
$futures[$req_module] = new ExecFuture(
'%s %s',
$bin,
$disk_path);
} else {
$dependencies[$req_module] = array();
}
}
}
}
- foreach (Futures($futures) as $key => $future) {
+ foreach (Futures($futures)->limit(16) as $key => $future) {
$dependencies[$key] = $future->resolveJSON();
}
foreach ($requirements as $key => $spec) {
$deps = array_intersect_key(
$dependencies,
$spec['requires']['module']);
$this->lintModule($key, $spec, $deps);
}
}
private function lintModule($key, $spec, $deps) {
$resolvable = array();
$need_classes = array();
$need_functions = array();
$drop_modules = array();
$used = array();
static $types = array(
'class' => self::LINT_UNDECLARED_CLASS,
'interface' => self::LINT_UNDECLARED_INTERFACE,
'function' => self::LINT_UNDECLARED_FUNCTION,
);
foreach ($types as $type => $lint_code) {
foreach ($spec['requires'][$type] as $name => $places) {
$declared = $this->checkDependency(
$type,
$name,
$deps);
if (!$declared) {
$module = $this->getModuleDisplayName($key);
$message = $this->raiseLintInModule(
$key,
$lint_code,
"Module '{$module}' uses {$type} '{$name}' but does not include ".
"any module which declares it.",
$places);
if ($type == 'class' || $type == 'interface') {
$loader = new PhutilSymbolLoader();
$loader->setType($type);
$loader->setName($name);
$symbols = $loader->selectSymbolsWithoutLoading();
if ($symbols) {
$class_spec = reset($symbols);
try {
$loader->selectAndLoadSymbols();
$loaded = true;
} catch (PhutilMissingSymbolException $ex) {
$loaded = false;
} catch (PhutilBootloaderException $ex) {
$loaded = false;
}
if ($loaded) {
$resolvable[] = $message;
$need_classes[$name] = $class_spec;
} else {
if (empty($this->unknownClasses[$name])) {
$this->unknownClasses[$name] = true;
$library = $class_spec['library'];
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_CLASS,
"Class '{$name}' exists in the library map for library ".
"'{$library}', but could not be loaded. You may need to ".
"rebuild the library map.",
$places);
}
}
} else {
if (empty($this->unknownClasses[$name])) {
$this->unknownClasses[$name] = true;
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_CLASS,
"Class '{$name}' could not be found in any known library. ".
"You may need to rebuild the map for the library which ".
"contains it.",
$places);
}
}
} else {
$loader = new PhutilSymbolLoader();
$loader->setType($type);
$loader->setName($name);
$symbols = $loader->selectSymbolsWithoutLoading();
if ($symbols) {
$func_spec = reset($symbols);
try {
$loader->selectAndLoadSymbols();
$loaded = true;
} catch (PhutilMissingSymbolException $ex) {
$loaded = false;
} catch (PhutilBootloaderException $ex) {
$loaded = false;
}
if ($loaded) {
$resolvable[] = $message;
$need_functions[$name] = $func_spec;
} else {
if (empty($this->unknownFunctions[$name])) {
$this->unknownFunctions[$name] = true;
$library = $func_spec['library'];
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_FUNCTION,
"Function '{$name}' exists in the library map for library ".
"'{$library}', but could not be loaded. You may need to ".
"rebuild the library map.",
$places);
}
}
} else {
if (empty($this->unknownFunctions[$name])) {
$this->unknownFunctions[$name] = true;
$this->raiseLintInModule(
$key,
self::LINT_UNKNOWN_FUNCTION,
"Function '{$name}' could not be found in any known ".
"library. You may need to rebuild the map for the library ".
"which contains it.",
$places);
}
}
}
}
$used[$declared] = true;
}
}
$unused = array_diff_key($deps, $used);
foreach ($unused as $unused_module_key => $ignored) {
$module = $this->getModuleDisplayName($key);
$unused_module = $this->getModuleDisplayName($unused_module_key);
$resolvable[] = $this->raiseLintInModule(
$key,
self::LINT_UNUSED_MODULE,
"Module '{$module}' requires module '{$unused_module}' but does not ".
"use anything it declares.",
$spec['requires']['module'][$unused_module_key]);
$drop_modules[] = $unused_module_key;
}
foreach ($spec['requires']['source'] as $file => $where) {
if (empty($spec['declares']['source'][$file])) {
$module = $this->getModuleDisplayName($key);
$resolvable[] = $this->raiseLintInModule(
$key,
self::LINT_UNDECLARED_SOURCE,
"Module '{$module}' requires source '{$file}', but it does not ".
"exist.",
$where);
}
}
foreach ($spec['declares']['source'] as $file => $ignored) {
if (empty($spec['requires']['source'][$file])) {
$module = $this->getModuleDisplayName($key);
$resolvable[] = $this->raiseLintInModule(
$key,
self::LINT_UNUSED_SOURCE,
"Module '{$module}' does not include source file '{$file}'.",
null);
}
}
if ($resolvable) {
$new_file = $this->buildNewModuleInit(
$key,
$spec,
$need_classes,
$need_functions,
$drop_modules);
$init_path = $this->getModulePathOnDisk($key).'/__init__.php';
- $try_path = Filesystem::readablePath($init_path);
- if (Filesystem::pathExists($try_path)) {
+
+ $root = $this->getEngine()->getWorkingCopy()->getProjectRoot();
+ $try_path = Filesystem::readablePath($init_path, $root);
+ $full_path = Filesystem::resolvePath($try_path, $root);
+ if (Filesystem::pathExists($full_path)) {
$init_path = $try_path;
- $old_file = Filesystem::readFile($init_path);
+ $old_file = Filesystem::readFile($full_path);
} else {
$old_file = '';
}
+
$this->willLintPath($init_path);
$message = $this->raiseLintAtOffset(
null,
self::LINT_INIT_REBUILD,
"This generated phutil '__init__.php' file is suggested to address ".
"lint problems with static dependencies in the module.",
$old_file,
$new_file);
$message->setDependentMessages($resolvable);
foreach ($resolvable as $message) {
$message->setObsolete(true);
}
$message->setGenerateFile(true);
}
}
private function buildNewModuleInit(
$key,
$spec,
$need_classes,
$need_functions,
$drop_modules) {
$init = array();
$init[] = '<?php';
$at = '@';
$init[] = <<<EOHEADER
/**
* This file is automatically generated. Lint this module to rebuild it.
* {$at}generated
*/
EOHEADER;
$init[] = null;
$modules = $spec['requires']['module'];
foreach ($drop_modules as $drop) {
unset($modules[$drop]);
}
foreach ($need_classes as $need => $class_spec) {
$modules[$class_spec['library'].':'.$class_spec['module']] = true;
}
foreach ($need_functions as $need => $func_spec) {
$modules[$func_spec['library'].':'.$func_spec['module']] = true;
}
ksort($modules);
$last = null;
foreach ($modules as $module_key => $ignored) {
if (is_array($ignored)) {
$in_init = false;
$in_file = false;
foreach ($ignored as $where) {
list($file, $line) = explode(':', $where);
if ($file == '__init__.php') {
$in_init = true;
} else {
$in_file = true;
}
}
if ($in_file && !$in_init) {
// If this is a runtime include, don't try to put it in the
// __init__ file.
continue;
}
}
list($library, $module_name) = explode(':', $module_key);
if ($last != $library) {
$last = $library;
if ($last != null) {
$init[] = null;
}
}
$library = "'".addcslashes($library, "'\\")."'";
$module_name = "'".addcslashes($module_name, "'\\")."'";
$init[] = "phutil_require_module({$library}, {$module_name});";
}
$init[] = null;
$init[] = null;
$files = array_keys($spec['declares']['source']);
sort($files);
foreach ($files as $file) {
$file = "'".addcslashes($file, "'\\")."'";
$init[] = "phutil_require_source({$file});";
}
$init[] = null;
return implode("\n", $init);
}
private function checkDependency($type, $name, $deps) {
foreach ($deps as $key => $dep) {
if (isset($dep['declares'][$type][$name])) {
return $key;
}
}
return false;
}
public function raiseLintInModule($key, $code, $desc, $places, $text = null) {
if ($places) {
foreach ($places as $place) {
list($file, $offset) = explode(':', $place);
$this->willLintPath(
Filesystem::readablePath(
$this->getModulePathOnDisk($key).'/'.$file,
$this->getEngine()->getWorkingCopy()->getProjectRoot()));
return $this->raiseLintAtOffset(
$offset,
$code,
$desc,
$text);
}
} else {
$this->willLintPath($this->getModuleDisplayName($key));
return $this->raiseLintAtPath(
$code,
$desc);
}
}
public function lintPath($path) {
return;
}
}
diff --git a/src/workflow/help/ArcanistHelpWorkflow.php b/src/workflow/help/ArcanistHelpWorkflow.php
index 5d5d7760..d4421e82 100644
--- a/src/workflow/help/ArcanistHelpWorkflow.php
+++ b/src/workflow/help/ArcanistHelpWorkflow.php
@@ -1,177 +1,180 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Seduces the reader with majestic prose.
*
* @group workflow
*/
class ArcanistHelpWorkflow extends ArcanistBaseWorkflow {
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**help** [__command__]
Supports: english
Shows this help. With __command__, shows help about a specific
command.
EOTEXT
);
}
public function getArguments() {
return array(
'*' => 'command',
);
}
public function run() {
$arc_config = $this->getArcanistConfiguration();
$workflows = $arc_config->buildAllWorkflows();
ksort($workflows);
$target = null;
if ($this->getArgument('command')) {
$target = reset($this->getArgument('command'));
if (empty($workflows[$target])) {
throw new ArcanistUsageException(
"Unrecognized command '{$target}'. Try 'arc help'.");
}
}
$cmdref = array();
foreach ($workflows as $command => $workflow) {
if ($target && $target != $command) {
continue;
}
$optref = array();
$arguments = $workflow->getArguments();
$config_arguments = $arc_config->getCustomArgumentsForCommand($command);
// This juggling is to put the extension arguments after the normal
// arguments, and make sure the normal arguments aren't overwritten.
ksort($arguments);
ksort($config_arguments);
foreach ($config_arguments as $argument => $spec) {
if (empty($arguments[$argument])) {
$arguments[$argument] = $spec;
}
}
foreach ($arguments as $argument => $spec) {
if ($argument == '*') {
continue;
}
+ if (!empty($spec['hide'])) {
+ continue;
+ }
if (isset($spec['param'])) {
if (isset($spec['short'])) {
$optref[] = phutil_console_format(
" __--%s__ __%s__, __-%s__ __%s__",
$argument,
$spec['param'],
$spec['short'],
$spec['param']);
} else {
$optref[] = phutil_console_format(
" __--%s__ __%s__",
$argument,
$spec['param']);
}
} else {
if (isset($spec['short'])) {
$optref[] = phutil_console_format(
" __--%s__, __-%s__",
$argument,
$spec['short']);
} else {
$optref[] = phutil_console_format(
" __--%s__",
$argument);
}
}
if (isset($config_arguments[$argument])) {
$optref[] = " (This is a custom option for this ".
"project.)";
}
if (isset($spec['supports'])) {
$optref[] = " Supports: ".
implode(', ', $spec['supports']);
}
if (isset($spec['help'])) {
$docs = $spec['help'];
} else {
$docs = 'This option is not documented.';
}
$docs = phutil_console_wrap($docs, 14);
$optref[] = " {$docs}\n";
}
if ($optref) {
$optref = implode("\n", $optref);
$optref = "\n\n".$optref;
} else {
$optref = "\n";
}
$cmdref[] = $workflow->getCommandHelp().$optref;
}
$cmdref = implode("\n\n", $cmdref);
if ($target) {
echo "\n".$cmdref."\n";
return;
}
$self = 'arc';
echo phutil_console_format(<<<EOTEXT
**NAME**
**{$self}** - arcanist, a code review and revision management utility
**SYNOPSIS**
**{$self}** __command__ [__options__] [__args__]
This help file provides a detailed command reference.
**COMMAND REFERENCE**
{$cmdref}
**OPTION REFERENCE**
__--trace__
Debugging command. Shows underlying commands as they are executed,
and full stack traces when exceptions are thrown.
__--no-ansi__
Output in plain ASCII text only, without color or style.
__--load-phutil-library=/path/to/library__
Ignore libraries listed in .arcconfig and explicitly load specified
libraries instead. Mostly useful for Arcanist development.
__--conduit-uri=...__
Ignore configured Conduit URI and use an explicit one instead. Mostly
useful for Arcanist development.
EOTEXT
);
}
}
diff --git a/src/workflow/liberate/ArcanistLiberateWorkflow.php b/src/workflow/liberate/ArcanistLiberateWorkflow.php
new file mode 100644
index 00000000..645cee4c
--- /dev/null
+++ b/src/workflow/liberate/ArcanistLiberateWorkflow.php
@@ -0,0 +1,336 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Create and update libphutil libraries.
+ *
+ * This workflow is unusual and involves reexecuting 'arc liberate' as a
+ * subprocess with "--remap" and "--verify". This is because there is no way
+ * to unload or reload a library, so every process is stuck with the library
+ * definition it had when it first loaded. This is normally fine, but
+ * problematic in this case because 'arc liberate' modifies library definitions.
+ *
+ * @group workflow
+ */
+class ArcanistLiberateWorkflow extends ArcanistBaseWorkflow {
+
+ public function getCommandHelp() {
+ return phutil_console_format(<<<EOTEXT
+ **liberate** [__path__]
+ Supports: libphutil
+ Create or update a libphutil library, generating required metadata
+ files like __init__.php.
+EOTEXT
+ );
+ }
+
+ public function getArguments() {
+ return array(
+ 'all' => array(
+ 'help' =>
+ "Drop the module cache before liberating. This will completely ".
+ "reanalyze the entire library. Thorough, but slow!",
+ ),
+ 'force-update' => array(
+ 'help' =>
+ "Force the library map to be updated, even in the presence of ".
+ "lint errors.",
+ ),
+ 'remap' => array(
+ 'hide' => true,
+ 'help' =>
+ "Internal. Run the remap step of liberation. You do not need to ".
+ "run this unless you are debugging the workflow.",
+ ),
+ 'verify' => array(
+ 'hide' => true,
+ 'help' =>
+ "Internal. Run the verify step of liberation. You do not need to ".
+ "run this unless you are debugging the workflow.",
+ ),
+ '*' => 'argv',
+ );
+ }
+
+ public function run() {
+ $argv = $this->getArgument('argv');
+ if (count($argv) > 1) {
+ throw new ArcanistUsageException(
+ "Provide only one path to 'arc liberate'. The path should be a ".
+ "directory where you want to create or update a libphutil library.");
+ } else if (count($argv) == 0) {
+ $path = getcwd();
+ } else {
+ $path = reset($argv);
+ }
+
+ $is_remap = $this->getArgument('remap');
+ $is_verify = $this->getArgument('verify');
+
+ $path = Filesystem::resolvePath($path);
+
+ if (Filesystem::pathExists($path) && is_dir($path)) {
+ $init = id(new FileFinder($path))
+ ->withPath('*/__phutil_library_init__.php')
+ ->find();
+ } else {
+ $init = null;
+ }
+
+ if ($init) {
+ if (count($init) > 1) {
+ throw new ArcanistUsageException(
+ "Specified directory contains more than one libphutil library. Use ".
+ "a more specific path.");
+ }
+ $path = Filesystem::resolvePath(dirname(reset($init)), $path);
+ } else {
+ $found = false;
+ foreach (Filesystem::walkToRoot($path) as $dir) {
+ if (Filesystem::pathExists($dir.'/__phutil_library_init__.php')) {
+ $path = $dir;
+ break;
+ }
+ }
+ if (!$found) {
+ echo "No library currently exists at that path...\n";
+ $this->liberateCreateDirectory($path);
+ $this->liberateCreateLibrary($path);
+ }
+ }
+
+ if ($this->getArgument('remap')) {
+ return $this->liberateRunRemap($path);
+ }
+
+ if ($this->getArgument('verify')) {
+ return $this->liberateRunVerify($path);
+ }
+
+ $readable = Filesystem::readablePath($path);
+ echo "Using library root at '{$readable}'...\n";
+
+ if ($this->getArgument('all')) {
+ echo "Dropping module cache...\n";
+ Filesystem::remove($path.'/.phutil_module_cache');
+ }
+
+ echo "Mapping library...\n";
+
+ // Force a rebuild of the library map before running lint. The remap
+ // operation will load the map before regenerating it, so if a class has
+ // been renamed (say, from OldClass to NewClass) this rebuild will
+ // cause the initial remap to see NewClass and correctly remove includes
+ // caused by use of OldClass.
+ $this->liberateGetChangedPaths($path);
+
+ $arc_bin = $this->getScriptPath('bin/arc');
+
+ do {
+ $future = new ExecFuture(
+ '%s liberate --remap -- %s',
+ $arc_bin,
+ $path);
+ $wrote = $future->resolveJSON();
+ foreach ($wrote as $wrote_path) {
+ echo "Updated '{$wrote_path}'...\n";
+ }
+ } while ($wrote);
+
+ echo "Verifying library...\n";
+
+ $err = 0;
+ $cmd = csprintf('%s liberate --verify -- %s', $arc_bin, $path);
+ passthru($cmd, $err);
+
+ $do_update = (!$err || $this->getArgument('force-update'));
+
+ if ($do_update) {
+ echo "Finalizing library map...\n";
+ execx('%s %s', $this->getPhutilMapperLocation(), $path);
+ }
+
+ if ($err) {
+ if ($do_update) {
+ echo phutil_console_format(
+ "<bg:yellow>** WARNING **</bg> Library update forced, but lint ".
+ "failures remain.\n");
+ } else {
+ echo phutil_console_format(
+ "<bg:red>** UNRESOLVED LINT ERRORS **</bg> This library has ".
+ "unresolved lint failures. The library map was not updated. Use ".
+ "--force-update to force an update.\n");
+ }
+ } else {
+ echo phutil_console_format(
+ "<bg:green>** OKAY **</bg> Library updated.\n");
+ }
+
+ return $err;
+ }
+
+ private function liberateLintModules($path, array $changed) {
+ $engine = $this->liberateBuildLintEngine($path, $changed);
+ if ($engine) {
+ return $engine->run();
+ } else {
+ return array();
+ }
+ }
+
+ private function liberateWritePatches(array $results) {
+ $wrote = array();
+
+ foreach ($results as $result) {
+ if ($result->isPatchable()) {
+ $patcher = ArcanistLintPatcher::newFromArcanistLintResult($result);
+ $patcher->writePatchToDisk();
+ $wrote[] = $result->getPath();
+ }
+ }
+
+ return $wrote;
+ }
+
+ private function liberateBuildLintEngine($path, array $changed) {
+ $lint_map = array();
+ foreach ($changed as $module) {
+ $module_path = $path.'/'.$module;
+ $files = Filesystem::listDirectory($module_path);
+ $lint_map[$module] = $files;
+ }
+
+ $working_copy = ArcanistWorkingCopyIdentity::newFromRootAndConfigFile(
+ $path,
+ json_encode(
+ array(
+ 'project_id' => '__arcliberate__',
+ )),
+ 'arc liberate');
+
+ $engine = new ArcanistLiberateLintEngine();
+ $engine->setWorkingCopy($working_copy);
+
+ $lint_paths = array();
+ foreach ($lint_map as $module => $files) {
+ foreach ($files as $file) {
+ $lint_paths[] = $module.'/'.$file;
+ }
+ }
+
+ if (!$lint_paths) {
+ return null;
+ }
+
+ $engine->setPaths($lint_paths);
+ $engine->setMinimumSeverity(ArcanistLintSeverity::SEVERITY_ERROR);
+
+ return $engine;
+ }
+
+
+ private function liberateCreateDirectory($path) {
+ if (Filesystem::pathExists($path)) {
+ if (!is_dir($path)) {
+ throw new ArcanistUsageException(
+ "Provide a directory to create or update a libphutil library in.");
+ }
+ return;
+ }
+
+ echo "The directory '{$path}' does not exist.";
+ if (!phutil_console_confirm('Do you want to create it?')) {
+ throw new ArcanistUsageException("Cancelled.");
+ }
+
+ execx('mkdir -p %s', $path);
+ }
+
+ private function liberateCreateLibrary($path) {
+ $init_path = $path.'/__phutil_library_init__.php';
+ if (Filesystem::pathExists($init_path)) {
+ return;
+ }
+
+ echo "Creating new libphutil library in '{$path}'.\n";
+ echo "Choose a name for the new library.\n";
+ do {
+ $name = phutil_console_prompt('What do you want to name this library?');
+ if (preg_match('/^[a-z]+$/', $name)) {
+ break;
+ } else {
+ echo "Library name should contain only lowercase letters.\n";
+ }
+ } while (true);
+
+ $template =
+ "<?php\n\n".
+ "phutil_register_library('{$name}', __FILE__);\n";
+
+ echo "Writing '__phutil_library_init__.php' to '{$init_path}'...\n";
+ Filesystem::writeFile($init_path, $template);
+ }
+
+ private function liberateGetChangedPaths($path) {
+ $mapper = $this->getPhutilMapperLocation();
+ $future = new ExecFuture('%s %s --find-paths-for-liberate', $mapper, $path);
+ return $future->resolveJSON();
+ }
+
+ private function getScriptPath($script) {
+ $root = dirname(phutil_get_library_root('arcanist'));
+ return $root.'/'.$script;
+ }
+
+ private function getPhutilMapperLocation() {
+ return $this->getScriptPath('scripts/phutil_mapper.php');
+ }
+
+ private function liberateRunRemap($path) {
+ phutil_load_library($path);
+
+ $paths = $this->liberateGetChangedPaths($path);
+ $results = $this->liberateLintModules($path, $paths);
+ $wrote = $this->liberateWritePatches($results);
+
+ echo json_encode($wrote, true);
+
+ return 0;
+ }
+
+ private function liberateRunVerify($path) {
+ phutil_load_library($path);
+
+ $paths = $this->liberateGetChangedPaths($path);
+ $results = $this->liberateLintModules($path, $paths);
+
+ $renderer = new ArcanistLintRenderer();
+
+ $unresolved = false;
+ foreach ($results as $result) {
+ foreach ($result->getMessages() as $message) {
+ echo $renderer->renderLintResult($result);
+ $unresolved = true;
+ break;
+ }
+ }
+
+ return (int)$unresolved;
+ }
+
+}
diff --git a/src/workflow/liberate/__init__.php b/src/workflow/liberate/__init__.php
new file mode 100644
index 00000000..aab0b048
--- /dev/null
+++ b/src/workflow/liberate/__init__.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('arcanist', 'exception/usage');
+phutil_require_module('arcanist', 'lint/engine/liberate');
+phutil_require_module('arcanist', 'lint/patcher');
+phutil_require_module('arcanist', 'lint/renderer');
+phutil_require_module('arcanist', 'lint/severity');
+phutil_require_module('arcanist', 'workflow/base');
+phutil_require_module('arcanist', 'workingcopyidentity');
+
+phutil_require_module('phutil', 'console');
+phutil_require_module('phutil', 'filesystem');
+phutil_require_module('phutil', 'filesystem/filefinder');
+phutil_require_module('phutil', 'future/exec');
+phutil_require_module('phutil', 'moduleutils');
+phutil_require_module('phutil', 'utils');
+phutil_require_module('phutil', 'xsprintf/csprintf');
+
+
+phutil_require_source('ArcanistLiberateWorkflow.php');
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 13:17 (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
556919
Default Alt Text
(67 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment