diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index f9023059fd..9255ee99d4 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -16,6 +16,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; +use PHPStan\Type\TypehintHelper; use PHPStan\Type\VerbosityLevel; use function array_merge; use function count; @@ -201,6 +202,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array $realPrototype = $method->getPrototype(); + $tentativeReturnTypeErrorReported = false; if ( $realPrototype instanceof MethodPrototypeReflection && $this->phpVersion->hasTentativeReturnTypes() @@ -209,6 +211,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array && count($prototypeDeclaringClass->getNativeReflection()->getMethod($prototype->getName())->getAttributes('ReturnTypeWillChange')) === 0 ) { if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($realPrototype->getTentativeReturnType(), $method->getNativeReturnType(), true)) { + $tentativeReturnTypeErrorReported = 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()), @@ -225,6 +228,39 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array } } + if ( + !$tentativeReturnTypeErrorReported + && $this->phpVersion->hasTentativeReturnTypes() + && !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()) + ) { + $parentNativeMethod = $prototypeDeclaringClass->getNativeReflection()->getMethod($prototype->getName()); + if ( + $parentNativeMethod->hasTentativeReturnType() + && count($parentNativeMethod->getAttributes('ReturnTypeWillChange')) === 0 + ) { + $parentTentativeReturnType = TypehintHelper::decideTypeFromReflection( + $parentNativeMethod->getTentativeReturnType(), + selfClass: $prototypeDeclaringClass, + ); + if (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($parentTentativeReturnType, $method->getNativeReturnType(), true)) { + $tentativeReturnTypeErrorReported = 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(), + $parentTentativeReturnType->describe(VerbosityLevel::typeOnly()), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.') + ->nonIgnorable() + ->identifier('method.tentativeReturnType') + ->build(); + } + } + } + $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method, false)); if (!$prototypeVariant instanceof ExtendedFunctionVariant) { @@ -234,8 +270,9 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array $prototypeReturnType = $prototypeVariant->getNativeReturnType(); $reportReturnType = true; if ($this->phpVersion->hasTentativeReturnTypes()) { - $reportReturnType = !$realPrototype instanceof MethodPrototypeReflection - || $realPrototype->getTentativeReturnType() === null + $hasTentativeReturnType = ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->getTentativeReturnType() !== null) + || $prototypeDeclaringClass->getNativeReflection()->getMethod($prototype->getName())->hasTentativeReturnType(); + $reportReturnType = !$hasTentativeReturnType || (is_bool($prototype->isBuiltin()) ? !$prototype->isBuiltin() : $prototype->isBuiltin()->no()); } else { if ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->isInternal()) { diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 55979eeb77..6f0817e13c 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -832,6 +832,25 @@ public function testBug9524(): void $this->analyse([__DIR__ . '/data/bug-9524.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug7317(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $tipText = 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.'; + $this->analyse([__DIR__ . '/data/bug-7317.php'], [ + [ + 'Return type bool of method Bug7317\MySimpleXMLElement::current() is not covariant with tentative return type static(SimpleXMLElement)|null of method SimpleXMLElement::current().', + 8, + $tipText, + ], + [ + 'Return type int of method Bug7317\MySimpleXMLElement::valid() is not covariant with tentative return type bool of method Iterator::valid().', + 12, + $tipText, + ], + ]); + } + #[RequiresPhp('>= 8.0')] public function testSimpleXmlElementChildClass(): void { diff --git a/tests/PHPStan/Rules/Methods/data/bug-7317.php b/tests/PHPStan/Rules/Methods/data/bug-7317.php new file mode 100644 index 0000000000..144f0fb5dd --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7317.php @@ -0,0 +1,15 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7317; + +class MySimpleXMLElement extends \SimpleXMLElement { + public function current(): bool { + return false; + } + + public function valid(): int { + return 1; + } +}