diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 63560cc932..b7072c7026 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -155,6 +155,7 @@ private function getRuleRegistry(Container $container): RuleRegistry $crossCheckInterfacesHelper = $container->getByType(CrossCheckInterfacesHelper::class); $phpVersion = $container->getByType(PhpVersion::class); $localTypeAliasesCheck = $container->getByType(LocalTypeAliasesCheck::class); + $phpClassReflectionExtension = $container->getByType(PhpClassReflectionExtension::class); $rules = [ // level 0 @@ -165,7 +166,7 @@ private function getRuleRegistry(Container $container): RuleRegistry new ExistingClassesInTypehintsRule($functionDefinitionCheck), new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck), new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, $unresolvableTypeHelper, $phpVersion, true, false), - new OverridingMethodRule($phpVersion, new MethodSignatureRule(true, true, $container->getParameter('featureToggles')['abstractTraitMethod']), true, new MethodParameterComparisonHelper($phpVersion, $container->getParameter('featureToggles')['genericPrototypeMessage']), $container->getByType(PhpClassReflectionExtension::class), $container->getParameter('featureToggles')['genericPrototypeMessage'], $container->getParameter('featureToggles')['finalByPhpDoc'], $container->getParameter('checkMissingOverrideMethodAttribute')), + new OverridingMethodRule($phpVersion, new MethodSignatureRule($phpClassReflectionExtension, true, true, $container->getParameter('featureToggles')['abstractTraitMethod']), true, new MethodParameterComparisonHelper($phpVersion, $container->getParameter('featureToggles')['genericPrototypeMessage']), $phpClassReflectionExtension, $container->getParameter('featureToggles')['genericPrototypeMessage'], $container->getParameter('featureToggles')['finalByPhpDoc'], $container->getParameter('checkMissingOverrideMethodAttribute')), new DuplicateDeclarationRule(), new LocalTypeAliasesRule($localTypeAliasesCheck), new LocalTypeTraitAliasesRule($localTypeAliasesCheck, $reflectionProvider), diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index adf1c6bbb2..3de09b7ca6 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -10,6 +10,8 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\Php\NativeBuiltinMethodReflection; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; @@ -22,7 +24,6 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; use function count; -use function is_bool; use function min; use function sprintf; @@ -33,6 +34,7 @@ class MethodSignatureRule implements Rule { public function __construct( + private PhpClassReflectionExtension $phpClassReflectionExtension, private bool $reportMaybes, private bool $reportStatic, private bool $abstractTraitMethod, @@ -62,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; $declaringClass = $method->getDeclaringClass(); - foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as $parentMethod) { + foreach ($this->collectParentMethods($methodName, $method->getDeclaringClass()) as [$parentMethod, $parentMethodDeclaringClass]) { $parentVariants = $parentMethod->getVariants(); if (count($parentVariants) !== 1) { continue; @@ -77,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array $method->getName(), $returnTypeCompatibility->no() ? 'compatible' : 'covariant', $parentReturnType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), + $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), ))->build(); } @@ -102,7 +104,7 @@ public function processNode(Node $node, Scope $scope): array $parameterResult->no() ? 'compatible' : 'contravariant', $parentParameter->getName(), $parentParameterType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), + $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), ))->build(); } @@ -112,7 +114,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return ExtendedMethodReflection[] + * @return list */ private function collectParentMethods(string $methodName, ClassReflection $class): array { @@ -122,7 +124,7 @@ private function collectParentMethods(string $methodName, ClassReflection $class if ($parentClass !== null && $parentClass->hasNativeMethod($methodName)) { $parentMethod = $parentClass->getNativeMethod($methodName); if (!$parentMethod->isPrivate()) { - $parentMethods[] = $parentMethod; + $parentMethods[] = [$parentMethod, $parentMethod->getDeclaringClass()]; } } @@ -131,24 +133,31 @@ private function collectParentMethods(string $methodName, ClassReflection $class continue; } - $parentMethods[] = $interface->getNativeMethod($methodName); + $method = $interface->getNativeMethod($methodName); + $parentMethods[] = [$method, $method->getDeclaringClass()]; } if ($this->abstractTraitMethod) { foreach ($class->getTraits(true) as $trait) { - if (!$trait->hasNativeMethod($methodName)) { + $nativeTraitReflection = $trait->getNativeReflection(); + if (!$nativeTraitReflection->hasMethod($methodName)) { continue; } - $method = $trait->getNativeMethod($methodName); - $isAbstract = $method->isAbstract(); - if (is_bool($isAbstract)) { - if ($isAbstract) { - $parentMethods[] = $method; - } - } elseif ($isAbstract->yes()) { - $parentMethods[] = $method; + $methodReflection = $nativeTraitReflection->getMethod($methodName); + $isAbstract = $methodReflection->isAbstract(); + if (!$isAbstract) { + continue; } + + $parentMethods[] = [ + $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $class, + new NativeBuiltinMethodReflection($methodReflection), + ), + $trait->getNativeMethod($methodName)->getDeclaringClass(), + ]; } } diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 4fe8b1efd5..58342f5ce3 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -22,12 +22,14 @@ protected function getRule(): Rule { $phpVersion = new PhpVersion(PHP_VERSION_ID); + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule($this->reportMaybes, $this->reportStatic, true), + new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic, true), true, new MethodParameterComparisonHelper($phpVersion, true), - self::getContainer()->getByType(PhpClassReflectionExtension::class), + $phpClassReflectionExtension, true, true, false, @@ -443,4 +445,21 @@ public function testTraits(): void ]); } + public function testBug10166(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + + $this->analyse([__DIR__ . '/data/bug-10166.php'], [ + [ + 'Return type Bug10166\ReturnTypeClass2|null of method Bug10166\ReturnTypeClass2::createSelf() is not covariant with return type Bug10166\ReturnTypeClass2 of method Bug10166\ReturnTypeTrait::createSelf().', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index d34759259e..7f83d1cdea 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -24,12 +24,14 @@ protected function getRule(): Rule { $phpVersion = new PhpVersion($this->phpVersionId); + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + return new OverridingMethodRule( $phpVersion, - new MethodSignatureRule(true, true, true), + new MethodSignatureRule($phpClassReflectionExtension, true, true, true), false, new MethodParameterComparisonHelper($phpVersion, true), - self::getContainer()->getByType(PhpClassReflectionExtension::class), + $phpClassReflectionExtension, true, true, $this->checkMissingOverrideMethodAttribute, diff --git a/tests/PHPStan/Rules/Methods/data/bug-10166.php b/tests/PHPStan/Rules/Methods/data/bug-10166.php new file mode 100644 index 0000000000..68a2a95e71 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10166.php @@ -0,0 +1,27 @@ +