Skip to content

Commit

Permalink
Method called on class-string<T> with static return type resolves to …
Browse files Browse the repository at this point in the history
…T; solve more cases for static calls and fetches
  • Loading branch information
ondrejmirtes committed Dec 8, 2019
1 parent f939d23 commit 57d296f
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 105 deletions.
224 changes: 122 additions & 102 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1504,76 +1504,51 @@ private function resolveType(Expr $node): Type

if ($node instanceof Expr\StaticCall && $node->name instanceof Node\Identifier) {
if ($node->class instanceof Name) {
$calleeType = new ObjectType($this->resolveName($node->class));
$staticMethodCalledOnType = new ObjectType($this->resolveName($node->class));
} else {
$calleeType = $this->getType($node->class);
}

$referencedClasses = TypeUtils::getDirectClassNames($this->getTypeToInstantiateForNew($calleeType));
$resolvedTypesFromDynamicReturnTypeExtensions = [];
$resolvedTypes = [];
foreach ($referencedClasses as $referencedClass) {
if (!$this->broker->hasClass($referencedClass)) {
continue;
}

$staticMethodClassReflection = $this->broker->getClass($referencedClass);
if (!$staticMethodClassReflection->hasMethod($node->name->name)) {
continue;
}

$staticMethodReflection = $staticMethodClassReflection->getMethod($node->name->name, $this);
$resolvedTypes[] = ParametersAcceptorSelector::selectFromArgs(
$this,
$node->args,
$staticMethodReflection->getVariants()
)->getReturnType();
foreach ($this->broker->getDynamicStaticMethodReturnTypeExtensionsForClass($staticMethodClassReflection->getName()) as $dynamicStaticMethodReturnTypeExtension) {
if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($staticMethodReflection)) {
continue;
}

$resolvedTypesFromDynamicReturnTypeExtensions[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($staticMethodReflection, $node, $this);
$staticMethodCalledOnType = $this->getType($node->class);
if ($staticMethodCalledOnType instanceof GenericClassStringType) {
$staticMethodCalledOnType = $staticMethodCalledOnType->getGenericType();
}
}
if (count($resolvedTypesFromDynamicReturnTypeExtensions) > 0) {
return TypeCombinator::union(...$resolvedTypesFromDynamicReturnTypeExtensions);
}

if ($calleeType->hasMethod($node->name->name)->yes()) {
$staticMethodReflection = $calleeType->getMethod($node->name->name, $this);
$staticMethodReturnType = ParametersAcceptorSelector::selectFromArgs(
$this,
$node->args,
$staticMethodReflection->getVariants()
)->getReturnType();

return TypeTraverser::map($staticMethodReturnType, function (Type $type, callable $traverse) use ($node, $calleeType): Type {
if ($type instanceof StaticType) {
if ($node->class instanceof Name) {
$nameNodeClassName = (string) $node->class;
$lowercasedNameNodeClassName = strtolower($nameNodeClassName);
if (in_array($lowercasedNameNodeClassName, [
'self',
'static',
'parent',
], true) && $this->isInClass()) {
return $traverse($type->changeBaseClass($this->getClassReflection()->getName()));
}
$methodName = $node->name->toString();
$map = function (Type $type, callable $traverse) use ($methodName, $node): Type {
if ($type instanceof UnionType) {
return $traverse($type);
}
if ($type instanceof IntersectionType) {
$returnTypes = [];
foreach ($type->getTypes() as $innerType) {
$returnType = $this->methodCallReturnType(
$type,
$innerType,
$methodName,
$node
);
if ($returnType === null) {
continue;
}

return $traverse($calleeType);
$returnTypes[] = $returnType;
}

return $traverse($type);
});
}

if (count($resolvedTypes) > 0) {
return TypeCombinator::union(...$resolvedTypes);
if (count($returnTypes) === 0) {
return new NeverType();
}
return TypeCombinator::intersect(...$returnTypes);
}
return $this->methodCallReturnType(
$type,
$type,
$methodName,
$node
) ?? new NeverType();
};
$returnType = TypeTraverser::map($staticMethodCalledOnType, $map);
if ($returnType instanceof NeverType) {
return new ErrorType();
}

return new ErrorType();
return $returnType;
}

if ($node instanceof PropertyFetch && $node->name instanceof Node\Identifier) {
Expand Down Expand Up @@ -1621,45 +1596,50 @@ private function resolveType(Expr $node): Type
&& $node->name instanceof Node\VarLikeIdentifier
) {
if ($node->class instanceof Name) {
$calleeType = new ObjectType($this->resolveName($node->class));
$staticPropertyFetchedOnType = new ObjectType($this->resolveName($node->class));
} else {
$calleeType = $this->getType($node->class);
}

$referencedClasses = TypeUtils::getDirectClassNames($calleeType);
$propertyName = $node->name->toString();
$types = [];
foreach ($referencedClasses as $referencedClass) {
if (!$this->broker->hasClass($referencedClass)) {
continue;
$staticPropertyFetchedOnType = $this->getType($node->class);
if ($staticPropertyFetchedOnType instanceof GenericClassStringType) {
$staticPropertyFetchedOnType = $staticPropertyFetchedOnType->getGenericType();
}
}

$propertyClassReflection = $this->broker->getClass($referencedClass);
if (!$propertyClassReflection->hasProperty($propertyName)) {
continue;
$staticPropertyName = $node->name->toString();
$map = function (Type $type, callable $traverse) use ($staticPropertyName, $node): Type {
if ($type instanceof UnionType) {
return $traverse($type);
}
if ($type instanceof IntersectionType) {
$returnTypes = [];
foreach ($type->getTypes() as $innerType) {
$returnType = $this->propertyFetchType(
$innerType,
$staticPropertyName,
$node
);
if ($returnType === null) {
continue;
}

$property = $propertyClassReflection->getProperty($propertyName, $this);
if ($this->isInExpressionAssign($node)) {
$types[] = $property->getWritableType();
} else {
$types[] = $property->getReadableType();
$returnTypes[] = $returnType;
}
if (count($returnTypes) === 0) {
return new NeverType();
}
return TypeCombinator::intersect(...$returnTypes);
}
}

if (count($types) > 0) {
return TypeCombinator::union(...$types);
}
return $this->propertyFetchType(
$type,
$staticPropertyName,
$node
) ?? new NeverType();
};

if (!$calleeType->hasProperty($node->name->name)->yes()) {
$returnType = TypeTraverser::map($staticPropertyFetchedOnType, $map);
if ($returnType instanceof NeverType) {
return new ErrorType();
}

$property = $calleeType->getProperty($node->name->name, $this);
if ($this->isInExpressionAssign($node)) {
return $property->getWritableType();
}
return $property->getReadableType();
return $returnType;
}

if ($node instanceof FuncCall) {
Expand Down Expand Up @@ -3330,7 +3310,14 @@ private function getTypeToInstantiateForNew(Type $type): Type
return $decidedType;
}

private function methodCallReturnType(Type $calledOnType, Type $typeWithMethod, string $methodName, MethodCall $node): ?Type
/**
* @param \PHPStan\Type\Type $calledOnType
* @param \PHPStan\Type\Type $typeWithMethod
* @param string $methodName
* @param MethodCall|\PhpParser\Node\Expr\StaticCall $methodCall
* @return \PHPStan\Type\Type|null
*/
private function methodCallReturnType(Type $calledOnType, Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type
{
if (!$typeWithMethod->hasMethod($methodName)->yes()) {
return null;
Expand All @@ -3340,12 +3327,23 @@ private function methodCallReturnType(Type $calledOnType, Type $typeWithMethod,

if ($typeWithMethod instanceof TypeWithClassName) {
$resolvedTypes = [];
foreach ($this->broker->getDynamicMethodReturnTypeExtensionsForClass($typeWithMethod->getClassName()) as $dynamicMethodReturnTypeExtension) {
if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
continue;

if ($methodCall instanceof MethodCall) {
foreach ($this->broker->getDynamicMethodReturnTypeExtensionsForClass($typeWithMethod->getClassName()) as $dynamicMethodReturnTypeExtension) {
if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
continue;
}

$resolvedTypes[] = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $methodCall, $this);
}
} else {
foreach ($this->broker->getDynamicStaticMethodReturnTypeExtensionsForClass($typeWithMethod->getClassName()) as $dynamicStaticMethodReturnTypeExtension) {
if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($methodReflection)) {
continue;
}

$resolvedTypes[] = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $node, $this);
$resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($methodReflection, $methodCall, $this);
}
}
if (count($resolvedTypes) > 0) {
return TypeCombinator::union(...$resolvedTypes);
Expand All @@ -3354,11 +3352,19 @@ private function methodCallReturnType(Type $calledOnType, Type $typeWithMethod,

$methodReturnType = ParametersAcceptorSelector::selectFromArgs(
$this,
$node->args,
$methodCall->args,
$methodReflection->getVariants()
)->getReturnType();

$calledOnThis = $calledOnType instanceof ThisType && $this->isInClass();
if ($methodCall instanceof MethodCall) {
$calledOnThis = $calledOnType instanceof ThisType && $this->isInClass();
} else {
if (!$methodCall->class instanceof Name) {
$calledOnThis = false;
} else {
$calledOnThis = in_array(strtolower($methodCall->class->toString()), ['self', 'static', 'parent'], true) && $this->isInClass();
}
}

$transformedCalledOnType = TypeTraverser::map($calledOnType, function (Type $type, callable $traverse) use ($calledOnThis): Type {
if ($type instanceof StaticType) {
Expand Down Expand Up @@ -3386,21 +3392,35 @@ private function methodCallReturnType(Type $calledOnType, Type $typeWithMethod,
});
}

private function propertyFetchType(Type $fetchedOnType, string $propertyName, PropertyFetch $node): ?Type
/**
* @param \PHPStan\Type\Type $fetchedOnType
* @param string $propertyName
* @param PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch
* @return \PHPStan\Type\Type|null
*/
private function propertyFetchType(Type $fetchedOnType, string $propertyName, Expr $propertyFetch): ?Type
{
if (!$fetchedOnType->hasProperty($propertyName)->yes()) {
return null;
}

$propertyReflection = $fetchedOnType->getProperty($propertyName, $this);

if ($this->isInExpressionAssign($node)) {
if ($this->isInExpressionAssign($propertyFetch)) {
$propertyType = $propertyReflection->getWritableType();
} else {
$propertyType = $propertyReflection->getReadableType();
}

$fetchedOnThis = $fetchedOnType instanceof ThisType && $this->isInClass();
if ($propertyFetch instanceof PropertyFetch) {
$fetchedOnThis = $fetchedOnType instanceof ThisType && $this->isInClass();
} else {
if (!$propertyFetch->class instanceof Name) {
$fetchedOnThis = false;
} else {
$fetchedOnThis = in_array(strtolower($propertyFetch->class->toString()), ['self', 'static', 'parent'], true) && $this->isInClass();
}
}

$transformedFetchedOnType = TypeTraverser::map($fetchedOnType, function (Type $type, callable $traverse) use ($fetchedOnThis): Type {
if ($type instanceof StaticType) {
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9581,6 +9581,11 @@ public function dataStaticProperties(): array
return $this->gatherAssertTypes(__DIR__ . '/data/static-properties.php');
}

public function dataStaticMethods(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/static-methods.php');
}

public function dataBug2612(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2612.php');
Expand All @@ -9607,6 +9612,7 @@ public function dataBug2676(): array
* @dataProvider dataArrayKey
* @dataProvider dataIntersectionStatic
* @dataProvider dataStaticProperties
* @dataProvider dataStaticMethods
* @dataProvider dataBug2612
* @dataProvider dataBug2677
* @dataProvider dataBug2676
Expand Down
34 changes: 34 additions & 0 deletions tests/PHPStan/Analyser/data/generics.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ function testG($int)

class Foo
{

/** @var static */
public static $staticProp;

/** @return static */
public static function returnsStatic()
{
Expand Down Expand Up @@ -1237,3 +1241,33 @@ function testClassString(
assertType('Exception', acceptsClassStringUpperBound($classString));
assertType('object', acceptsClassStringOfObject($classString));
}

class Bar extends Foo
{

}

/**
* @template T of Foo
* @param class-string<T> $classString
* @param class-string<Foo> $anotherClassString
* @param class-string<Bar> $yetAnotherClassString
*/
function returnStaticOnClassString(
string $classString,
string $anotherClassString,
string $yetAnotherClassString
)
{
assertType('T of PHPStan\Generics\FunctionsAssertType\Foo (function PHPStan\Generics\FunctionsAssertType\returnStaticOnClassString(), argument)', $classString::returnsStatic());
assertType('T of PHPStan\Generics\FunctionsAssertType\Foo (function PHPStan\Generics\FunctionsAssertType\returnStaticOnClassString(), argument)', $classString::instanceReturnsStatic());
assertType('T of PHPStan\Generics\FunctionsAssertType\Foo (function PHPStan\Generics\FunctionsAssertType\returnStaticOnClassString(), argument)', $classString::$staticProp);

assertType('PHPStan\Generics\FunctionsAssertType\Foo', $anotherClassString::instanceReturnsStatic());
assertType('PHPStan\Generics\FunctionsAssertType\Foo', $anotherClassString::returnsStatic());
assertType('PHPStan\Generics\FunctionsAssertType\Foo', $anotherClassString::$staticProp);

assertType('PHPStan\Generics\FunctionsAssertType\Bar', $yetAnotherClassString::instanceReturnsStatic());
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $yetAnotherClassString::returnsStatic());
assertType('PHPStan\Generics\FunctionsAssertType\Bar', $yetAnotherClassString::$staticProp);
}

0 comments on commit 57d296f

Please sign in to comment.