Skip to content

Commit b2a3bb0

Browse files
phpstan-botclaude
andcommitted
Generalize cast type narrowing for (int) in addition to (string)
Refactor the cast narrowing logic in TypeSpecifier::resolveNormalizedIdentical() to handle any Expr\Cast via a new determineCastProducingType() helper method. - (string)$expr === '' narrows to null|false|'' (unchanged behavior) - (int)$expr === 0 narrows to null|false|0|0.0|''|'0' This addresses the review feedback to generalize the approach beyond just string casts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 719330d commit b2a3bb0

2 files changed

Lines changed: 74 additions & 11 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2934,23 +2934,17 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
29342934
}
29352935
}
29362936

2937-
// (string)$expr === '' - propagate narrowing to inner expression
2937+
// (cast)$expr === value - propagate narrowing to inner expression
29382938
if (
29392939
!$context->null()
2940-
&& $unwrappedLeftExpr instanceof Expr\Cast\String_
2940+
&& $unwrappedLeftExpr instanceof Expr\Cast
29412941
) {
2942-
$rightConstantStrings = $rightType->getConstantStrings();
2943-
if (count($rightConstantStrings) === 1 && $rightConstantStrings[0]->getValue() === '') {
2944-
// Types that produce '' when cast to string: null, false, ''
2945-
$castToEmptyStringType = TypeCombinator::union(
2946-
new NullType(),
2947-
new ConstantBooleanType(false),
2948-
new ConstantStringType(''),
2949-
);
2942+
$castProducingType = $this->determineCastProducingType($unwrappedLeftExpr, $rightType);
2943+
if ($castProducingType !== null) {
29502944
$innerExpr = $unwrappedLeftExpr->expr;
29512945
$result = $this->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr);
29522946
return $result->unionWith(
2953-
$this->create($innerExpr, $castToEmptyStringType, $context, $scope)->setRootExpr($expr),
2947+
$this->create($innerExpr, $castProducingType, $context, $scope)->setRootExpr($expr),
29542948
);
29552949
}
29562950
}
@@ -3144,4 +3138,40 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
31443138
return (new SpecifiedTypes([], []))->setRootExpr($expr);
31453139
}
31463140

3141+
/**
3142+
* Given a cast expression and the value it's compared to,
3143+
* returns the union of types that produce that value when cast.
3144+
*/
3145+
private function determineCastProducingType(Expr\Cast $cast, Type $comparedType): ?Type
3146+
{
3147+
if ($cast instanceof Expr\Cast\String_) {
3148+
$constantStrings = $comparedType->getConstantStrings();
3149+
if (count($constantStrings) === 1 && $constantStrings[0]->getValue() === '') {
3150+
// Types that produce '' when cast to string: null, false, ''
3151+
return TypeCombinator::union(
3152+
new NullType(),
3153+
new ConstantBooleanType(false),
3154+
new ConstantStringType(''),
3155+
);
3156+
}
3157+
}
3158+
3159+
if ($cast instanceof Expr\Cast\Int_) {
3160+
$constantScalars = $comparedType->getConstantScalarValues();
3161+
if (count($constantScalars) === 1 && $constantScalars[0] === 0) {
3162+
// Types that produce 0 when cast to int: null, false, 0, 0.0, '', '0'
3163+
return TypeCombinator::union(
3164+
new NullType(),
3165+
new ConstantBooleanType(false),
3166+
new ConstantIntegerType(0),
3167+
new ConstantFloatType(0.0),
3168+
new ConstantStringType(''),
3169+
new ConstantStringType('0'),
3170+
);
3171+
}
3172+
}
3173+
3174+
return null;
3175+
}
3176+
31473177
}

tests/PHPStan/Analyser/nsrt/bug-8231.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,36 @@ function testBool(bool|string|null $x): void {
4040
assertType('non-empty-string|true', $x);
4141
}
4242
}
43+
44+
// (int) cast narrowing
45+
function testIntCast(int|null $x): void {
46+
if ((int)$x !== 0) {
47+
assertType('int<min, -1>|int<1, max>', $x);
48+
}
49+
}
50+
51+
function testIntCastIdentical(int|null $x): void {
52+
if ((int)$x === 0) {
53+
assertType('0|null', $x);
54+
} else {
55+
assertType('int<min, -1>|int<1, max>', $x);
56+
}
57+
}
58+
59+
function testIntCastWithString(int|string|null $x): void {
60+
if ((int)$x !== 0) {
61+
assertType("int<min, -1>|int<1, max>|non-falsy-string", $x);
62+
}
63+
}
64+
65+
function testIntCastWithFloat(float|null $x): void {
66+
if ((int)$x !== 0) {
67+
assertType('float', $x);
68+
}
69+
}
70+
71+
function testIntCastWithBool(bool|int|null $x): void {
72+
if ((int)$x !== 0) {
73+
assertType('int<min, -1>|int<1, max>|true', $x);
74+
}
75+
}

0 commit comments

Comments
 (0)