Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -2942,6 +2942,30 @@
)->setRootExpr($expr);
}

// filter_var($a, FILTER_VALIDATE_*) === false
if (
!$context->null()
&& $unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& strtolower($unwrappedLeftExpr->name->toString()) === 'filter_var'
&& $rightType->isFalse()->yes()

Check warning on line 2951 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ && $unwrappedLeftExpr instanceof FuncCall && $unwrappedLeftExpr->name instanceof Name && strtolower($unwrappedLeftExpr->name->toString()) === 'filter_var' - && $rightType->isFalse()->yes() + && !$rightType->toBoolean()->isFalse()->no() && count($unwrappedLeftExpr->getArgs()) >= 1 ) { $funcCallTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);

Check warning on line 2951 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\LooseBooleanMutator": @@ @@ && $unwrappedLeftExpr instanceof FuncCall && $unwrappedLeftExpr->name instanceof Name && strtolower($unwrappedLeftExpr->name->toString()) === 'filter_var' - && $rightType->isFalse()->yes() + && $rightType->toBoolean()->isFalse()->yes() && count($unwrappedLeftExpr->getArgs()) >= 1 ) { $funcCallTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);

Check warning on line 2951 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ && $unwrappedLeftExpr instanceof FuncCall && $unwrappedLeftExpr->name instanceof Name && strtolower($unwrappedLeftExpr->name->toString()) === 'filter_var' - && $rightType->isFalse()->yes() + && !$rightType->toBoolean()->isFalse()->no() && count($unwrappedLeftExpr->getArgs()) >= 1 ) { $funcCallTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);

Check warning on line 2951 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\LooseBooleanMutator": @@ @@ && $unwrappedLeftExpr instanceof FuncCall && $unwrappedLeftExpr->name instanceof Name && strtolower($unwrappedLeftExpr->name->toString()) === 'filter_var' - && $rightType->isFalse()->yes() + && $rightType->toBoolean()->isFalse()->yes() && count($unwrappedLeftExpr->getArgs()) >= 1 ) { $funcCallTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
&& count($unwrappedLeftExpr->getArgs()) >= 1
) {
$funcCallTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);

if ($context->false()) {

Check warning on line 2956 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ ) { $funcCallTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); - if ($context->false()) { + if ($context->falsey()) { $argSpecifiedTypes = $this->specifyTypesInCondition( $scope, $leftExpr,

Check warning on line 2956 in src/Analyser/TypeSpecifier.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrueTruthyFalseFalseyTypeSpecifierContextMutator": @@ @@ ) { $funcCallTypes = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); - if ($context->false()) { + if ($context->falsey()) { $argSpecifiedTypes = $this->specifyTypesInCondition( $scope, $leftExpr,
$argSpecifiedTypes = $this->specifyTypesInCondition(
$scope,
$leftExpr,
TypeSpecifierContext::createTrue(),
)->setRootExpr($expr);

return $funcCallTypes->unionWith($argSpecifiedTypes);
}

return $funcCallTypes;
}

// get_class($a) === 'Foo'
if (
$context->true()
Expand Down
31 changes: 31 additions & 0 deletions src/Type/Php/FilterFunctionReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,37 @@ private function getFlagsValue(Type $exprType): Type
);
}

public function getInputNarrowingType(?Type $filterType, ?Type $flagsType): ?Type
{
if ($filterType === null) {
return null;
}

if (!$filterType instanceof ConstantIntegerType) {
return null;
}

$filterValue = $filterType->getValue();

if (($filterValue & self::VALIDATION_FILTER_BITMASK) === 0) {
return null;
}

$returnType = $this->getType(new StringType(), $filterType, $flagsType);
$successType = TypeCombinator::remove($returnType, new ConstantBooleanType(false));
$successType = TypeCombinator::remove($successType, new NullType());

if (!$successType->isString()->yes() || !$successType->isNonEmptyString()->yes()) {
return null;
}

if ($successType->isNonFalsyString()->yes()) {
return new AccessoryNonFalsyStringType();
}

return new AccessoryNonEmptyStringType();
}

private function canStringBeSanitized(int $filterValue, ?Type $flagsType): TrinaryLogic
{
// If it is a validation filter, the string will not be changed
Expand Down
56 changes: 56 additions & 0 deletions src/Type/Php/FilterVarTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use function count;
use function strtolower;

#[AutowiredService]
final class FilterVarTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private TypeSpecifier $typeSpecifier;

public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
{
return strtolower($functionReflection->getName()) === 'filter_var'
&& $context->truthy();
}

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
$args = $node->getArgs();
if (count($args) < 2) {
return new SpecifiedTypes();
}

$filterType = $scope->getType($args[1]->value);
$flagsType = isset($args[2]) ? $scope->getType($args[2]->value) : null;

$narrowingType = $this->filterFunctionReturnTypeHelper->getInputNarrowingType($filterType, $flagsType);
if ($narrowingType === null) {
return new SpecifiedTypes();
}

return $this->typeSpecifier->create($args[0]->value, $narrowingType, $context, $scope);
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}

}
138 changes: 138 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14486.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php declare(strict_types = 1);

namespace Bug14486;

use function PHPStan\Testing\assertType;

function assertEmailInline(string $email): void
{
assertType('string', $email);

if (false === filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException(sprintf('Invalid email "%s".', $email));
}

assertType('non-falsy-string', $email);
}

function assertEmailNotEquals(string $email): void
{
if (filter_var($email, FILTER_VALIDATE_EMAIL) !== false) {
assertType('non-falsy-string', $email);
}
}

function assertEmailTruthy(string $email): void
{
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
assertType('non-falsy-string', $email);
}
}

function assertEmailNegated(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return;
}

assertType('non-falsy-string', $email);
}

function assertIpInline(string $ip): void
{
if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException('Invalid IP');
}

assertType('non-falsy-string', $ip);
}

function assertUrlInline(string $url): void
{
if (false === filter_var($url, FILTER_VALIDATE_URL)) {
throw new \InvalidArgumentException('Invalid URL');
}

assertType('non-falsy-string', $url);
}

function assertMacInline(string $mac): void
{
if (false === filter_var($mac, FILTER_VALIDATE_MAC)) {
throw new \InvalidArgumentException('Invalid MAC');
}

assertType('non-falsy-string', $mac);
}

function noNarrowingForDomain(string $domain): void
{
if (false === filter_var($domain, FILTER_VALIDATE_DOMAIN)) {
throw new \InvalidArgumentException('Invalid domain');
}

// FILTER_VALIDATE_DOMAIN return type in filter map is just string, so no narrowing
assertType('string', $domain);
}

function noNarrowingForRegexp(string $str): void
{
if (false === filter_var($str, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '//']])) {
throw new \InvalidArgumentException('Does not match');
}

// FILTER_VALIDATE_REGEXP could match empty string, no narrowing
assertType('string', $str);
}

function noNarrowingForDefault(string $str): void
{
if (false === filter_var($str, FILTER_DEFAULT)) {
throw new \InvalidArgumentException('Invalid');
}

// FILTER_DEFAULT is not a validation filter, no narrowing
assertType('string', $str);
}

function noNarrowingWithoutFilter(string $str): void
{
if (filter_var($str)) {
// No second argument, uses FILTER_DEFAULT, no narrowing
assertType('string', $str);
}
}

function noNarrowingInFalsyBranch(string $email): void
{
if (false === filter_var($email, FILTER_VALIDATE_EMAIL)) {
// Filter failed, but $email could still be any string
assertType('string', $email);
}
}

function filterWithNullOnFailure(string $email): void
{
$result = filter_var($email, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE);
assertType('non-falsy-string|null', $result);

if ($result !== null) {
assertType('non-falsy-string', $result);
}
}

function noNarrowingForValidateInt(string $str): void
{
if (filter_var($str, FILTER_VALIDATE_INT) !== false) {
// FILTER_VALIDATE_INT returns int, not string - no string narrowing
assertType('string', $str);
}
}

function noNarrowingForSanitize(string $str): void
{
if (filter_var($str, FILTER_SANITIZE_EMAIL)) {
// FILTER_SANITIZE_EMAIL is a sanitize filter, not validation
assertType('string', $str);
}
}
Loading