Skip to content

Commit 5c62c33

Browse files
phpstan-botclaude
authored andcommitted
Confirm the decimal-int-string round-trip through the type system
Instead of structurally requiring a `(string)` cast to wrap an `(int)` cast in that exact order, strip the cast/`strval()`/`intval()` layers off one operand to locate the compared value expression, then confirm the casts really compute the int-then-string round-trip via `Type::toInteger()->toString()`. The AST is now consulted only to match the value expression; the cast semantics are checked with the type system's cast methods. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3e7d13d commit 5c62c33

1 file changed

Lines changed: 38 additions & 46 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,51 +2969,58 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
29692969
}
29702970

29712971
/**
2972-
* Returns the inner expression E when $expr casts E to a string and back to an int,
2973-
* i.e. `(string) (int) E`, `strval(intval(E))` or any mix of the cast/function forms.
2974-
* This is the canonical "decimal integer string" round-trip that
2972+
* When $castExpr casts $valueExpr to an int and back to a string — i.e. the
2973+
* `(string) (int) $valueExpr` round-trip (in any combination of the (string)/(int)
2974+
* casts and the strval()/intval() function forms) — returns $valueExpr so the
2975+
* comparison `$castExpr === $valueExpr` can narrow it to a decimal / non-decimal
2976+
* integer string. This is the canonical "decimal integer string" round-trip that
29752977
* ConstantStringType::isDecimalIntegerString() checks with `(string) (int) $value === $value`.
2978+
*
2979+
* The cast forms are stripped from the AST only to match the value expression; whether
2980+
* the casts actually compute the int-then-string round-trip is confirmed through the
2981+
* type system via Type::toInteger() and Type::toString() rather than by relying on the
2982+
* exact order or shape of the casts.
29762983
*/
2977-
private function getDecimalIntegerStringCastedExpr(Expr $expr): ?Expr
2984+
private function getDecimalIntegerStringRoundTripExpr(Expr $castExpr, Expr $valueExpr, Scope $scope): ?Expr
29782985
{
2979-
$intCasted = $this->getStringCastedExpr($expr);
2980-
if ($intCasted === null) {
2986+
$valueType = $scope->getType($valueExpr);
2987+
if (!$valueType->isString()->yes()) {
29812988
return null;
29822989
}
29832990

2984-
return $this->getIntCastedExpr($intCasted);
2985-
}
2986-
2987-
private function getStringCastedExpr(Expr $expr): ?Expr
2988-
{
2989-
if ($expr instanceof Expr\Cast\String_) {
2990-
return $expr->expr;
2991+
$baseExpr = $castExpr;
2992+
$unwrapped = false;
2993+
while (($inner = $this->getCastInnerExpr($baseExpr)) !== null) {
2994+
$baseExpr = $inner;
2995+
$unwrapped = true;
29912996
}
29922997

29932998
if (
2994-
$expr instanceof FuncCall
2995-
&& $expr->name instanceof Name
2996-
&& !$expr->isFirstClassCallable()
2997-
&& strtolower($expr->name->toString()) === 'strval'
2998-
&& count($expr->getArgs()) === 1
2999+
!$unwrapped
3000+
|| $this->exprPrinter->printExpr($baseExpr) !== $this->exprPrinter->printExpr($valueExpr)
29993001
) {
3000-
return $expr->getArgs()[0]->value;
3002+
return null;
30013003
}
30023004

3003-
return null;
3005+
return $scope->getType($castExpr)->equals($valueType->toInteger()->toString())
3006+
? $valueExpr
3007+
: null;
30043008
}
30053009

3006-
private function getIntCastedExpr(Expr $expr): ?Expr
3010+
/**
3011+
* Strips a single (string)/(int) cast or strval()/intval() call, returning its inner expression.
3012+
*/
3013+
private function getCastInnerExpr(Expr $expr): ?Expr
30073014
{
3008-
if ($expr instanceof Expr\Cast\Int_) {
3015+
if ($expr instanceof Expr\Cast\String_ || $expr instanceof Expr\Cast\Int_) {
30093016
return $expr->expr;
30103017
}
30113018

30123019
if (
30133020
$expr instanceof FuncCall
30143021
&& $expr->name instanceof Name
30153022
&& !$expr->isFirstClassCallable()
3016-
&& strtolower($expr->name->toString()) === 'intval'
3023+
&& in_array(strtolower($expr->name->toString()), ['strval', 'intval'], true)
30173024
&& count($expr->getArgs()) === 1
30183025
) {
30193026
return $expr->getArgs()[0]->value;
@@ -3286,32 +3293,17 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
32863293

32873294
// (string) (int) $x === $x (and the strval(intval()) equivalents)
32883295
if (!$context->null()) {
3289-
$leftCastedExpr = $this->getDecimalIntegerStringCastedExpr($unwrappedLeftExpr);
3290-
$rightCastedExpr = $this->getDecimalIntegerStringCastedExpr($unwrappedRightExpr);
3291-
3292-
$decimalValueExpr = null;
3293-
if (
3294-
$leftCastedExpr !== null
3295-
&& $this->exprPrinter->printExpr($leftCastedExpr) === $this->exprPrinter->printExpr($unwrappedRightExpr)
3296-
) {
3297-
$decimalValueExpr = $unwrappedRightExpr;
3298-
} elseif (
3299-
$rightCastedExpr !== null
3300-
&& $this->exprPrinter->printExpr($rightCastedExpr) === $this->exprPrinter->printExpr($unwrappedLeftExpr)
3301-
) {
3302-
$decimalValueExpr = $unwrappedLeftExpr;
3303-
}
3296+
$decimalValueExpr = $this->getDecimalIntegerStringRoundTripExpr($unwrappedLeftExpr, $unwrappedRightExpr, $scope)
3297+
?? $this->getDecimalIntegerStringRoundTripExpr($unwrappedRightExpr, $unwrappedLeftExpr, $scope);
33043298

33053299
if ($decimalValueExpr !== null) {
33063300
$decimalValueType = $scope->getType($decimalValueExpr);
3307-
if ($decimalValueType->isString()->yes()) {
3308-
return $this->create(
3309-
$decimalValueExpr,
3310-
TypeCombinator::intersect($decimalValueType, new AccessoryDecimalIntegerStringType($context->falsey())),
3311-
TypeSpecifierContext::createTruthy(),
3312-
$scope,
3313-
)->setRootExpr($expr);
3314-
}
3301+
return $this->create(
3302+
$decimalValueExpr,
3303+
TypeCombinator::intersect($decimalValueType, new AccessoryDecimalIntegerStringType($context->falsey())),
3304+
TypeSpecifierContext::createTruthy(),
3305+
$scope,
3306+
)->setRootExpr($expr);
33153307
}
33163308
}
33173309

0 commit comments

Comments
 (0)