Skip to content

Commit

Permalink
Support for tentative return types
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 5, 2021
1 parent 546e87c commit 762fc47
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 5 deletions.
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Expand Up @@ -152,4 +152,9 @@ public function supportsCaseInsensitiveConstantNames(): bool
return $this->versionId < 80000;
}

public function hasTentativeReturnTypes(): bool
{
return $this->versionId >= 80100;
}

}
14 changes: 13 additions & 1 deletion src/Reflection/MethodPrototypeReflection.php
Expand Up @@ -2,6 +2,8 @@

namespace PHPStan\Reflection;

use PHPStan\Type\Type;

class MethodPrototypeReflection implements ClassMemberReflection
{

Expand All @@ -22,6 +24,8 @@ class MethodPrototypeReflection implements ClassMemberReflection
/** @var ParametersAcceptor[] */
private array $variants;

private ?Type $tentativeReturnType;

/**
* @param string $name
* @param ClassReflection $declaringClass
Expand All @@ -31,6 +35,7 @@ class MethodPrototypeReflection implements ClassMemberReflection
* @param bool $isAbstract
* @param bool $isFinal
* @param ParametersAcceptor[] $variants
* @param ?Type $tentativeReturnType
*/
public function __construct(
string $name,
Expand All @@ -40,7 +45,8 @@ public function __construct(
bool $isPublic,
bool $isAbstract,
bool $isFinal,
array $variants
array $variants,
?Type $tentativeReturnType
)
{
$this->name = $name;
Expand All @@ -51,6 +57,7 @@ public function __construct(
$this->isAbstract = $isAbstract;
$this->isFinal = $isFinal;
$this->variants = $variants;
$this->tentativeReturnType = $tentativeReturnType;
}

public function getName(): string
Expand Down Expand Up @@ -101,4 +108,9 @@ public function getVariants(): array
return $this->variants;
}

public function getTentativeReturnType(): ?Type
{
return $this->tentativeReturnType;
}

}
9 changes: 8 additions & 1 deletion src/Reflection/Native/NativeMethodReflection.php
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
use PHPStan\Type\TypehintHelper;
use PHPStan\Type\VoidType;

class NativeMethodReflection implements MethodReflection
Expand Down Expand Up @@ -89,6 +90,11 @@ public function getPrototype(): ClassMemberReflection
$prototypeMethod = $this->reflection->getPrototype();
$prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName());

$tentativeReturnType = null;
if ($prototypeMethod->getTentativeReturnType() !== null) {
$tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType());
}

return new MethodPrototypeReflection(
$prototypeMethod->getName(),
$prototypeDeclaringClass,
Expand All @@ -97,7 +103,8 @@ public function getPrototype(): ClassMemberReflection
$prototypeMethod->isPublic(),
$prototypeMethod->isAbstract(),
$prototypeMethod->isFinal(),
$prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants()
$prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(),
$tentativeReturnType
);
} catch (\ReflectionException $e) {
return $this;
Expand Down
2 changes: 2 additions & 0 deletions src/Reflection/Php/BuiltinMethodReflection.php
Expand Up @@ -35,6 +35,8 @@ public function isVariadic(): bool;

public function getReturnType(): ?\ReflectionType;

public function getTentativeReturnType(): ?\ReflectionType;

/**
* @return \ReflectionParameter[]
*/
Expand Down
5 changes: 5 additions & 0 deletions src/Reflection/Php/FakeBuiltinMethodReflection.php
Expand Up @@ -105,6 +105,11 @@ public function getReturnType(): ?\ReflectionType
return null;
}

public function getTentativeReturnType(): ?\ReflectionType
{
return null;
}

/**
* @return \ReflectionParameter[]
*/
Expand Down
9 changes: 9 additions & 0 deletions src/Reflection/Php/NativeBuiltinMethodReflection.php
Expand Up @@ -124,6 +124,15 @@ public function getReturnType(): ?\ReflectionType
return $this->reflection->getReturnType();
}

public function getTentativeReturnType(): ?\ReflectionType
{
if (method_exists($this->reflection, 'getTentativeReturnType')) {
return $this->reflection->getTentativeReturnType();
}

return null;
}

/**
* @return \ReflectionParameter[]
*/
Expand Down
8 changes: 7 additions & 1 deletion src/Reflection/Php/PhpMethodReflection.php
Expand Up @@ -162,6 +162,11 @@ public function getPrototype(): ClassMemberReflection
$prototypeMethod = $this->reflection->getPrototype();
$prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName());

$tentativeReturnType = null;
if ($prototypeMethod->getTentativeReturnType() !== null) {
$tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType());
}

return new MethodPrototypeReflection(
$prototypeMethod->getName(),
$prototypeDeclaringClass,
Expand All @@ -170,7 +175,8 @@ public function getPrototype(): ClassMemberReflection
$prototypeMethod->isPublic(),
$prototypeMethod->isAbstract(),
$prototypeMethod->isFinal(),
$prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants()
$prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(),
$tentativeReturnType
);
} catch (\ReflectionException $e) {
return $this;
Expand Down
35 changes: 33 additions & 2 deletions src/Rules/Methods/OverridingMethodRule.php
Expand Up @@ -145,8 +145,28 @@ public function processNode(Node $node, Scope $scope): array
$prototypeVariant = $prototypeVariants[0];

$methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants());
$methodReturnType = $methodVariant->getNativeReturnType();
$methodParameters = $methodVariant->getParameters();

if (
$this->phpVersion->hasTentativeReturnTypes()
&& $prototype->getTentativeReturnType() !== null
&& !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode())
) {

if (!$this->isTypeCompatible($prototype->getTentativeReturnType(), $methodVariant->getNativeReturnType(), true)) {
$messages[] = RuleErrorBuilder::message(sprintf(
'Return type %s of method %s::%s() is not covariant with tentative return type %s of method %s::%s().',
$methodReturnType->describe(VerbosityLevel::typeOnly()),
$method->getDeclaringClass()->getDisplayName(),
$method->getName(),
$prototype->getTentativeReturnType()->describe(VerbosityLevel::typeOnly()),
$prototype->getDeclaringClass()->getDisplayName(),
$prototype->getName()
))->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.')->nonIgnorable()->build();
}
}

$prototypeAfterVariadic = false;
foreach ($prototypeVariant->getParameters() as $i => $prototypeParameter) {
if (!array_key_exists($i, $methodParameters)) {
Expand Down Expand Up @@ -380,8 +400,6 @@ public function processNode(Node $node, Scope $scope): array
}
}

$methodReturnType = $methodVariant->getNativeReturnType();

if (!$prototypeVariant instanceof FunctionVariantWithPhpDocs) {
return $this->addErrors($messages, $node, $scope);
}
Expand Down Expand Up @@ -466,4 +484,17 @@ private function addErrors(
return $this->methodSignatureRule->processNode($classMethod, $scope);
}

private function hasReturnTypeWillChangeAttribute(Node\Stmt\ClassMethod $method): bool
{
foreach ($method->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
if ($attr->name->toLowerString() === 'returntypewillchange') {
return true;
}
}
}

return false;
}

}
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php
Expand Up @@ -516,4 +516,42 @@ public function testBug4516(): void
$this->analyse([__DIR__ . '/data/bug-4516.php'], []);
}

public function dataTentativeReturnTypes(): array
{
return [
[70400, []],
[80000, []],
[
80100,
[
[
'Return type mixed of method TentativeReturnTypes\Foo::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().',
8,
'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.',
],
[
'Return type string of method TentativeReturnTypes\Lorem::getIterator() is not covariant with tentative return type Traversable of method IteratorAggregate::getIterator().',
40,
'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.',
],
],
],
];
}

/**
* @dataProvider dataTentativeReturnTypes
* @param int $phpVersionId
* @param mixed[] $errors
*/
public function testTentativeReturnTypes(int $phpVersionId, array $errors): void
{
if (!self::$useStaticReflectionProvider) {
$this->markTestSkipped('Test requires static reflection.');
}

$this->phpVersionId = $phpVersionId;
$this->analyse([__DIR__ . '/data/tentative-return-types.php'], $errors);
}

}
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Methods/data/tentative-return-types.php
@@ -0,0 +1,45 @@
<?php

namespace TentativeReturnTypes;

class Foo implements \IteratorAggregate
{

public function getIterator()
{

}

}

class Bar implements \IteratorAggregate
{

#[\ReturnTypeWillChange]
public function getIterator()
{

}

}

class Baz implements \IteratorAggregate
{

#[\ReturnTypeWillChange]
public function getIterator(): string
{

}

}

class Lorem implements \IteratorAggregate
{

public function getIterator(): string
{

}

}

0 comments on commit 762fc47

Please sign in to comment.