diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 25c70705792..69d934aba8a 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -211,6 +211,11 @@ public function findSpecifiedType( if ($objectType->getObjectClassNames() !== []) { if ($objectType->hasMethod($methodType->getValue())->yes()) { + foreach ($objectType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->hasNativeMethod($methodType->getValue())) { + return null; + } + } return true; } @@ -231,7 +236,15 @@ public function findSpecifiedType( if ($genericType instanceof TypeWithClassName) { if ($genericType->hasMethod($methodType->getValue())->yes()) { - return true; + $classReflection = $genericType->getClassReflection(); + if ( + $classReflection !== null + && $classReflection->hasNativeMethod($methodType->getValue()) + ) { + return true; + } + + return null; } $classReflection = $genericType->getClassReflection(); diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 3e42c9c42e5..9ec69cdd1d6 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -27,6 +28,10 @@ final class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyin private TypeSpecifier $typeSpecifier; + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -53,17 +58,27 @@ public function specifyTypes( $args = $node->getArgs(); $methodNameType = $scope->getType($args[1]->value); if (!$methodNameType instanceof ConstantStringType) { - return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()), - new ConstantBooleanType(true), - $context, - $scope, - ); + return $this->createFuncCallSpec($node, $context, $scope); } $objectType = $scope->getType($args[0]->value); if ($objectType->isString()->yes()) { if ($objectType->isClassString()->yes()) { + foreach ($objectType->getConstantStrings() as $constantString) { + if ($this->reflectionProvider->hasClass($constantString->getValue())) { + $classReflection = $this->reflectionProvider->getClass($constantString->getValue()); + if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) { + return $this->createFuncCallSpec($node, $context, $scope); + } + } + } + + foreach ($objectType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) { + if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) { + return $this->createFuncCallSpec($node, $context, $scope); + } + } + return $this->typeSpecifier->create( $args[0]->value, new IntersectionType([ @@ -78,6 +93,12 @@ public function specifyTypes( return new SpecifiedTypes([], []); } + foreach ($objectType->getObjectClassReflections() as $classReflection) { + if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) { + return $this->createFuncCallSpec($node, $context, $scope); + } + } + return $this->typeSpecifier->create( $args[0]->value, new UnionType([ @@ -92,4 +113,14 @@ public function specifyTypes( ); } + private function createFuncCallSpec(FuncCall $node, TypeSpecifierContext $context, Scope $scope): SpecifiedTypes + { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + } diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index b299f6f14fd..3c6405295d8 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -58,12 +58,7 @@ public function specifyTypes( $args = $node->getArgs(); $propertyNameType = $scope->getType($args[1]->value); if (!$propertyNameType instanceof ConstantStringType) { - return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), - new ConstantBooleanType(true), - $context, - $scope, - ); + return $this->createFuncCallSpec($node, $context, $scope); } if ($propertyNameType->getValue() === '') { @@ -85,7 +80,7 @@ public function specifyTypes( $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope); if ($propertyReflection !== null) { if (!$propertyReflection->isNative()) { - return new SpecifiedTypes([], []); + return $this->createFuncCallSpec($node, $context, $scope); } } @@ -100,4 +95,14 @@ public function specifyTypes( ); } + private function createFuncCallSpec(FuncCall $node, TypeSpecifierContext $context, Scope $scope): SpecifiedTypes + { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), + new ConstantBooleanType(true), + $context, + $scope, + ); + } + } diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 0861db6f447..5a847ad0293 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -236,6 +236,25 @@ public function testBug6822(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6822.php'], []); } + public function testBug6211(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6211.php'], [ + [ + 'If condition is always true.', + 93, + ], + [ + 'If condition is always true.', + 100, + ], + [ + 'If condition is always true.', + 114, + ], + ]); + } + public function testBug5020(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 17b6d29c7b5..02c61941e13 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1230,4 +1230,48 @@ public function testBug8217(): void $this->analyse([__DIR__ . '/data/bug-8217.php'], []); } + public function testBug6211(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6211.php'], [ + [ + 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', + 34, + ], + [ + 'Call to function method_exists() with \'Bug6211\\\\Hell\' and \'test\' will always evaluate to true.', + 39, + ], + [ + 'Call to function method_exists() with Bug6211\Bar and \'realMethod\' will always evaluate to true.', + 62, + ], + [ + 'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.', + 87, + ], + [ + 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', + 106, + ], + [ + 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', + 107, + ], + [ + 'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.', + 120, + ], + [ + 'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.', + 121, + ], + [ + 'Call to function method_exists() with class-string and \'test\' will always evaluate to true.', + 136, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6211.php b/tests/PHPStan/Rules/Comparison/data/bug-6211.php new file mode 100644 index 00000000000..9d5b806db5b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6211.php @@ -0,0 +1,139 @@ + $classString + */ +function testGenericClassString(string $classString): void { + // @method via generic class-string should not make method_exists always true + if (\method_exists($classString, 'isTrue')) { + + } + + // native method via generic class-string should still be always true + if (\method_exists($classString, 'test')) { + + } +}