Skip to content
Merged
18 changes: 14 additions & 4 deletions src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,22 @@
return [new NeverType(), new NeverType(), false];
}

$scope = $scope->filterByTruthyValue($expr);
$truthyScope = $scope->filterByTruthyValue($expr);

$optional = !$booleanResult->isTrue()->yes();

Check warning on line 265 in src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\LooseBooleanMutator": @@ @@ $truthyScope = $scope->filterByTruthyValue($expr); - $optional = !$booleanResult->isTrue()->yes(); + $optional = !$booleanResult->toBoolean()->isTrue()->yes(); if ($optional) { $falseyScope = $scope->filterByFalseyValue($expr); $falseyItemType = $itemVarName !== null ? $falseyScope->getVariableType($itemVarName) : $itemType;

Check warning on line 265 in src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\LooseBooleanMutator": @@ @@ $truthyScope = $scope->filterByTruthyValue($expr); - $optional = !$booleanResult->isTrue()->yes(); + $optional = !$booleanResult->toBoolean()->isTrue()->yes(); if ($optional) { $falseyScope = $scope->filterByFalseyValue($expr); $falseyItemType = $itemVarName !== null ? $falseyScope->getVariableType($itemVarName) : $itemType;
if ($optional) {
$falseyScope = $scope->filterByFalseyValue($expr);
$falseyItemType = $itemVarName !== null ? $falseyScope->getVariableType($itemVarName) : $itemType;
$falseyKeyType = $keyVarName !== null ? $falseyScope->getVariableType($keyVarName) : $keyType;
if ($falseyItemType instanceof NeverType || $falseyKeyType instanceof NeverType) {
$optional = false;
}
}

return [
$keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType,
$itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType,
!$booleanResult->isTrue()->yes(),
$keyVarName !== null ? $truthyScope->getVariableType($keyVarName) : $keyType,
$itemVarName !== null ? $truthyScope->getVariableType($itemVarName) : $itemType,
$optional,
];
}

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

declare(strict_types = 1);

namespace Bug11730;

use function PHPStan\Testing\assertType;

class Foo {
public bool $active = false;
}

/** @return ($value is Foo ? true : false) */
function isFoo(mixed $value): bool {
return $value instanceof Foo;
}

/** @phpstan-assert-if-true Foo $value */
function checkFoo(mixed $value): bool {
return $value instanceof Foo;
}

$data = [new Foo, new Foo];

assertType('array{Bug11730\Foo, Bug11730\Foo}', array_filter($data, isFoo(...)));
assertType('array{Bug11730\Foo, Bug11730\Foo}', array_filter($data, checkFoo(...)));

/** @param array{Foo|int, Foo|int} $data */
function doFoo ($data) {
assertType('array{0?: Bug11730\Foo, 1?: Bug11730\Foo}', array_filter($data, isFoo(...)));
assertType('array{0?: Bug11730\Foo, 1?: Bug11730\Foo}', array_filter($data, checkFoo(...)));
}

class Asserter {
/** @return ($value is Foo ? true : false) */
function isFoo(mixed $value): bool {
return $value instanceof Foo;
}

/** @phpstan-assert-if-true Foo $value */
function checkFoo(mixed $value): bool {
return $value instanceof Foo;
}

/** @return ($value is Foo ? true : false) */
static function isFooStatic(mixed $value): bool {
return $value instanceof Foo;
}

/** @phpstan-assert-if-true Foo $value */
static function checkFooStatic(mixed $value): bool {
return $value instanceof Foo;
}
}

function doBar(): void {
$data = [new Foo, new Foo];
$asserter = new Asserter;

assertType('array{Bug11730\Foo, Bug11730\Foo}', array_filter($data, $asserter->isFoo(...)));
assertType('array{Bug11730\Foo, Bug11730\Foo}', array_filter($data, $asserter->checkFoo(...)));

assertType('array{Bug11730\Foo, Bug11730\Foo}', array_filter($data, Asserter::isFooStatic(...)));
assertType('array{Bug11730\Foo, Bug11730\Foo}', array_filter($data, Asserter::checkFooStatic(...)));
}

/** @param array{Foo|int, Foo|int} $data */
function doBaz(array $data): void {
$asserter = new Asserter;

assertType('array{0?: Bug11730\Foo, 1?: Bug11730\Foo}', array_filter($data, $asserter->isFoo(...)));
assertType('array{0?: Bug11730\Foo, 1?: Bug11730\Foo}', array_filter($data, $asserter->checkFoo(...)));

assertType('array{0?: Bug11730\Foo, 1?: Bug11730\Foo}', array_filter($data, Asserter::isFooStatic(...)));
assertType('array{0?: Bug11730\Foo, 1?: Bug11730\Foo}', array_filter($data, Asserter::checkFooStatic(...)));
}

class CallableAsserterConditionalReturn {
/** @return ($value is Foo ? true : false) */
function __invoke(mixed $value): bool {
return $value instanceof Foo;
}
}

$data = [new Foo, new Foo];
assertType('array<0|1, Bug11730\Foo>', array_filter($data, new CallableAsserterConditionalReturn())); // could be array{Foo, Foo}

class CallableAssertIfTrue {
/** @phpstan-assert-if-true Foo $value */
function __invoke(mixed $value): bool {
return $value instanceof Foo;
}
}

$data = [new Foo, new Foo];
assertType('array<0|1, Bug11730\Foo>', array_filter($data, new CallableAssertIfTrue())); // could be array{Foo, Foo}

/** @phpstan-assert-if-true =Foo $value */
function checkFooAndMore(mixed $value): bool {
return $value instanceof Foo && $value->active;
}

$data = [new Foo, new Foo];
assertType('array{0?: Bug11730\Foo, 1?: Bug11730\Foo}', array_filter($data, checkFooAndMore(...)));
Loading