diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 0a1621c842..6b4a685502 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -58,6 +58,7 @@ public function findSpecifiedType( Expr $node, ): ?bool { + $onlyReportFalse = false; if ($node instanceof FuncCall) { if ($node->isFirstClassCallable()) { return null; @@ -66,6 +67,17 @@ public function findSpecifiedType( $argsCount = count($args); if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); + if (in_array($functionName, [ + 'class_exists', + 'interface_exists', + 'trait_exists', + 'enum_exists', + ], true)) { + // Runtime autoload can always fail, so do not report "always true" for these. + // "Always false" is still reportable when the type specifier proves impossibility + // (e.g. class_exists() on a constant string that names an interface). + $onlyReportFalse = true; + } if ($functionName === 'assert' && $argsCount >= 1) { $arg = $args[0]->value; $assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean(); @@ -76,13 +88,7 @@ public function findSpecifiedType( return $assertValueIsTrue; } - if (in_array($functionName, [ - 'class_exists', - 'interface_exists', - 'trait_exists', - 'enum_exists', - 'function_exists', - ], true)) { + if ($functionName === 'function_exists') { return null; } if (in_array($functionName, ['count', 'sizeof'], true)) { @@ -284,7 +290,12 @@ public function findSpecifiedType( $rootExprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($rootExpr) : $scope->getNativeType($rootExpr); if ($rootExprType instanceof ConstantBooleanType) { - return $rootExprType->getValue(); + $value = $rootExprType->getValue(); + if ($onlyReportFalse && $value === true) { + return null; + } + + return $value; } return null; @@ -362,7 +373,16 @@ public function findSpecifiedType( } $result = TrinaryLogic::createYes()->and(...$results); - return $result->maybe() ? null : $result->yes(); + if ($result->maybe()) { + return null; + } + + $value = $result->yes(); + if ($onlyReportFalse && $value === true) { + return null; + } + + return $value; } private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index b70e6ac15e..84c37b4e1c 100644 --- a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php @@ -13,14 +13,18 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\AlwaysRememberedExpr; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use function count; use function in_array; use function ltrim; @@ -31,6 +35,10 @@ final class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSp private TypeSpecifier $typeSpecifier; + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, @@ -49,6 +57,16 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n { $args = $node->getArgs(); $argType = $scope->getType($args[0]->value); + $functionName = $functionReflection->getName(); + + if ($this->isCallAlwaysFalse($functionName, $argType)) { + return $this->typeSpecifier->create( + $args[0]->value, + new NeverType(), + $context, + $scope, + ); + } // class_exists() will only assure one of the classes to exist. $constantStrings = $argType->getConstantStrings(); @@ -101,4 +119,40 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void $this->typeSpecifier = $typeSpecifier; } + private function isCallAlwaysFalse(string $functionName, Type $argType): bool + { + $constantStrings = $argType->getConstantStrings(); + if ($constantStrings === []) { + return false; + } + + foreach ($constantStrings as $constantString) { + $name = ltrim($constantString->getValue(), '\\'); + if (!$this->reflectionProvider->hasClass($name)) { + return false; + } + if ($this->matchesFunctionKind($functionName, $this->reflectionProvider->getClass($name))) { + return false; + } + } + + return true; + } + + private function matchesFunctionKind(string $functionName, ClassReflection $reflection): bool + { + switch ($functionName) { + case 'class_exists': + return !$reflection->isInterface() && !$reflection->isTrait(); + case 'interface_exists': + return $reflection->isInterface(); + case 'trait_exists': + return $reflection->isTrait(); + case 'enum_exists': + return $reflection->isEnum(); + default: + return true; + } + } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14683.php b/tests/PHPStan/Analyser/nsrt/bug-14683.php new file mode 100644 index 0000000000..b3200dfd83 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14683.php @@ -0,0 +1,48 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14683; + +use function PHPStan\Testing\assertType; + +interface SomeInterface {} +class SomeClass {} +trait SomeTrait {} +enum SomeEnum {} + +function classExistsOnConstantStringInterface(): void +{ + if (class_exists(SomeInterface::class)) { + assertType('*NEVER*', SomeInterface::class); + } +} + +function classExistsOnConstantStringTrait(): void +{ + if (class_exists(SomeTrait::class)) { + assertType('*NEVER*', SomeTrait::class); + } +} + +function interfaceExistsOnConstantStringClass(): void +{ + if (interface_exists(SomeClass::class)) { + assertType('*NEVER*', SomeClass::class); + } +} + +function enumExistsOnConstantStringEnum(): void +{ + if (enum_exists(SomeEnum::class)) { + assertType('\'Bug14683\\\\SomeEnum\'', SomeEnum::class); + } +} + +function classExistsOnEnumConstantString(): void +{ + // enums are classes in PHP, so this is NOT impossible + if (class_exists(SomeEnum::class)) { + assertType('\'Bug14683\\\\SomeEnum\'', SomeEnum::class); + } +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 00306e738b..073d30331f 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -284,6 +284,25 @@ public function testBug7898(): void $this->analyse([__DIR__ . '/data/bug-7898.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testStructExists(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/check-type-function-call-struct-exists.php'], [ + ['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 23], + ['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 25], + ['Call to function interface_exists() with \'CheckTypeFunctionCall\\\\_Enum\' will always evaluate to false.', 27], + ['Call to function interface_exists() with \'CheckTypeFunctionCall\\\\_Class\' will always evaluate to false.', 29], + ['Call to function interface_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 30], + ['Call to function trait_exists() with \'CheckTypeFunctionCall\\\\_Enum\' will always evaluate to false.', 32], + ['Call to function trait_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 33], + ['Call to function trait_exists() with \'CheckTypeFunctionCall\\\\_Class\' will always evaluate to false.', 34], + ['Call to function enum_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 38], + ['Call to function enum_exists() with \'CheckTypeFunctionCall\\\\_Class\' will always evaluate to false.', 39], + ['Call to function enum_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 40], + ]); + } + public function testDoNotReportTypesFromPhpDocs(): void { $this->treatPhpDocTypesAsCertain = false; diff --git a/tests/PHPStan/Rules/Comparison/data/check-type-function-call-struct-exists.php b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-struct-exists.php new file mode 100644 index 0000000000..285d6f59f2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call-struct-exists.php @@ -0,0 +1,41 @@ += 8.1 + +namespace CheckTypeFunctionCall; + +// see https://3v4l.org/V9nmf + +interface _Interface {} +class _Class {} +trait _Trait {} +enum _Enum {} + +class_exists($mixed); +interface_exists($mixed); +enum_exists($mixed); +trait_exists($mixed); + +class_exists(); +interface_exists(); +enum_exists(); +trait_exists(); + +class_exists(_Enum::class); +class_exists(_Interface::class); // always false +class_exists(_Class::class); +class_exists(_Trait::class); // always false + +interface_exists(_Enum::class); // always false +interface_exists(_Interface::class); +interface_exists(_Class::class); // always false +interface_exists(_Trait::class); // always false + +trait_exists(_Enum::class); // always false +trait_exists(_Interface::class); // always false +trait_exists(_Class::class); // always false +trait_exists(_Trait::class); + +enum_exists(_Enum::class); +enum_exists(_Interface::class); // always false +enum_exists(_Class::class); // always false +enum_exists(_Trait::class); // always false +