From 364912fd90528c34253c9c5258baac4898e09d82 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 23 May 2026 13:35:04 +0200 Subject: [PATCH 1/7] Do not report function_exists as always-true conditions --- .../Comparison/ImpossibleCheckTypeHelper.php | 1 + .../ImpossibleCheckTypeFunctionCallRuleTest.php | 6 ++++++ .../PHPStan/Rules/Comparison/data/bug-8980.php | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-8980.php diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 69d934aba8..0a1621c842 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -81,6 +81,7 @@ public function findSpecifiedType( 'interface_exists', 'trait_exists', 'enum_exists', + 'function_exists', ], true)) { return null; } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index ce1094a5b2..00306e738b 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1236,6 +1236,12 @@ public function testBug8217(): void $this->analyse([__DIR__ . '/data/bug-8217.php'], []); } + public function testBug8980(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8980.php'], []); + } + public function testBug6211(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8980.php b/tests/PHPStan/Rules/Comparison/data/bug-8980.php new file mode 100644 index 0000000000..8c4312d2db --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8980.php @@ -0,0 +1,17 @@ + Date: Sat, 23 May 2026 13:55:07 +0200 Subject: [PATCH 2/7] test if-constant with function_exists --- .../Rules/Comparison/IfConstantConditionRuleTest.php | 11 +++++++++++ tests/PHPStan/Rules/Comparison/data/bug-8980.php | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 5a847ad029..197afcbd3f 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -261,4 +261,15 @@ public function testBug5020(): void $this->analyse([__DIR__ . '/data/bug-5020.php'], []); } + public function testBug8980(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-8980.php'], [ + [ + 'If condition is always true.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8980.php b/tests/PHPStan/Rules/Comparison/data/bug-8980.php index 8c4312d2db..fb5709ab11 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-8980.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-8980.php @@ -15,3 +15,12 @@ static function( $function_name ) { return ! function_exists( $function_name ); } ); + +function testFunctionExistsGuardReturn(): void { + if (!function_exists('curl_init')) { + return; + } + if (function_exists('curl_init')) { + echo 'exists'; + } +} From 5acde8ba21b96901a008980cb5dbc4316b985d1f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 May 2026 11:02:32 +0200 Subject: [PATCH 3/7] improve fix --- phpstan-baseline.neon | 6 ---- .../Comparison/ImpossibleCheckTypeHelper.php | 1 - ...nExistsFunctionTypeSpecifyingExtension.php | 28 ++++++++++++------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d7174f5db4..7325f424b4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1587,12 +1587,6 @@ parameters: count: 1 path: src/Type/Php/FilterFunctionReturnTypeHelper.php - - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 0a1621c842..69d934aba8 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -81,7 +81,6 @@ public function findSpecifiedType( 'interface_exists', 'trait_exists', 'enum_exists', - 'function_exists', ], true)) { return null; } diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php index 330735d9e4..9789d876c9 100644 --- a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -15,8 +15,8 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\CallableType; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function count; use function ltrim; #[AutowiredService] @@ -37,15 +37,23 @@ public function isFunctionSupported( public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $argType = $scope->getType($node->getArgs()[0]->value); - if ($argType instanceof ConstantStringType) { - return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('function_exists'), [ - new Arg(new String_(ltrim($argType->getValue(), '\\'))), - ]), - new ConstantBooleanType(true), - $context, - $scope, - ); + + $constantStrings = $argType->getConstantStrings(); + if (count($constantStrings) > 0) { + $specifiedTypes = new SpecifiedTypes(); + + foreach ($constantStrings as $constantString) { + $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + new FuncCall(new FullyQualified('function_exists'), [ + new Arg(new String_(ltrim($constantString->getValue(), '\\'))), + ]), + new ConstantBooleanType(true), + $context, + $scope, + )); + } + + return $specifiedTypes; } return $this->typeSpecifier->create( From b7a34585224e3df580e970db637f08c0f6396647 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 May 2026 11:52:53 +0200 Subject: [PATCH 4/7] fix --- .../Comparison/ImpossibleCheckTypeHelper.php | 1 + ...tionExistsFunctionTypeSpecifyingExtension.php | 2 +- .../CallToNonExistentFunctionRuleTest.php | 16 ++++++++++++++++ tests/PHPStan/Rules/Functions/data/bug-8980.php | 14 ++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-8980.php diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 69d934aba8..0a1621c842 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -81,6 +81,7 @@ public function findSpecifiedType( 'interface_exists', 'trait_exists', 'enum_exists', + 'function_exists', ], true)) { return null; } diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php index 9789d876c9..ccab9e96ae 100644 --- a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -39,7 +39,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $argType = $scope->getType($node->getArgs()[0]->value); $constantStrings = $argType->getConstantStrings(); - if (count($constantStrings) > 0) { + if (count($constantStrings) === 1) { $specifiedTypes = new SpecifiedTypes(); foreach ($constantStrings as $constantString) { diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index 703eaba247..78334e703b 100644 --- a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php @@ -264,4 +264,20 @@ public function testBug14384(): void $this->analyse([__DIR__ . '/data/bug-14384.php'], []); } + public function testBug8980(): void + { + $this->analyse([__DIR__ . '/data/bug-8980.php'], [ + [ + 'Function funcA not found.', + 12, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function funcB not found.', + 13, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-8980.php b/tests/PHPStan/Rules/Functions/data/bug-8980.php new file mode 100644 index 0000000000..b8699979bd --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8980.php @@ -0,0 +1,14 @@ + Date: Sun, 24 May 2026 12:16:35 +0200 Subject: [PATCH 5/7] Update bug-8980.php --- tests/PHPStan/Rules/Functions/data/bug-8980.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Functions/data/bug-8980.php b/tests/PHPStan/Rules/Functions/data/bug-8980.php index b8699979bd..13b73b26ad 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-8980.php +++ b/tests/PHPStan/Rules/Functions/data/bug-8980.php @@ -11,4 +11,5 @@ function doFoo():void { funcA(); funcB(); + $func(); } From 0740a3cb6687860a1a89b1d894a8d1d2b314aa36 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 May 2026 13:18:16 +0200 Subject: [PATCH 6/7] Update bug-8980.php --- tests/PHPStan/Rules/Functions/data/bug-8980.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Functions/data/bug-8980.php b/tests/PHPStan/Rules/Functions/data/bug-8980.php index 13b73b26ad..5cdcfbb85c 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-8980.php +++ b/tests/PHPStan/Rules/Functions/data/bug-8980.php @@ -9,6 +9,7 @@ function doFoo():void { throw new \Exception(); } + // the function_exists() will only assure one of the functions to exist. funcA(); funcB(); $func(); From 6ff823c0160c8ba1e19e59c492102f718ccb634a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 24 May 2026 13:25:58 +0200 Subject: [PATCH 7/7] Update CallToNonExistentFunctionRuleTest.php --- .../Rules/Functions/CallToNonExistentFunctionRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index 78334e703b..54081d235c 100644 --- a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php @@ -269,12 +269,12 @@ public function testBug8980(): void $this->analyse([__DIR__ . '/data/bug-8980.php'], [ [ 'Function funcA not found.', - 12, + 13, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ 'Function funcB not found.', - 13, + 14, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], ]);