diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index af128a42272..31da7cb5e1e 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -515,11 +515,12 @@ public function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic ); } - private function hasThrowOnFailureFlag(?Type $flagsType, PhpVersions $phpVersions): TrinaryLogic + public function hasThrowOnFailureFlag(?Type $flagsType, PhpVersions $phpVersions): TrinaryLogic { - return $phpVersions->hasFilterThrowOnFailureConstant()->no() - ? TrinaryLogic::createNo() - : $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType); + // Flag values are resolved from the runtime. Before PHP 8.5, FILTER_FLAG_GLOBAL_RANGE used the bit + // that PHP 8.5 assigned to FILTER_THROW_ON_FAILURE, so a set bit only definitely means + // FILTER_THROW_ON_FAILURE when the analysed code definitely runs on PHP 8.5+. + return $phpVersions->hasFilterThrowOnFailureConstant()->and($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)); } private function getFlagsValue(Type $exprType): Type diff --git a/src/Type/Php/FilterFunctionsThrowTypeExtension.php b/src/Type/Php/FilterFunctionsThrowTypeExtension.php index 6d5d78610be..81069b5d588 100644 --- a/src/Type/Php/FilterFunctionsThrowTypeExtension.php +++ b/src/Type/Php/FilterFunctionsThrowTypeExtension.php @@ -37,15 +37,16 @@ public function getThrowTypeFromFunctionCall( Scope $scope, ): ?Type { + $phpVersions = $scope->getPhpVersion(); if ( - $scope->getPhpVersion()->hasFilterThrowOnFailureConstant()->no() + $phpVersions->hasFilterThrowOnFailureConstant()->no() || !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null) ) { return null; } foreach ($this->filterFunctionFlagsHelper->getFlagsTypes($functionReflection, $funcCall, $scope) as $flagsType) { - if ($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->no()) { + if ($this->filterFunctionReturnTypeHelper->hasThrowOnFailureFlag($flagsType, $phpVersions)->no()) { continue; } diff --git a/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions-php82.php b/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions-php82.php new file mode 100644 index 00000000000..ca677ae8a55 --- /dev/null +++ b/tests/PHPStan/Analyser/data/scope-php-version-range-return-type-extensions-php82.php @@ -0,0 +1,13 @@ += 8.2 + +namespace ScopePhpVersionRangeReturnTypeExtensions; + +use function PHPStan\Testing\assertType; + +function filterFlagGlobalRange(string $address): void +{ + assertType('non-falsy-string|false', filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)); + assertType('non-falsy-string|false', filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_GLOBAL_RANGE)); + assertType('non-falsy-string|null', filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE | FILTER_NULL_ON_FAILURE)); + assertType('array{ip: non-falsy-string|false|null}', filter_input_array(INPUT_GET, ['ip' => ['filter' => FILTER_VALIDATE_IP, 'flags' => FILTER_FLAG_GLOBAL_RANGE]])); +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php index 85cbc4a08e4..71bea413312 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -24,12 +25,23 @@ protected function getRule(): Rule public function testThrowTypesInPhpVersionRange(): void { - $this->analyse([__DIR__ . '/data/throw-type-php-versions.php'], [ + $errors = [ [ 'Dead catch - ValueError is never thrown in the try block.', 19, ], - ]); + ]; + + if (PHP_VERSION_ID >= 80500) { + // Before PHP 8.5, FILTER_FLAG_GLOBAL_RANGE has the same runtime value as FILTER_THROW_ON_FAILURE on PHP 8.5, + // so it might throw Filter\FilterFailedException in a PHP version range that only maybe includes PHP 8.5. + $errors[] = [ + 'Dead catch - Filter\\FilterFailedException is never thrown in the try block.', + 58, + ]; + } + + $this->analyse([__DIR__ . '/data/throw-type-php-versions.php'], $errors); } public static function getAdditionalConfigFiles(): array diff --git a/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php b/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php index f3f83eedf7d..1ecf8296627 100644 --- a/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php +++ b/tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php @@ -51,4 +51,22 @@ public function doStrSplit(string $s, int $length): void } } + public function doFilterVar(string $address): void + { + try { + $a = filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE); + } catch (\Filter\FilterFailedException $e) { + + } + } + + public function doFilterVarThrowOnFailure(string $address): void + { + try { + $a = filter_var($address, FILTER_VALIDATE_IP, FILTER_THROW_ON_FAILURE); + } catch (\Filter\FilterFailedException $e) { + + } + } + }