Skip to content
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,9 @@ public function supportsObjectsInArraySumProduct(): bool
return $this->versionId >= 80300;
}

public function hasFilterThrowOnFailureConstant(): bool
{
return $this->versionId >= 80500;
}

}
37 changes: 22 additions & 15 deletions src/Rules/Functions/FilterVarRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
Expand All @@ -23,6 +24,7 @@ final class FilterVarRule implements Rule
public function __construct(
private ReflectionProvider $reflectionProvider,
private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper,
private PhpVersion $phpVersion,
)
{
}
Expand All @@ -44,23 +46,28 @@ public function processNode(Node $node, Scope $scope): array

$args = $node->getArgs();

if ($this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) {
if (count($args) < 3) {
return [];
}
if (count($args) < 3) {
return [];
}

if (
!$this->phpVersion->hasFilterThrowOnFailureConstant()
|| !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)
) {
return [];
}

$flagsType = $scope->getType($args[2]->value);
$flagsType = $scope->getType($args[2]->value);

if ($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
->and($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType))
->yes()
) {
return [
RuleErrorBuilder::message('Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.')
->identifier('filterVar.nullOnFailureAndThrowOnFailure')
->build(),
];
}
if ($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)
->and($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType))
->yes()
) {
return [
RuleErrorBuilder::message('Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.')
->identifier('filterVar.nullOnFailureAndThrowOnFailure')
->build(),
];
}

return [];
Expand Down
4 changes: 3 additions & 1 deletion src/Type/Php/FilterFunctionReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,11 @@

$inputIsArray = $inputType->isArray();
$hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType);
$hasThrowOnFailureFlag = $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType);
$hasThrowOnFailureFlag = $this->phpVersion->hasFilterThrowOnFailureConstant()
? $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)
: TrinaryLogic::createNo();
if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) {
if ($hasThrowOnFailureFlag->yes()) {

Check warning on line 158 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ ? $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType) : TrinaryLogic::createNo(); if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) { - if ($hasThrowOnFailureFlag->yes()) { + if (!$hasThrowOnFailureFlag->no()) { return new ErrorType(); }

Check warning on line 158 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ ? $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType) : TrinaryLogic::createNo(); if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) { - if ($hasThrowOnFailureFlag->yes()) { + if (!$hasThrowOnFailureFlag->no()) { return new ErrorType(); }
return new ErrorType();
}

Expand Down Expand Up @@ -198,13 +200,13 @@
}
}

if ($hasThrowOnFailureFlag->yes()) {

Check warning on line 203 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } } - if ($hasThrowOnFailureFlag->yes()) { + if (!$hasThrowOnFailureFlag->no()) { $type = TypeCombinator::remove($type, $defaultType); }

Check warning on line 203 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } } - if ($hasThrowOnFailureFlag->yes()) { + if (!$hasThrowOnFailureFlag->no()) { $type = TypeCombinator::remove($type, $defaultType); }
$type = TypeCombinator::remove($type, $defaultType);
}

if ($hasRequireArrayFlag->yes()) {
$type = new ArrayType($inputArrayKeyType ?? $mixedType, $type);
if (!$hasThrowOnFailureFlag->yes() && !$inputIsArray->yes()) {

Check warning on line 209 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($hasRequireArrayFlag->yes()) { $type = new ArrayType($inputArrayKeyType ?? $mixedType, $type); - if (!$hasThrowOnFailureFlag->yes() && !$inputIsArray->yes()) { + if ($hasThrowOnFailureFlag->no() && !$inputIsArray->yes()) { $type = TypeCombinator::union($type, $defaultType); } }

Check warning on line 209 in src/Type/Php/FilterFunctionReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($hasRequireArrayFlag->yes()) { $type = new ArrayType($inputArrayKeyType ?? $mixedType, $type); - if (!$hasThrowOnFailureFlag->yes() && !$inputIsArray->yes()) { + if ($hasThrowOnFailureFlag->no() && !$inputIsArray->yes()) { $type = TypeCombinator::union($type, $defaultType); } }
$type = TypeCombinator::union($type, $defaultType);
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/Type/Php/FilterVarThrowTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Constant\ConstantIntegerType;
Expand All @@ -20,6 +21,7 @@ final class FilterVarThrowTypeExtension implements DynamicFunctionThrowTypeExten

public function __construct(
private ReflectionProvider $reflectionProvider,
private PhpVersion $phpVersion,
)
{
}
Expand All @@ -28,8 +30,7 @@ public function isFunctionSupported(
FunctionReflection $functionReflection,
): bool
{
return $functionReflection->getName() === 'filter_var'
&& $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null);
return $functionReflection->getName() === 'filter_var';
}

public function getThrowTypeFromFunctionCall(
Expand All @@ -42,6 +43,13 @@ public function getThrowTypeFromFunctionCall(
return null;
}

if (
!$this->phpVersion->hasFilterThrowOnFailureConstant()
|| !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)
) {
return null;
}

$flagsExpr = $funcCall->getArgs()[3]->value;
$flagsType = $scope->getType($flagsExpr);

Expand Down
29 changes: 29 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14397.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php // lint >= 8.2

declare(strict_types = 1);

namespace Bug14397;

use PHPStan\TrinaryLogic;

use function filter_var;
use function PHPStan\Testing\assertType;
use function PHPStan\Testing\assertVariableCertainty;

function test(string $ipAddress): void
{
assertType('non-falsy-string|false', filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_GLOBAL_RANGE));
}

function test2(mixed $mixed): void
{
try {
filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE);
$foo = 1;
} catch (\Filter\FilterFailedException $e) {
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
}

assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE));
assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FLAG_GLOBAL_RANGE]));
}
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/Functions/FilterVarRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Rules\Functions;

use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Php\FilterFunctionReturnTypeHelper;
Expand All @@ -16,6 +17,7 @@ protected function getRule(): Rule
return new FilterVarRule(
self::createReflectionProvider(),
self::getContainer()->getByType(FilterFunctionReturnTypeHelper::class),
self::getContainer()->getByType(PhpVersion::class),
);
}

Expand All @@ -29,4 +31,16 @@ public function testRule(): void
]);
}

#[RequiresPhp('>= 8.2')]
public function testRuleWithGlobalRange(): void
{
$this->analyse([__DIR__ . '/data/filter_var_null_and_throw_global_range.php'], []);
}

#[RequiresPhp('>= 8.5')]
public function testRuleGlobalRangePhp85(): void
{
$this->analyse([__DIR__ . '/data/filter_var_null_and_global_range_php85.php'], []);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php // lint >= 8.5

namespace FilterVarNullAndGlobalRangePhp85;

filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE);

$flag = FILTER_NULL_ON_FAILURE|FILTER_FLAG_GLOBAL_RANGE;
filter_var(100, FILTER_VALIDATE_INT, $flag);

filter_var(
'johndoe',
FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE]
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php // lint >= 8.2

namespace FilterVarNullAndThrowGlobalRange;

filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE);

$flag = FILTER_NULL_ON_FAILURE|FILTER_FLAG_GLOBAL_RANGE;
filter_var(100, FILTER_VALIDATE_INT, $flag);

filter_var(
'johndoe',
FILTER_VALIDATE_REGEXP,
['options' => ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE]
);
Loading