Changeset View
Changeset View
Standalone View
Standalone View
scripts/phutil_symbols.php
Show First 20 Lines • Show All 70 Lines • ▼ Show 20 Lines | try { | ||||
$json = new PhutilJSON(); | $json = new PhutilJSON(); | ||||
echo $json->encodeFormatted($result); | echo $json->encodeFormatted($result); | ||||
exit(0); | exit(0); | ||||
} | } | ||||
$root = $tree->getRootNode(); | $root = $tree->getRootNode(); | ||||
$root->buildSelectCache(); | $root->buildSelectCache(); | ||||
// -( Unsupported Constructs )------------------------------------------------ | // -( Namespace and Use resolution )------------------------------------------ | ||||
$namespaces = $root->selectDescendantsOfType('n_NAMESPACE'); | $namespaces = $root->selectDescendantsOfType('n_NAMESPACE'); | ||||
$file_namespace = ''; | |||||
foreach ($namespaces as $namespace) { | foreach ($namespaces as $namespace) { | ||||
if ($file_namespace) { | |||||
// Make no attempt to support multiple namespaces per file - this is silly, | |||||
// overly complicated, and unused in practice | |||||
phutil_fail_on_unsupported_feature($namespace, $path, pht('namespaces')); | phutil_fail_on_unsupported_feature($namespace, $path, pht('namespaces')); | ||||
} else { | |||||
$file_namespace = $namespace->getChildByIndex(0)->getConcreteString().'\\'; | |||||
} | |||||
} | } | ||||
// Map imported classes as rule => class | |||||
$imports = array(); | |||||
$uses = $root->selectDescendantsOfType('n_USE'); | $uses = $root->selectDescendantsOfType('n_USE'); | ||||
foreach ($namespaces as $namespace) { | foreach ($uses as $use) { | ||||
phutil_fail_on_unsupported_feature( | if (2 !== count($use->getChildren())) { | ||||
$namespace, $path, pht('namespace `use` statements')); | // This is the "outer" T_USE statement; the actual contents will get picked | ||||
// up below | |||||
continue; | |||||
} | } | ||||
$import_from = $use->getChildByIndex(0)->getConcreteString(); | |||||
$possible_traits = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); | $import_as = $use->getChildByIndex(1)->getConcreteString(); | ||||
foreach ($possible_traits as $possible_trait) { | // If something is not explicitly imported with an alias, the alias is | ||||
$attributes = $possible_trait->getChildByIndex(0); | // implied to be the deepest sub-namespace. | ||||
// Can't use getChildByIndex here because not all classes have attributes | if (!$import_as) { | ||||
foreach ($attributes->getChildren() as $attribute) { | if (false === strpos($import_from, '\\')) { // Global NS | ||||
if (strtolower($attribute->getConcreteString()) === 'trait') { | $import_as = $import_from; | ||||
phutil_fail_on_unsupported_feature($possible_trait, $path, pht('traits')); | } else { | ||||
$import_as = substr($import_from, strrpos($import_from, '\\') + 1); | |||||
} | } | ||||
} | } | ||||
$imports[$import_as] = $import_from; | |||||
} | } | ||||
// It's possible to explicitly request the current namespace, although it's | |||||
// implied by using anything other than a fully qualified name. Add it (without | |||||
// the trailing backslash) to the import rules. In effect: | |||||
// namespace Foo\Bar; | |||||
// use Foo\Bar as namespace; | |||||
// As a result, the two are functionally identical: | |||||
// new namespace\Foo(); | |||||
// new Foo(); | |||||
$imports['namespace'] = substr($file_namespace, 0, -1); | |||||
// TODO: "use function foo" | |||||
Firehed: Note: these are currently unsupported by XHPAST, as it hasn't pulled in the 5.6 syntax… | |||||
// TODO: "use function foo as bar" | |||||
// TODO: "use const FOO" | |||||
// -( Marked Externals )------------------------------------------------------ | // -( Marked Externals )------------------------------------------------------ | ||||
// Identify symbols marked with "@phutil-external-symbol", so we exclude them | // Identify symbols marked with "@phutil-external-symbol", so we exclude them | ||||
// from the dependency list. | // from the dependency list. | ||||
$externals = array(); | $externals = array(); | ||||
▲ Show 20 Lines • Show All 112 Lines • ▼ Show 20 Lines | |||||
// -( Classes )--------------------------------------------------------------- | // -( Classes )--------------------------------------------------------------- | ||||
// Find classes declared by this file. | // Find classes declared by this file. | ||||
// This is "class X ... { ... }". | // This is "class X ... { ... }" or "trait X ... { ... }". | ||||
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); | $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); | ||||
foreach ($classes as $class) { | foreach ($classes as $class) { | ||||
$class_attributes = $class->getChildByIndex(0); | |||||
$is_trait = false; | |||||
foreach ($class_attributes->getChildren() as $attribute) { | |||||
if (strtolower($attribute->getConcreteString() === 'trait')) { | |||||
$is_trait = true; | |||||
} | |||||
} | |||||
$class_name = $class->getChildByIndex(1); | $class_name = $class->getChildByIndex(1); | ||||
$type = $is_trait ? 'trait' : 'class'; | |||||
$have[] = array( | $have[] = array( | ||||
'type' => 'class', | 'type' => $type, | ||||
'symbol' => $class_name, | 'symbol' => $class_name, | ||||
); | ); | ||||
// Trait use in trait and class declaration | |||||
$trait_use_statements = $class->selectDescendantsOfType('n_TRAIT_USE'); | |||||
foreach ($trait_use_statements as $trait_use_statement) { | |||||
foreach ($trait_use_statement->getChildren() as $used_trait) { | |||||
// n_TRAIT_ADAPTATION_LIST - conflict resolution OR n_EMPTY | |||||
if ($used_trait->getTypeName() !== 'n_CLASS_NAME') { | |||||
continue; | |||||
} | |||||
$need[] = array( | |||||
'type' => 'trait', | |||||
'symbol' => $used_trait, | |||||
); | |||||
} | |||||
} | |||||
} | } | ||||
// Find classes used by this file. We identify these: | // Find classes used by this file. We identify these: | ||||
// | // | ||||
// - class ... extends X | // - class ... extends X | ||||
// - new X | // - new X | ||||
// - Static method call | // - Static method call | ||||
Show All 16 Lines | foreach ($classes as $class) { | ||||
$extends = $class->getChildByIndex(2); | $extends = $class->getChildByIndex(2); | ||||
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { | foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { | ||||
$need[] = array( | $need[] = array( | ||||
'type' => 'class', | 'type' => 'class', | ||||
'symbol' => $parent, | 'symbol' => $parent, | ||||
); | ); | ||||
// Track all 'extends' in the extension map. | // Track all 'extends' in the extension map. | ||||
$xmap[$class_name][] = $parent->getConcreteString(); | $xmap['class'][$class_name][] = array( | ||||
$parent->getConcreteString(), | |||||
'class', | |||||
); | |||||
} | } | ||||
} | } | ||||
// This is "new X()". | // This is "new X()". | ||||
$uses_of_new = $root->selectDescendantsOfType('n_NEW'); | $uses_of_new = $root->selectDescendantsOfType('n_NEW'); | ||||
foreach ($uses_of_new as $new_operator) { | foreach ($uses_of_new as $new_operator) { | ||||
$name = $new_operator->getChildByIndex(0); | $name = $new_operator->getChildByIndex(0); | ||||
if ($name->getTypeName() === 'n_VARIABLE' || | if ($name->getTypeName() === 'n_VARIABLE' || | ||||
▲ Show 20 Lines • Show All 112 Lines • ▼ Show 20 Lines | foreach ($classes as $class) { | ||||
$interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME'); | $interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME'); | ||||
foreach ($interfaces as $interface) { | foreach ($interfaces as $interface) { | ||||
$need[] = array( | $need[] = array( | ||||
'type' => 'interface', | 'type' => 'interface', | ||||
'symbol' => $interface, | 'symbol' => $interface, | ||||
); | ); | ||||
// Track 'class ... implements' in the extension map. | // Track 'class ... implements' in the extension map. | ||||
$xmap[$class_name][] = $interface->getConcreteString(); | $xmap['class'][$class_name][] = array( | ||||
$interface->getConcreteString(), | |||||
'interface', | |||||
); | |||||
} | } | ||||
} | } | ||||
// This is "interface X ... { ... }". | // This is "interface X ... { ... }". | ||||
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); | $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); | ||||
foreach ($interfaces as $interface) { | foreach ($interfaces as $interface) { | ||||
$interface_name = $interface->getChildByIndex(1)->getConcreteString(); | $interface_name = $interface->getChildByIndex(1)->getConcreteString(); | ||||
$extends = $interface->getChildByIndex(2); | $extends = $interface->getChildByIndex(2); | ||||
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { | foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) { | ||||
$need[] = array( | $need[] = array( | ||||
'type' => 'interface', | 'type' => 'interface', | ||||
'symbol' => $parent, | 'symbol' => $parent, | ||||
); | ); | ||||
// Track 'interface ... extends' in the extension map. | // Track 'interface ... extends' in the extension map. | ||||
$xmap[$interface_name][] = $parent->getConcreteString(); | $xmap['interface'][$interface_name][] = array( | ||||
$parent->getConcreteString(), | |||||
'interface', | |||||
); | |||||
} | } | ||||
} | } | ||||
// -( Analysis )-------------------------------------------------------------- | // -( Analysis )-------------------------------------------------------------- | ||||
$declared_symbols = array(); | $declared_symbols = array(); | ||||
foreach ($have as $key => $spec) { | foreach ($have as $key => $spec) { | ||||
$name = $spec['symbol']->getConcreteString(); | $name = $file_namespace.$spec['symbol']->getConcreteString(); | ||||
$declared_symbols[$spec['type']][$name] = $spec['symbol']->getOffset(); | $declared_symbols[$spec['type']][$name] = $spec['symbol']->getOffset(); | ||||
} | } | ||||
$required_symbols = array(); | $required_symbols = array(); | ||||
foreach ($need as $key => $spec) { | foreach ($need as $key => $spec) { | ||||
$name = idx($spec, 'name'); | $name = idx($spec, 'name'); | ||||
if (!$name) { | if (!$name) { | ||||
$name = $spec['symbol']->getConcreteString(); | $name = $spec['symbol']->getConcreteString(); | ||||
} | } | ||||
$type = $spec['type']; | $type = $spec['type']; | ||||
$name = phutil_resolve_namespace($name, $type, $file_namespace, $imports); | |||||
foreach (explode('/', $type) as $libtype) { | foreach (explode('/', $type) as $libtype) { | ||||
if (!$show_all) { | if (!$show_all) { | ||||
if (!empty($externals[$libtype][$name])) { | if (!empty($externals[$libtype][$name])) { | ||||
// Ignore symbols declared as externals. | // Ignore symbols declared as externals. | ||||
continue 2; | continue 2; | ||||
} | } | ||||
if (!empty($builtins[$libtype][$name])) { | if (!empty($builtins[$libtype][$name])) { | ||||
// Ignore symbols declared as builtins. | // Ignore symbols declared as builtins. | ||||
continue 2; | continue 2; | ||||
} | } | ||||
} | } | ||||
if (!empty($declared_symbols[$libtype][$name])) { | if (!empty($declared_symbols[$libtype][$name])) { | ||||
// We declare this symbol, so don't treat it as a requirement. | // We declare this symbol, so don't treat it as a requirement. | ||||
continue 2; | continue 2; | ||||
} | } | ||||
if ($libtype === 'function' && | |||||
!empty($declared_symbols[$libtype][$file_namespace.$name])) { | |||||
// Function calls can't be resolved except at runtime. If the file has | |||||
// a namespace, try the function in the current namespace. If found, the | |||||
// function was declared in this file (as above) and isn't a requirement. | |||||
continue 2; | |||||
} | |||||
} | } | ||||
if (!empty($required_symbols[$type][$name])) { | if (!empty($required_symbols[$type][$name])) { | ||||
// Report only the first use of a symbol, since reporting all of them | // Report only the first use of a symbol, since reporting all of them | ||||
// isn't terribly informative. | // isn't terribly informative. | ||||
continue; | continue; | ||||
} | } | ||||
$required_symbols[$type][$name] = $spec['symbol']->getOffset(); | $required_symbols[$type][$name] = $spec['symbol']->getOffset(); | ||||
} | } | ||||
$parsed_xmap = array(); | |||||
foreach ($xmap as $type => $contents) { | |||||
foreach ($contents as $declaration => $requirements) { | |||||
$parsed_declaration = phutil_resolve_namespace($declaration, | |||||
$type, | |||||
$file_namespace, | |||||
$imports); | |||||
foreach ($requirements as $requirement) { | |||||
list($req_name, $req_type) = $requirement; | |||||
$parsed_requirement = phutil_resolve_namespace($req_name, | |||||
$req_type, | |||||
$file_namespace, | |||||
$imports); | |||||
if (!empty($declared_symbols[$req_type][$parsed_requirement])) { | |||||
// Declared in this file, don't treat as a requirement. | |||||
continue; | |||||
} | |||||
$parsed_xmap[$parsed_declaration][] = $parsed_requirement; | |||||
} | |||||
} | |||||
} | |||||
$result = array( | $result = array( | ||||
'have' => $declared_symbols, | 'have' => $declared_symbols, | ||||
'need' => $required_symbols, | 'need' => $required_symbols, | ||||
'xmap' => $xmap, | 'xmap' => $parsed_xmap, | ||||
); | ); | ||||
// -( Output )---------------------------------------------------------------- | // -( Output )---------------------------------------------------------------- | ||||
if ($args->getArg('ugly')) { | if ($args->getArg('ugly')) { | ||||
echo json_encode($result); | echo json_encode($result); | ||||
▲ Show 20 Lines • Show All 70 Lines • ▼ Show 20 Lines | 'function' => array_filter( | ||||
// builtins and do not exist in vanilla PHP. Make sure we don't mark | // 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. | // them as builtin since we need to add dependencies for them. | ||||
'idx' => false, | 'idx' => false, | ||||
'id' => false, | 'id' => false, | ||||
) + array_fill_keys($builtin['functions'], true)), | ) + array_fill_keys($builtin['functions'], true)), | ||||
'interface' => array_fill_keys($builtin['interfaces'], true), | 'interface' => array_fill_keys($builtin['interfaces'], true), | ||||
); | ); | ||||
} | } | ||||
/** | |||||
* Resolve the fully qualified identifier of a provided name based on the | |||||
* file's namespace and imported symbols | |||||
* | |||||
* @param string The symbol name as it exists in code form | |||||
* @param string The symbol type (class, function, interface, etc) | |||||
* @param string The current namespace | |||||
* @param map<string, string> Import rules: imported_as => identifier | |||||
* | |||||
* @return string Fully qualified identifier name | |||||
*/ | |||||
function phutil_resolve_namespace($name, $type, $file_ns, array $imports) { | |||||
if (false === $separator = strpos($name, '\\')) { // Unqualified name | |||||
if ($type === 'function') { | |||||
// Do nothing - resolving function usage can't be done outside of runtime | |||||
// since it depends on the symbol table as it exists at execution | |||||
} else { | |||||
// Automatically resolve special keywords | |||||
if (in_array($name, array('self', 'static', 'parent'))) { | |||||
// Do nothing | |||||
} else if (isset($imports[$name])) { | |||||
$name = $imports[$name]; | |||||
} else { | |||||
$name = $file_ns.$name; | |||||
} | |||||
} | |||||
} else { // Qualified or fully qualified name | |||||
if ($separator === 0) { // Fully qualified name | |||||
$name = substr($name, 1); | |||||
} else { // Qualified name | |||||
$base = substr($name, 0, $separator); | |||||
if (isset($imports[$base])) { // Check the "use" list | |||||
$name = $imports[$base].substr($name, $separator); | |||||
} else { // Not hit in "use", it's the current NS | |||||
$name = $file_ns.$name; | |||||
} | |||||
} | |||||
} | |||||
return $name; | |||||
} |
Note: these are currently unsupported by XHPAST, as it hasn't pulled in the 5.6 syntax additions yet. Once that is done, adding these should be reasonably straightforward (loop over what will probably be n_USE_FUNCTION and n_USE_CONST and stuff them into $need)