Skip to content

Commit 2b59c59

Browse files
phpstan-botclaude
andcommitted
Narrow function return type to true/false based on @phpstan-assert-if-true/false
Instead of patching the optionality check in ArrayFilterFunctionReturnTypeHelper, fix the root cause: when a function has @phpstan-assert-if-true annotations and the argument types already satisfy the asserted type, narrow the return type from bool to true (or false) at the FuncCallHandler level. This makes $scope->getType(checkFoo($fooVar)) return true when checkFoo has @phpstan-assert-if-true Foo $value and $fooVar is Foo, consistent with how conditional return types (@return ($value is Foo ? true : false)) already work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3aeb668 commit 2b59c59

2 files changed

Lines changed: 107 additions & 15 deletions

File tree

src/Analyser/ExprHandler/FuncCallHandler.php

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
use PHPStan\Type\ClosureType;
4747
use PHPStan\Type\Constant\ConstantArrayType;
4848
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
49+
use PHPStan\Type\Constant\ConstantBooleanType;
4950
use PHPStan\Type\ErrorType;
5051
use PHPStan\Type\GeneralizePrecision;
5152
use PHPStan\Type\IntegerRangeType;
@@ -70,6 +71,7 @@
7071
use function in_array;
7172
use function sprintf;
7273
use function str_starts_with;
74+
use function substr;
7375

7476
/**
7577
* @implements ExprHandler<FuncCall>
@@ -817,7 +819,107 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
817819
}
818820
}
819821

820-
return VoidToNullTypeTransformer::transform($parametersAcceptor->getReturnType(), $expr);
822+
$returnType = $parametersAcceptor->getReturnType();
823+
$returnType = $this->narrowReturnTypeByAssertions($returnType, $functionReflection, $normalizedNode ?? $expr, $scope, $parametersAcceptor);
824+
825+
return VoidToNullTypeTransformer::transform($returnType, $expr);
826+
}
827+
828+
private function narrowReturnTypeByAssertions(
829+
Type $returnType,
830+
FunctionReflection $functionReflection,
831+
FuncCall $call,
832+
MutatingScope $scope,
833+
ParametersAcceptor $parametersAcceptor,
834+
): Type
835+
{
836+
if (!$returnType->isBoolean()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes()) {
837+
return $returnType;
838+
}
839+
840+
$assertions = $functionReflection->getAsserts();
841+
$assertsIfFalse = $assertions->getAssertsIfFalse();
842+
$assertsIfTrue = $assertions->getAssertsIfTrue();
843+
844+
if (count($assertsIfFalse) === 0 && count($assertsIfTrue) === 0) {
845+
return $returnType;
846+
}
847+
848+
$argTypes = $this->buildArgTypesForAssertions($call, $scope, $parametersAcceptor);
849+
850+
foreach ($assertsIfFalse as $assert) {
851+
$param = $assert->getParameter();
852+
if ($param->describe() !== $param->getParameterName()) {
853+
continue;
854+
}
855+
856+
$paramName = substr($param->getParameterName(), 1);
857+
if (!isset($argTypes[$paramName])) {
858+
continue;
859+
}
860+
861+
$actualType = $argTypes[$paramName];
862+
$assertedType = $assert->getType();
863+
864+
if ($assert->isNegated()) {
865+
if ($assertedType->isSuperTypeOf($actualType)->yes()) {
866+
return new ConstantBooleanType(true);
867+
}
868+
} else {
869+
if ($assertedType->isSuperTypeOf($actualType)->no()) {
870+
return new ConstantBooleanType(true);
871+
}
872+
}
873+
}
874+
875+
foreach ($assertsIfTrue as $assert) {
876+
$param = $assert->getParameter();
877+
if ($param->describe() !== $param->getParameterName()) {
878+
continue;
879+
}
880+
881+
$paramName = substr($param->getParameterName(), 1);
882+
if (!isset($argTypes[$paramName])) {
883+
continue;
884+
}
885+
886+
$actualType = $argTypes[$paramName];
887+
$assertedType = $assert->getType();
888+
889+
if ($assert->isNegated()) {
890+
if ($assertedType->isSuperTypeOf($actualType)->yes()) {
891+
return new ConstantBooleanType(false);
892+
}
893+
} else {
894+
if ($assertedType->isSuperTypeOf($actualType)->no()) {
895+
return new ConstantBooleanType(false);
896+
}
897+
}
898+
}
899+
900+
return $returnType;
901+
}
902+
903+
/**
904+
* @return array<string, Type>
905+
*/
906+
private function buildArgTypesForAssertions(FuncCall $call, MutatingScope $scope, ParametersAcceptor $parametersAcceptor): array
907+
{
908+
$argTypes = [];
909+
$parameters = $parametersAcceptor->getParameters();
910+
foreach ($call->getArgs() as $i => $arg) {
911+
$name = null;
912+
if ($arg->name !== null) {
913+
$name = $arg->name->toString();
914+
} elseif (isset($parameters[$i])) {
915+
$name = $parameters[$i]->getName();
916+
}
917+
if ($name !== null) {
918+
$argTypes[$name] = $scope->getType($arg->value);
919+
}
920+
}
921+
922+
return $argTypes;
821923
}
822924

823925
private function getDynamicFunctionReturnType(MutatingScope $scope, FuncCall $normalizedNode, FunctionReflection $functionReflection): ?Type

src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -260,22 +260,12 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type
260260
return [new NeverType(), new NeverType(), false];
261261
}
262262

263-
$truthyScope = $scope->filterByTruthyValue($expr);
264-
265-
$optional = !$booleanResult->isTrue()->yes();
266-
if ($optional) {
267-
$falseyScope = $scope->filterByFalseyValue($expr);
268-
$falseyItemType = $itemVarName !== null ? $falseyScope->getVariableType($itemVarName) : $itemType;
269-
$falseyKeyType = $keyVarName !== null ? $falseyScope->getVariableType($keyVarName) : $keyType;
270-
if ($falseyItemType instanceof NeverType || $falseyKeyType instanceof NeverType) {
271-
$optional = false;
272-
}
273-
}
263+
$scope = $scope->filterByTruthyValue($expr);
274264

275265
return [
276-
$keyVarName !== null ? $truthyScope->getVariableType($keyVarName) : $keyType,
277-
$itemVarName !== null ? $truthyScope->getVariableType($itemVarName) : $itemType,
278-
$optional,
266+
$keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType,
267+
$itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType,
268+
!$booleanResult->isTrue()->yes(),
279269
];
280270
}
281271

0 commit comments

Comments
 (0)