1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-22 06:42:41 +01:00

Remove all libphutil v1 support

Summary:
Delete all code related to v1 libraries in arcanist.

When users liberate a v1 library, prompt them to upgrade.

Test Plan: Reverted phabricator/ to a couple of months ago and liberated it. Got prompted to upgrade. Upgraded.

Reviewers: vrana, btrahan

Reviewed By: vrana

CC: aran

Maniphest Tasks: T1103

Differential Revision: https://secure.phabricator.com/D2861
This commit is contained in:
epriestley 2012-06-26 12:40:42 -07:00
parent 5b1a00eab1
commit 67956306cb
6 changed files with 3 additions and 1393 deletions

View file

@ -1,409 +0,0 @@
#!/usr/bin/env php
<?php
/*
* Copyright 2012 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_filter(
array(
'empty' => true,
'isset' => true,
'echo' => true,
'print' => true,
'exit' => true,
'die' => true,
// These are provided by libphutil but not visible in the map.
'phutil_is_windows' => true,
'phutil_load_library' => true,
'phutil_is_hiphop_runtime' => true,
// HPHP/i defines these functions as 'internal', but they are NOT
// builtins and do not exist in vanilla PHP. Make sure we don't mark them
// as builtin since we need to add dependencies for them.
'idx' => false,
'id' => false,
) + array_fill_keys($builtin_functions, 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]);
$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);
$doc_parser = new PhutilDocblockParser();
$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, true);
}
}
} else {
$has_files = true;
$requirements->addSourceDeclaration(basename($file));
// Find symbols declared as "@phutil-external-symbol function example",
// and ignore these in building dependency lists.
$externals = array();
foreach ($root->getTokens() as $token) {
if ($token->getTypeName() == 'T_DOC_COMMENT') {
list($block, $special) = $doc_parser->parse($token->getValue());
$ext_list = idx($special, 'phutil-external-symbol');
$ext_list = explode("\n", $ext_list);
$ext_list = array_filter($ext_list);
foreach ($ext_list as $ext_ref) {
$matches = null;
if (preg_match('/^\s*(\S+)\s+(\S+)/', $ext_ref, $matches)) {
$externals[$matches[1]][$matches[2]] = true;
}
}
}
}
// 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, false);
} 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 if (empty($externals['function'][$name->getConcreteString()])) {
$requirements->addFunctionDependency(
$name,
$name->getConcreteString());
}
}
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
foreach ($functions as $function) {
$name = $function->getChildByIndex(2);
if ($name->getTypeName() == 'n_EMPTY') {
// This is an anonymous function; don't record it into the symbol
// index.
} else {
$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) {
if (empty($externals['class'][$parent->getConcreteString()])) {
$requirements->addClassDependency(
$class_name->getConcreteString(),
$parent,
$parent->getConcreteString());
}
}
$implements = $class->getChildByIndex(3);
$interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME');
foreach ($interfaces as $interface) {
if (empty($externals['interface'][$interface->getConcreteString()])) {
$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;
}
if (empty($externals['class'][$name->getConcreteString()])) {
$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;
}
if (empty($externals['class'][$name_concrete])) {
$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) {
if (empty($externals['interface'][$parent->getConcreteString()])) {
$requirements->addInterfaceDependency(
$interface_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,
$create_dependency) {
$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;
}
if ($create_dependency) {
$requirements->addModuleDependency(
$name,
$library_value.':'.$module_value);
}
}

View file

@ -1,210 +0,0 @@
#!/usr/bin/env php
<?php
/*
* Copyright 2012 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);
}
$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();
$need_update = array();
foreach ($signatures as $module => $signature) {
if (isset($signature_cache[$module]) &&
$signature_cache[$module]['signature'] == $signature) {
$specs[$module] = $signature_cache[$module];
} else {
$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";

View file

@ -63,7 +63,6 @@ phutil_register_library_map(array(
'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php', 'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php',
'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php', 'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php',
'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php', 'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php',
'ArcanistLiberateLintEngine' => 'lint/engine/ArcanistLiberateLintEngine.php',
'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php', 'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php',
'ArcanistLicenseLinter' => 'lint/linter/ArcanistLicenseLinter.php', 'ArcanistLicenseLinter' => 'lint/linter/ArcanistLicenseLinter.php',
'ArcanistLintConsoleRenderer' => 'lint/renderer/ArcanistLintConsoleRenderer.php', 'ArcanistLintConsoleRenderer' => 'lint/renderer/ArcanistLintConsoleRenderer.php',
@ -93,7 +92,6 @@ phutil_register_library_map(array(
'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php', 'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php',
'ArcanistPhpcsLinter' => 'lint/linter/ArcanistPhpcsLinter.php', 'ArcanistPhpcsLinter' => 'lint/linter/ArcanistPhpcsLinter.php',
'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php', 'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php',
'ArcanistPhutilModuleLinter' => 'lint/linter/ArcanistPhutilModuleLinter.php',
'ArcanistPhutilTestCase' => 'unit/engine/phutil/ArcanistPhutilTestCase.php', 'ArcanistPhutilTestCase' => 'unit/engine/phutil/ArcanistPhutilTestCase.php',
'ArcanistPhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php', 'ArcanistPhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php',
'ArcanistPhutilTestSkippedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php', 'ArcanistPhutilTestSkippedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php',
@ -178,7 +176,6 @@ phutil_register_library_map(array(
'ArcanistInstallCertificateWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistInstallCertificateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistJSHintLinter' => 'ArcanistLinter', 'ArcanistJSHintLinter' => 'ArcanistLinter',
'ArcanistLandWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistLandWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLiberateLintEngine' => 'ArcanistLintEngine',
'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLicenseLinter' => 'ArcanistLinter', 'ArcanistLicenseLinter' => 'ArcanistLinter',
'ArcanistLintConsoleRenderer' => 'ArcanistLintRenderer', 'ArcanistLintConsoleRenderer' => 'ArcanistLintRenderer',
@ -200,7 +197,6 @@ phutil_register_library_map(array(
'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPhpcsLinter' => 'ArcanistLinter', 'ArcanistPhpcsLinter' => 'ArcanistLinter',
'ArcanistPhutilLibraryLinter' => 'ArcanistLinter', 'ArcanistPhutilLibraryLinter' => 'ArcanistLinter',
'ArcanistPhutilModuleLinter' => 'ArcanistLinter',
'ArcanistPhutilTestCaseTestCase' => 'ArcanistPhutilTestCase', 'ArcanistPhutilTestCaseTestCase' => 'ArcanistPhutilTestCase',
'ArcanistPhutilTestSkippedException' => 'Exception', 'ArcanistPhutilTestSkippedException' => 'Exception',
'ArcanistPhutilTestTerminatedException' => 'Exception', 'ArcanistPhutilTestTerminatedException' => 'Exception',

View file

@ -1,38 +0,0 @@
<?php
/*
* Copyright 2012 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
*/
final 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);
}
}

View file

@ -1,536 +0,0 @@
<?php
/*
* Copyright 2012 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
*/
final 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;
return $this;
}
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();
$bootloader = PhutilBootloader::getInstance();
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);
$version = $bootloader->getLibraryFormatVersion($library_name);
if ($version != 1) {
continue;
}
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)->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)->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';
$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($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 $resolvable_message) {
$resolvable_message->setObsolete(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;
}
}

View file

@ -131,7 +131,9 @@ EOTEXT
if ($this->getArgument('upgrade')) { if ($this->getArgument('upgrade')) {
return $this->upgradeLibrary($path); return $this->upgradeLibrary($path);
} }
return $this->liberateVersion1($path); throw new ArcanistUsageException(
"This library is using libphutil v1, which is no longer supported. ".
"Run 'arc liberate --upgrade' to upgrade to v2.");
case 2: case 2:
if ($this->getArgument('upgrade')) { if ($this->getArgument('upgrade')) {
throw new ArcanistUsageException( throw new ArcanistUsageException(
@ -186,139 +188,6 @@ EOTEXT
$this->liberateVersion2($path); $this->liberateVersion2($path);
} }
private function liberateVersion1($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";
$this->checkForLooseFiles($path);
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 = phutil_passthru('%s liberate --verify -- %s', $arc_bin, $path);
$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) {
assert_instances_of($results, 'ArcanistLintResult');
$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) { private function liberateCreateDirectory($path) {
if (Filesystem::pathExists($path)) { if (Filesystem::pathExists($path)) {
if (!is_dir($path)) { if (!is_dir($path)) {
@ -361,72 +230,10 @@ EOTEXT
Filesystem::writeFile($init_path, $template); 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) { private function getScriptPath($script) {
$root = dirname(phutil_get_library_root('arcanist')); $root = dirname(phutil_get_library_root('arcanist'));
return $root.'/'.$script; 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 ArcanistLintConsoleRenderer();
$unresolved = false;
foreach ($results as $result) {
foreach ($result->getMessages() as $message) {
echo $renderer->renderLintResult($result);
$unresolved = true;
break;
}
}
return (int)$unresolved;
}
/**
* Sanity check to catch people putting class files in the root of a libphutil
* library.
*/
private function checkForLooseFiles($path) {
foreach (Filesystem::listDirectory($path) as $item) {
if (!preg_match('/\.php$/', $item)) {
// Not a ".php" file.
continue;
}
if (preg_match('/^__/', $item)) {
// Has magic '__' prefix.
continue;
}
echo phutil_console_wrap(
"WARNING: File '{$item}' is not in a module and won't be loaded. ".
"Put source files in subdirectories, not the top level directory.\n");
}
}
} }