diff --git a/scripts/phutil_symbols.php b/scripts/phutil_symbols.php --- a/scripts/phutil_symbols.php +++ b/scripts/phutil_symbols.php @@ -76,30 +76,55 @@ $root = $tree->getRootNode(); $root->buildSelectCache(); -// -( Unsupported Constructs )------------------------------------------------ +// -( Namespace and Use resolution )------------------------------------------ $namespaces = $root->selectDescendantsOfType('n_NAMESPACE'); +$file_namespace = ''; foreach ($namespaces as $namespace) { - phutil_fail_on_unsupported_feature($namespace, $path, pht('namespaces')); + 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')); + } else { + $file_namespace = $namespace->getChildByIndex(0)->getConcreteString().'\\'; + } } +// Map imported classes as rule => class +$imports = array(); $uses = $root->selectDescendantsOfType('n_USE'); -foreach ($namespaces as $namespace) { - phutil_fail_on_unsupported_feature( - $namespace, $path, pht('namespace `use` statements')); -} - -$possible_traits = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); -foreach ($possible_traits as $possible_trait) { - $attributes = $possible_trait->getChildByIndex(0); - // Can't use getChildByIndex here because not all classes have attributes - foreach ($attributes->getChildren() as $attribute) { - if (strtolower($attribute->getConcreteString()) === 'trait') { - phutil_fail_on_unsupported_feature($possible_trait, $path, pht('traits')); +foreach ($uses as $use) { + if (2 !== count($use->getChildren())) { + // This is the "outer" T_USE statement; the actual contents will get picked + // up below + continue; + } + $import_from = $use->getChildByIndex(0)->getConcreteString(); + $import_as = $use->getChildByIndex(1)->getConcreteString(); + // If something is not explicitly imported with an alias, the alias is + // implied to be the deepest sub-namespace. + if (!$import_as) { + if (false === strpos($import_from, '\\')) { // Global NS + $import_as = $import_from; + } 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" +// TODO: "use function foo as bar" +// TODO: "use const FOO" // -( Marked Externals )------------------------------------------------------ @@ -228,14 +253,36 @@ // Find classes declared by this file. -// This is "class X ... { ... }". +// This is "class X ... { ... }" or "trait X ... { ... }". $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); 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); + $type = $is_trait ? 'trait' : 'class'; $have[] = array( - 'type' => 'class', + 'type' => $type, '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, + ); + } + } } @@ -268,7 +315,10 @@ ); // Track all 'extends' in the extension map. - $xmap[$class_name][] = $parent->getConcreteString(); + $xmap['class'][$class_name][] = array( + $parent->getConcreteString(), + 'class', + ); } } @@ -397,7 +447,10 @@ ); // Track 'class ... implements' in the extension map. - $xmap[$class_name][] = $interface->getConcreteString(); + $xmap['class'][$class_name][] = array( + $interface->getConcreteString(), + 'interface', + ); } } @@ -415,7 +468,10 @@ ); // Track 'interface ... extends' in the extension map. - $xmap[$interface_name][] = $parent->getConcreteString(); + $xmap['interface'][$interface_name][] = array( + $parent->getConcreteString(), + 'interface', + ); } } @@ -425,7 +481,7 @@ $declared_symbols = array(); foreach ($have as $key => $spec) { - $name = $spec['symbol']->getConcreteString(); + $name = $file_namespace.$spec['symbol']->getConcreteString(); $declared_symbols[$spec['type']][$name] = $spec['symbol']->getOffset(); } @@ -437,6 +493,7 @@ } $type = $spec['type']; + $name = phutil_resolve_namespace($name, $type, $file_namespace, $imports); foreach (explode('/', $type) as $libtype) { if (!$show_all) { if (!empty($externals[$libtype][$name])) { @@ -452,6 +509,13 @@ // We declare this symbol, so don't treat it as a requirement. 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])) { // Report only the first use of a symbol, since reporting all of them @@ -461,10 +525,33 @@ $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( 'have' => $declared_symbols, 'need' => $required_symbols, - 'xmap' => $xmap, + 'xmap' => $parsed_xmap, ); @@ -551,3 +638,44 @@ '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 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; +} diff --git a/src/moduleutils/PhutilLibraryMapBuilder.php b/src/moduleutils/PhutilLibraryMapBuilder.php --- a/src/moduleutils/PhutilLibraryMapBuilder.php +++ b/src/moduleutils/PhutilLibraryMapBuilder.php @@ -343,7 +343,16 @@ foreach ($symbol_map as $file => $info) { foreach ($info['have'] as $type => $symbols) { foreach ($symbols as $symbol => $declaration) { - $lib_type = ($type == 'interface') ? 'class' : $type; + // Treat interfaces and traits provided as classes + switch ($type) { + case 'interface': + case 'trait': + $lib_type = 'class'; + break; + default: + $lib_type = $type; + break; + } if (!empty($library_map[$lib_type][$symbol])) { $prior = $library_map[$lib_type][$symbol]; throw new Exception(