Chris@13: atLeastPhp55 = \version_compare(PHP_VERSION, '5.5', '>='); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate class, interface and trait definitions. Chris@13: * Chris@13: * Validate them upon entering the node, so that we know about their Chris@13: * presence and can validate constant fetches and static calls in class or Chris@13: * trait methods. Chris@13: * Chris@13: * @param Node $node Chris@13: */ Chris@13: public function enterNode(Node $node) Chris@13: { Chris@13: parent::enterNode($node); Chris@13: Chris@13: if (self::isConditional($node)) { Chris@13: $this->conditionalScopes++; Chris@13: } else { Chris@13: // @todo add an "else" here which adds a runtime check for instances where we can't tell Chris@13: // whether a class is being redefined by static analysis alone. Chris@13: if ($this->conditionalScopes === 0) { Chris@13: if ($node instanceof Class_) { Chris@13: $this->validateClassStatement($node); Chris@13: } elseif ($node instanceof Interface_) { Chris@13: $this->validateInterfaceStatement($node); Chris@13: } elseif ($node instanceof Trait_) { Chris@13: $this->validateTraitStatement($node); Chris@13: } Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate `new` expressions, class constant fetches, and static calls. Chris@13: * Chris@13: * @throws FatalErrorException if a class, interface or trait is referenced which does not exist Chris@13: * @throws FatalErrorException if a class extends something that is not a class Chris@13: * @throws FatalErrorException if a class implements something that is not an interface Chris@13: * @throws FatalErrorException if an interface extends something that is not an interface Chris@13: * @throws FatalErrorException if a class, interface or trait redefines an existing class, interface or trait name Chris@13: * Chris@13: * @param Node $node Chris@13: */ Chris@13: public function leaveNode(Node $node) Chris@13: { Chris@13: if (self::isConditional($node)) { Chris@13: $this->conditionalScopes--; Chris@13: } elseif ($node instanceof New_) { Chris@13: $this->validateNewExpression($node); Chris@13: } elseif ($node instanceof ClassConstFetch) { Chris@13: $this->validateClassConstFetchExpression($node); Chris@13: } elseif ($node instanceof StaticCall) { Chris@13: $this->validateStaticCallExpression($node); Chris@13: } Chris@13: } Chris@13: Chris@13: private static function isConditional(Node $node) Chris@13: { Chris@13: return $node instanceof If_ || Chris@13: $node instanceof While_ || Chris@13: $node instanceof Do_ || Chris@13: $node instanceof Switch_; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate a class definition statement. Chris@13: * Chris@13: * @param Class_ $stmt Chris@13: */ Chris@13: protected function validateClassStatement(Class_ $stmt) Chris@13: { Chris@16: $this->ensureCanDefine($stmt, self::CLASS_TYPE); Chris@13: if (isset($stmt->extends)) { Chris@13: $this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt); Chris@13: } Chris@13: $this->ensureInterfacesExist($stmt->implements, $stmt); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate an interface definition statement. Chris@13: * Chris@13: * @param Interface_ $stmt Chris@13: */ Chris@13: protected function validateInterfaceStatement(Interface_ $stmt) Chris@13: { Chris@16: $this->ensureCanDefine($stmt, self::INTERFACE_TYPE); Chris@13: $this->ensureInterfacesExist($stmt->extends, $stmt); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate a trait definition statement. Chris@13: * Chris@13: * @param Trait_ $stmt Chris@13: */ Chris@13: protected function validateTraitStatement(Trait_ $stmt) Chris@13: { Chris@16: $this->ensureCanDefine($stmt, self::TRAIT_TYPE); Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate a `new` expression. Chris@13: * Chris@13: * @param New_ $stmt Chris@13: */ Chris@13: protected function validateNewExpression(New_ $stmt) Chris@13: { Chris@13: // if class name is an expression or an anonymous class, give it a pass for now Chris@13: if (!$stmt->class instanceof Expr && !$stmt->class instanceof Class_) { Chris@13: $this->ensureClassExists($this->getFullyQualifiedName($stmt->class), $stmt); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate a class constant fetch expression's class. Chris@13: * Chris@13: * @param ClassConstFetch $stmt Chris@13: */ Chris@13: protected function validateClassConstFetchExpression(ClassConstFetch $stmt) Chris@13: { Chris@13: // there is no need to check exists for ::class const for php 5.5 or newer Chris@17: if (\strtolower($stmt->name) === 'class' && $this->atLeastPhp55) { Chris@13: return; Chris@13: } Chris@13: Chris@13: // if class name is an expression, give it a pass for now Chris@13: if (!$stmt->class instanceof Expr) { Chris@13: $this->ensureClassOrInterfaceExists($this->getFullyQualifiedName($stmt->class), $stmt); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Validate a class constant fetch expression's class. Chris@13: * Chris@13: * @param StaticCall $stmt Chris@13: */ Chris@13: protected function validateStaticCallExpression(StaticCall $stmt) Chris@13: { Chris@13: // if class name is an expression, give it a pass for now Chris@13: if (!$stmt->class instanceof Expr) { Chris@13: $this->ensureMethodExists($this->getFullyQualifiedName($stmt->class), $stmt->name, $stmt); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Ensure that no class, interface or trait name collides with a new definition. Chris@13: * Chris@13: * @throws FatalErrorException Chris@13: * Chris@16: * @param Stmt $stmt Chris@16: * @param string $scopeType Chris@13: */ Chris@16: protected function ensureCanDefine(Stmt $stmt, $scopeType = self::CLASS_TYPE) Chris@13: { Chris@13: $name = $this->getFullyQualifiedName($stmt->name); Chris@13: Chris@13: // check for name collisions Chris@13: $errorType = null; Chris@13: if ($this->classExists($name)) { Chris@13: $errorType = self::CLASS_TYPE; Chris@13: } elseif ($this->interfaceExists($name)) { Chris@13: $errorType = self::INTERFACE_TYPE; Chris@13: } elseif ($this->traitExists($name)) { Chris@13: $errorType = self::TRAIT_TYPE; Chris@13: } Chris@13: Chris@13: if ($errorType !== null) { Chris@17: throw $this->createError(\sprintf('%s named %s already exists', \ucfirst($errorType), $name), $stmt); Chris@13: } Chris@13: Chris@13: // Store creation for the rest of this code snippet so we can find local Chris@13: // issue too Chris@17: $this->currentScope[\strtolower($name)] = $scopeType; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Ensure that a referenced class exists. Chris@13: * Chris@13: * @throws FatalErrorException Chris@13: * Chris@13: * @param string $name Chris@13: * @param Stmt $stmt Chris@13: */ Chris@13: protected function ensureClassExists($name, $stmt) Chris@13: { Chris@13: if (!$this->classExists($name)) { Chris@17: throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Ensure that a referenced class _or interface_ exists. Chris@13: * Chris@13: * @throws FatalErrorException Chris@13: * Chris@13: * @param string $name Chris@13: * @param Stmt $stmt Chris@13: */ Chris@13: protected function ensureClassOrInterfaceExists($name, $stmt) Chris@13: { Chris@13: if (!$this->classExists($name) && !$this->interfaceExists($name)) { Chris@17: throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt); Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Ensure that a referenced class _or trait_ exists. Chris@17: * Chris@17: * @throws FatalErrorException Chris@17: * Chris@17: * @param string $name Chris@17: * @param Stmt $stmt Chris@17: */ Chris@17: protected function ensureClassOrTraitExists($name, $stmt) Chris@17: { Chris@17: if (!$this->classExists($name) && !$this->traitExists($name)) { Chris@17: throw $this->createError(\sprintf('Class \'%s\' not found', $name), $stmt); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Ensure that a statically called method exists. Chris@13: * Chris@13: * @throws FatalErrorException Chris@13: * Chris@13: * @param string $class Chris@13: * @param string $name Chris@13: * @param Stmt $stmt Chris@13: */ Chris@13: protected function ensureMethodExists($class, $name, $stmt) Chris@13: { Chris@17: $this->ensureClassOrTraitExists($class, $stmt); Chris@13: Chris@13: // let's pretend all calls to self, parent and static are valid Chris@17: if (\in_array(\strtolower($class), ['self', 'parent', 'static'])) { Chris@13: return; Chris@13: } Chris@13: Chris@13: // ... and all calls to classes defined right now Chris@13: if ($this->findInScope($class) === self::CLASS_TYPE) { Chris@13: return; Chris@13: } Chris@13: Chris@13: // if method name is an expression, give it a pass for now Chris@13: if ($name instanceof Expr) { Chris@13: return; Chris@13: } Chris@13: Chris@17: if (!\method_exists($class, $name) && !\method_exists($class, '__callStatic')) { Chris@17: throw $this->createError(\sprintf('Call to undefined method %s::%s()', $class, $name), $stmt); Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Ensure that a referenced interface exists. Chris@13: * Chris@13: * @throws FatalErrorException Chris@13: * Chris@13: * @param Interface_[] $interfaces Chris@13: * @param Stmt $stmt Chris@13: */ Chris@13: protected function ensureInterfacesExist($interfaces, $stmt) Chris@13: { Chris@13: foreach ($interfaces as $interface) { Chris@13: /** @var string $name */ Chris@13: $name = $this->getFullyQualifiedName($interface); Chris@13: if (!$this->interfaceExists($name)) { Chris@17: throw $this->createError(\sprintf('Interface \'%s\' not found', $name), $stmt); Chris@13: } Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Get a symbol type key for storing in the scope name cache. Chris@13: * Chris@16: * @deprecated No longer used. Scope type should be passed into ensureCanDefine directly. Chris@16: * @codeCoverageIgnore Chris@16: * Chris@13: * @param Stmt $stmt Chris@13: * Chris@13: * @return string Chris@13: */ Chris@13: protected function getScopeType(Stmt $stmt) Chris@13: { Chris@13: if ($stmt instanceof Class_) { Chris@13: return self::CLASS_TYPE; Chris@13: } elseif ($stmt instanceof Interface_) { Chris@13: return self::INTERFACE_TYPE; Chris@13: } elseif ($stmt instanceof Trait_) { Chris@13: return self::TRAIT_TYPE; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether a class exists, or has been defined in the current code snippet. Chris@13: * Chris@13: * Gives `self`, `static` and `parent` a free pass. Chris@13: * Chris@13: * @param string $name Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: protected function classExists($name) Chris@13: { Chris@13: // Give `self`, `static` and `parent` a pass. This will actually let Chris@13: // some errors through, since we're not checking whether the keyword is Chris@13: // being used in a class scope. Chris@17: if (\in_array(\strtolower($name), ['self', 'static', 'parent'])) { Chris@13: return true; Chris@13: } Chris@13: Chris@17: return \class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether an interface exists, or has been defined in the current code snippet. Chris@13: * Chris@13: * @param string $name Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: protected function interfaceExists($name) Chris@13: { Chris@17: return \interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Check whether a trait exists, or has been defined in the current code snippet. Chris@13: * Chris@13: * @param string $name Chris@13: * Chris@13: * @return bool Chris@13: */ Chris@13: protected function traitExists($name) Chris@13: { Chris@17: return \trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE; Chris@13: } Chris@13: Chris@13: /** Chris@13: * Find a symbol in the current code snippet scope. Chris@13: * Chris@13: * @param string $name Chris@13: * Chris@13: * @return string|null Chris@13: */ Chris@13: protected function findInScope($name) Chris@13: { Chris@17: $name = \strtolower($name); Chris@13: if (isset($this->currentScope[$name])) { Chris@13: return $this->currentScope[$name]; Chris@13: } Chris@13: } Chris@13: Chris@13: /** Chris@13: * Error creation factory. Chris@13: * Chris@13: * @param string $msg Chris@13: * @param Stmt $stmt Chris@13: * Chris@13: * @return FatalErrorException Chris@13: */ Chris@13: protected function createError($msg, $stmt) Chris@13: { Chris@13: return new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine()); Chris@13: } Chris@13: }