diff --git a/src/Rules/Methods/MethodPrototypeFinder.php b/src/Rules/Methods/MethodPrototypeFinder.php index 0ec7c9a1a6..e76d0b156a 100644 --- a/src/Rules/Methods/MethodPrototypeFinder.php +++ b/src/Rules/Methods/MethodPrototypeFinder.php @@ -25,14 +25,20 @@ public function __construct( } /** - * @return array{ExtendedMethodReflection, ClassReflection, bool}|null + * Finds the prototype method that a class method should be validated against. + * Returns two prototypes with different purposes: + * - Signature prototype: Used for validating method signature (parameters, return type, ...). + * - Inheritance prototype: Used for validating inheritance rules (final keyword, override attribute, ...). + * Also, return a bool to precise if the visibility of the prototype needs to be respected. + * + * @return array{ExtendedMethodReflection, ClassReflection, bool, ExtendedMethodReflection, ClassReflection}|null */ public function findPrototype(ClassReflection $classReflection, string $methodName): ?array { foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { if ($immediateInterface->hasNativeMethod($methodName)) { $method = $immediateInterface->getNativeMethod($methodName); - return [$method, $method->getDeclaringClass(), true]; + return [$method, $method->getDeclaringClass(), true, $method, $method->getDeclaringClass()]; } } @@ -47,15 +53,19 @@ public function findPrototype(ClassReflection $classReflection, string $methodNa $isAbstract = $methodReflection->isAbstract(); if ($isAbstract) { $declaringTrait = $trait->getNativeMethod($methodName)->getDeclaringClass(); + $prototype = $this->phpClassReflectionExtension->createUserlandMethodReflection( + $trait, + $classReflection, + $methodReflection, + $declaringTrait->getName(), + ); + return [ - $this->phpClassReflectionExtension->createUserlandMethodReflection( - $trait, - $classReflection, - $methodReflection, - $declaringTrait->getName(), - ), + $prototype, $declaringTrait, false, + $prototype, + $declaringTrait, ]; } } @@ -94,7 +104,23 @@ public function findPrototype(ClassReflection $classReflection, string $methodNa } } - return [$method, $method->getDeclaringClass(), true]; + $prototype = $method; + if (strtolower($method->getName()) === '__construct') { + foreach ($parentClass->getInterfaces() as $interface) { + if ($interface->hasNativeMethod($method->getName())) { + $prototype = $interface->getNativeMethod($method->getName()); + break; + } + } + } + + return [ + $prototype, + $prototype->getDeclaringClass(), + true, + $method, + $declaringClass, + ]; } } diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 3a568a95a8..f9023059fd 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -107,7 +107,13 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array return []; } - [$prototype, $prototypeDeclaringClass, $checkVisibility] = $prototypeData; + [ + $prototype, + $prototypeDeclaringClass, + $checkVisibility, + $inheritancePrototype, + $inheritancePrototypeDeclaringClass, + ] = $prototypeData; $messages = []; if ( @@ -119,8 +125,8 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array 'Method %s::%s() overrides method %s::%s() but is missing the #[\Override] attribute.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototypeDeclaringClass->getDisplayName(true), - $prototype->getName(), + $inheritancePrototypeDeclaringClass->getDisplayName(true), + $inheritancePrototype->getName(), )) ->identifier('method.missingOverride') ->fixNode($node->getOriginalNode(), static function (Node\Stmt\ClassMethod $method) { @@ -132,24 +138,24 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array }) ->build(); } - if ($prototype->isFinalByKeyword()->yes()) { + if ($inheritancePrototype->isFinalByKeyword()->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototypeDeclaringClass->getDisplayName(true), - $prototype->getName(), + $inheritancePrototypeDeclaringClass->getDisplayName(true), + $inheritancePrototype->getName(), )) ->nonIgnorable() ->identifier('method.parentMethodFinal') ->build(); - } elseif ($prototype->isFinal()->yes()) { + } elseif ($inheritancePrototype->isFinal()->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Method %s::%s() overrides @final method %s::%s().', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $prototypeDeclaringClass->getDisplayName(true), - $prototype->getName(), + $inheritancePrototypeDeclaringClass->getDisplayName(true), + $inheritancePrototype->getName(), ))->identifier('method.parentMethodFinalByPhpDoc') ->build(); } diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index a309c79faa..55979eeb77 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -803,6 +803,29 @@ public function testBug10165(): void $this->analyse([__DIR__ . '/data/bug-10165.php'], []); } + public function testBug11067(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-11067.php'], [ + [ + 'Method Bug11067\BooleanBuilder2::__construct() overrides final method Bug11067\BaseBuilder2::__construct().', + 41, + ], + ]); + } + + public function testBug12272(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-12272.php'], []); + } + + public function testBug12830(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-12830.php'], []); + } + public function testBug9524(): void { $this->phpVersionId = PHP_VERSION_ID; diff --git a/tests/PHPStan/Rules/Methods/data/bug-11067.php b/tests/PHPStan/Rules/Methods/data/bug-11067.php new file mode 100644 index 0000000000..7245f5feca --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-11067.php @@ -0,0 +1,46 @@ +MustBeString = $mustBeString; + $this->CanBeInt = $canBeInt; + } +} + +class B extends A +{ + public bool $CanBeBool; + + public function __construct(string $mustBeString, bool $canBeBool = false) + { + $this->MustBeString = $mustBeString; + $this->CanBeBool = $canBeBool; + } +} + +var_dump([ + new A('A', 1), + new B('B', true), +]);