/** * Updates the context of returned ThisType types. * * ```php * class A { * public function returnsThis() { * return $this; // this<A> * } * } * * class B extends A { * public function returnsSomething() { * $rs = $this->returnsThis(); * * return $rs; // this<B> * } * * public function returnsSomethingElse() * { * $rs = parent::returnsThis(); * * return $rs; // this<B> * } * } * * class C extends B { } * * $c = new C(); * $c->returnsThis(); // object<C> * $c->returnsSomething(); // object<C> * ``` * * We have two basic cases: * * 1. The called node refers to the current scope ($this->, self::, * parent::, static::, or SuperTypeName::). * 2. The node was called from the "outside" $foo->...(). * * In the first case, we need to preserve the wrapping with ThisType with * an updated creation context. In the second case, we have to unwrap the * ThisType. * * @param \PHPParser_Node $calledNode * @param PhpType $thisType Type of the current context * @param PhpType $calledType Type of the $calledNode * @param PhpType $returnType * * @return PhpType */ private function updateThisReference(\PHPParser_Node $calledNode, PhpType $thisType = null, PhpType $calledType, PhpType $returnType = null) { // Delays execution until necessary. $needsWrapping = function () use($calledNode, $thisType, $calledType) { if (null === $thisType) { return false; } switch (true) { case $calledNode instanceof \PHPParser_Node_Expr_Variable: return 'this' === $calledNode->name; case $calledNode instanceof \PHPParser_Node_Name: if (in_array(strtolower(implode("\\", $calledNode->parts)), array('self', 'static', 'parent'), true)) { return true; } // This handles the following case: // // class A { public function foo() { return $this; } } // class B extends A { public function bar() { return A::foo(); } } // $x = (new B())->bar(); // // $x is resolved to object<B>. if (null !== $thisType && $thisType->isSubtypeOf($calledType)) { return true; } return false; default: return false; } }; switch (true) { case $returnType instanceof ThisType: return $needsWrapping() ? $this->typeRegistry->getThisType($thisType) : $calledType; case $returnType instanceof UnionType: $types = array(); foreach ($returnType->getAlternates() as $alt) { if ($alt instanceof ThisType) { $types[] = $needsWrapping() ? $this->typeRegistry->getThisType($thisType) : $calledType; continue; } $types[] = $alt; } return $this->typeRegistry->createUnionType($types); default: return $returnType; } }
/** * @param string $type * * @return null|PhpType */ private function tryGettingMoreSpecificType(PhpType $docType = null, PhpType $actualType, AbstractFunction $function, array $importedNamespaces, MethodContainer $container = null, $type) { if (!$docType) { if (!$actualType->isUnknownType() && !$actualType->isAllType()) { return null; } return $this->{'infer' . $type . 'TypeForFunction'}($function, $container); } // If the type defined by the comment is an object (and not the NoObjectType), // and a super type of the actual type, we keep it. if (null !== $docType->toMaybeObjectType() && $actualType->isSubtypeOf($docType)) { return $docType; } // If the type defined by the comment is a nullable object type (excluding the // NoObject type), we keep the comment that is currently in place. if ($this->isNullableObjectType($docType) && $actualType->isSubtypeOf($docType)) { return $docType; } if ($docType->getDocType($importedNamespaces) !== $actualType->getDocType($importedNamespaces)) { return $actualType; } if ($actualType === $this->registry->getNativeType('array')) { $inferredType = $this->{'infer' . $type . 'TypeForFunction'}($function, $container); if ($inferredType && $inferredType->isArrayType()) { return $inferredType; } } return null; }
private function isMoreSpecificType(PhpType $actualType, PhpType $annotatedType) { if ($actualType->equals($annotatedType)) { return false; } if ($actualType->isUnknownType()) { return false; } return $actualType->isSubtypeOf($annotatedType); }