[aliasName => originalName]] */ protected $aliases = []; /** @var ErrorHandler Error handler */ protected $errorHandler; /** @var bool Whether to preserve original names */ protected $preserveOriginalNames; /** @var bool Whether to replace resolved nodes in place, or to add resolvedNode attributes */ protected $replaceNodes; /** * Constructs a name resolution visitor. * * Options: * * preserveOriginalNames (default false): An "originalName" attribute will be added to * all name nodes that underwent resolution. * * replaceNodes (default true): Resolved names are not replaced in-place. Instead a * resolvedName attribute is added. (Names that cannot be statically resolved receive a * namespacedName attribute, as usual.) * * @param ErrorHandler|null $errorHandler Error handler * @param array $options Options */ public function __construct(ErrorHandler $errorHandler = null, array $options = []) { $this->errorHandler = $errorHandler ?: new ErrorHandler\Throwing; $this->preserveOriginalNames = !empty($options['preserveOriginalNames']); $this->replaceNodes = isset($options['replaceNodes']) ? $options['replaceNodes'] : true; } public function beforeTraverse(array $nodes) { $this->resetState(); return null; } public function enterNode(Node $node) { if ($node instanceof Stmt\Namespace_) { $this->resetState($node->name); } elseif ($node instanceof Stmt\Use_) { foreach ($node->uses as $use) { $this->addAlias($use, $node->type, null); } } elseif ($node instanceof Stmt\GroupUse) { foreach ($node->uses as $use) { $this->addAlias($use, $node->type, $node->prefix); } } elseif ($node instanceof Stmt\Class_) { if (null !== $node->extends) { $node->extends = $this->resolveClassName($node->extends); } foreach ($node->implements as &$interface) { $interface = $this->resolveClassName($interface); } if (null !== $node->name) { $this->addNamespacedName($node); } } elseif ($node instanceof Stmt\Interface_) { foreach ($node->extends as &$interface) { $interface = $this->resolveClassName($interface); } $this->addNamespacedName($node); } elseif ($node instanceof Stmt\Trait_) { $this->addNamespacedName($node); } elseif ($node instanceof Stmt\Function_) { $this->addNamespacedName($node); $this->resolveSignature($node); } elseif ($node instanceof Stmt\ClassMethod || $node instanceof Expr\Closure ) { $this->resolveSignature($node); } elseif ($node instanceof Stmt\Const_) { foreach ($node->consts as $const) { $this->addNamespacedName($const); } } elseif ($node instanceof Expr\StaticCall || $node instanceof Expr\StaticPropertyFetch || $node instanceof Expr\ClassConstFetch || $node instanceof Expr\New_ || $node instanceof Expr\Instanceof_ ) { if ($node->class instanceof Name) { $node->class = $this->resolveClassName($node->class); } } elseif ($node instanceof Stmt\Catch_) { foreach ($node->types as &$type) { $type = $this->resolveClassName($type); } } elseif ($node instanceof Expr\FuncCall) { if ($node->name instanceof Name) { $node->name = $this->resolveOtherName($node->name, Stmt\Use_::TYPE_FUNCTION); } } elseif ($node instanceof Expr\ConstFetch) { $node->name = $this->resolveOtherName($node->name, Stmt\Use_::TYPE_CONSTANT); } elseif ($node instanceof Stmt\TraitUse) { foreach ($node->traits as &$trait) { $trait = $this->resolveClassName($trait); } foreach ($node->adaptations as $adaptation) { if (null !== $adaptation->trait) { $adaptation->trait = $this->resolveClassName($adaptation->trait); } if ($adaptation instanceof Stmt\TraitUseAdaptation\Precedence) { foreach ($adaptation->insteadof as &$insteadof) { $insteadof = $this->resolveClassName($insteadof); } } } } elseif ($node instanceof Node\NullableType) { if ($node->type instanceof Name) { $node->type = $this->resolveClassName($node->type); } } return null; } protected function resetState(Name $namespace = null) { $this->namespace = $namespace; $this->aliases = array( Stmt\Use_::TYPE_NORMAL => array(), Stmt\Use_::TYPE_FUNCTION => array(), Stmt\Use_::TYPE_CONSTANT => array(), ); } protected function addAlias(Stmt\UseUse $use, $type, Name $prefix = null) { // Add prefix for group uses $name = $prefix ? Name::concat($prefix, $use->name) : $use->name; // Type is determined either by individual element or whole use declaration $type |= $use->type; // Constant names are case sensitive, everything else case insensitive if ($type === Stmt\Use_::TYPE_CONSTANT) { $aliasName = $use->alias; } else { $aliasName = strtolower($use->alias); } if (isset($this->aliases[$type][$aliasName])) { $typeStringMap = array( Stmt\Use_::TYPE_NORMAL => '', Stmt\Use_::TYPE_FUNCTION => 'function ', Stmt\Use_::TYPE_CONSTANT => 'const ', ); $this->errorHandler->handleError(new Error( sprintf( 'Cannot use %s%s as %s because the name is already in use', $typeStringMap[$type], $name, $use->alias ), $use->getAttributes() )); return; } $this->aliases[$type][$aliasName] = $name; } /** @param Stmt\Function_|Stmt\ClassMethod|Expr\Closure $node */ private function resolveSignature($node) { foreach ($node->params as $param) { $param->type = $this->resolveType($param->type); } $node->returnType = $this->resolveType($node->returnType); } private function resolveType($node) { if ($node instanceof Node\NullableType) { $node->type = $this->resolveType($node->type); return $node; } if ($node instanceof Name) { return $this->resolveClassName($node); } return $node; } /** * Resolve class name, according to name resolver options. * * @param Name $name Class ame to resolve * * @return Name Resolved name, or original name with attribute */ protected function resolveClassName(Name $name) { if (!$this->replaceNodes) { $name->setAttribute('resolvedName', $this->getResolvedClassName($name)); return $name; } if ($this->preserveOriginalNames) { // Save the original name $originalName = $name; $name = clone $originalName; $name->setAttribute('originalName', $originalName); } return $this->getResolvedClassName($name); } /** * Resolve function or constant name, according to name resolver options. * * @param Name $name Function or constant name to resolve * @param int $type One of Stmt\Use_::TYPE_{FUNCTION|CONSTANT} * * @return Name Resolved name, or original name with attribute */ protected function resolveOtherName(Name $name, $type) { if (!$this->replaceNodes) { $resolvedName = $this->getResolvedOtherName($name, $type); if (null !== $resolvedName) { $name->setAttribute('resolvedName', $resolvedName); } else { $name->setAttribute('namespacedName', FullyQualified::concat($this->namespace, $name, $name->getAttributes())); } return $name; } if ($this->preserveOriginalNames) { // Save the original name $originalName = $name; $name = clone $originalName; $name->setAttribute('originalName', $originalName); } $resolvedName = $this->getResolvedOtherName($name, $type); if (null !== $resolvedName) { return $resolvedName; } // unqualified names inside a namespace cannot be resolved at compile-time // add the namespaced version of the name as an attribute $name->setAttribute('namespacedName', FullyQualified::concat($this->namespace, $name, $name->getAttributes())); return $name; } /** * Get resolved class name. * * @param Name $name Class ame to resolve * * @return Name Resolved name */ protected function getResolvedClassName(Name $name) { // don't resolve special class names if (in_array(strtolower($name->toString()), array('self', 'parent', 'static'))) { if (!$name->isUnqualified()) { $this->errorHandler->handleError(new Error( sprintf("'\\%s' is an invalid class name", $name->toString()), $name->getAttributes() )); } return $name; } // fully qualified names are already resolved if ($name->isFullyQualified()) { return $name; } $aliasName = strtolower($name->getFirst()); if (!$name->isRelative() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) { // resolve aliases (for non-relative names) $alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName]; return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes()); } // if no alias exists prepend current namespace return FullyQualified::concat($this->namespace, $name, $name->getAttributes()); } /** * Get resolved function or constant name. * * @param Name $name Function or constant name to resolve * @param int $type One of Stmt\Use_::TYPE_{FUNCTION|CONSTANT} * * @return null|Name Resolved name, or null if static resolution is not possible */ protected function getResolvedOtherName(Name $name, $type) { // fully qualified names are already resolved if ($name->isFullyQualified()) { return $name; } // resolve aliases for qualified names $aliasName = strtolower($name->getFirst()); if ($name->isQualified() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) { $alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName]; return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes()); } if ($name->isUnqualified()) { if ($type === Stmt\Use_::TYPE_CONSTANT) { // constant aliases are case-sensitive, function aliases case-insensitive $aliasName = $name->getFirst(); } if (isset($this->aliases[$type][$aliasName])) { // resolve unqualified aliases return new FullyQualified($this->aliases[$type][$aliasName], $name->getAttributes()); } if (null === $this->namespace) { // outside of a namespace unaliased unqualified is same as fully qualified return new FullyQualified($name, $name->getAttributes()); } // Cannot resolve statically return null; } // if no alias exists prepend current namespace return FullyQualified::concat($this->namespace, $name, $name->getAttributes()); } protected function addNamespacedName(Node $node) { $node->namespacedName = Name::concat($this->namespace, (string) $node->name); } }