Skip to content
9 changes: 5 additions & 4 deletions src/Type/Php/FilterFunctionReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/Type/Php/FilterFunctionsThrowTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php // lint >= 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]]));
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<CatchWithUnthrownExceptionRule>
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/throw-type-php-versions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

}
}

}
Loading