Skip to content

Commit 9196d03

Browse files
phpstan-botclaude
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 ff4a32d commit 9196d03

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
@@ -2962,51 +2962,58 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
29622962
}
29632963

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

2977-
return $this->getIntCastedExpr($intCasted);
2978-
}
2979-
2980-
private function getStringCastedExpr(Expr $expr): ?Expr
2981-
{
2982-
if ($expr instanceof Expr\Cast\String_) {
2983-
return $expr->expr;
2984+
$baseExpr = $castExpr;
2985+
$unwrapped = false;
2986+
while (($inner = $this->getCastInnerExpr($baseExpr)) !== null) {
2987+
$baseExpr = $inner;
2988+
$unwrapped = true;
29842989
}
29852990

29862991
if (
2987-
$expr instanceof FuncCall
2988-
&& $expr->name instanceof Name
2989-
&& !$expr->isFirstClassCallable()
2990-
&& strtolower($expr->name->toString()) === 'strval'
2991-
&& count($expr->getArgs()) === 1
2992+
!$unwrapped
2993+
|| $this->exprPrinter->printExpr($baseExpr) !== $this->exprPrinter->printExpr($valueExpr)
29922994
) {
2993-
return $expr->getArgs()[0]->value;
2995+
return null;
29942996
}
29952997

2996-
return null;
2998+
return $scope->getType($castExpr)->equals($valueType->toInteger()->toString())
2999+
? $valueExpr
3000+
: null;
29973001
}
29983002

2999-
private function getIntCastedExpr(Expr $expr): ?Expr
3003+
/**
3004+
* Strips a single (string)/(int) cast or strval()/intval() call, returning its inner expression.
3005+
*/
3006+
private function getCastInnerExpr(Expr $expr): ?Expr
30003007
{
3001-
if ($expr instanceof Expr\Cast\Int_) {
3008+
if ($expr instanceof Expr\Cast\String_ || $expr instanceof Expr\Cast\Int_) {
30023009
return $expr->expr;
30033010
}
30043011

30053012
if (
30063013
$expr instanceof FuncCall
30073014
&& $expr->name instanceof Name
30083015
&& !$expr->isFirstClassCallable()
3009-
&& strtolower($expr->name->toString()) === 'intval'
3016+
&& in_array(strtolower($expr->name->toString()), ['strval', 'intval'], true)
30103017
&& count($expr->getArgs()) === 1
30113018
) {
30123019
return $expr->getArgs()[0]->value;
@@ -3279,32 +3286,17 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
32793286

32803287
// (string) (int) $x === $x (and the strval(intval()) equivalents)
32813288
if (!$context->null()) {
3282-
$leftCastedExpr = $this->getDecimalIntegerStringCastedExpr($unwrappedLeftExpr);
3283-
$rightCastedExpr = $this->getDecimalIntegerStringCastedExpr($unwrappedRightExpr);
3284-
3285-
$decimalValueExpr = null;
3286-
if (
3287-
$leftCastedExpr !== null
3288-
&& $this->exprPrinter->printExpr($leftCastedExpr) === $this->exprPrinter->printExpr($unwrappedRightExpr)
3289-
) {
3290-
$decimalValueExpr = $unwrappedRightExpr;
3291-
} elseif (
3292-
$rightCastedExpr !== null
3293-
&& $this->exprPrinter->printExpr($rightCastedExpr) === $this->exprPrinter->printExpr($unwrappedLeftExpr)
3294-
) {
3295-
$decimalValueExpr = $unwrappedLeftExpr;
3296-
}
3289+
$decimalValueExpr = $this->getDecimalIntegerStringRoundTripExpr($unwrappedLeftExpr, $unwrappedRightExpr, $scope)
3290+
?? $this->getDecimalIntegerStringRoundTripExpr($unwrappedRightExpr, $unwrappedLeftExpr, $scope);
32973291

32983292
if ($decimalValueExpr !== null) {
32993293
$decimalValueType = $scope->getType($decimalValueExpr);
3300-
if ($decimalValueType->isString()->yes()) {
3301-
return $this->create(
3302-
$decimalValueExpr,
3303-
TypeCombinator::intersect($decimalValueType, new AccessoryDecimalIntegerStringType($context->falsey())),
3304-
TypeSpecifierContext::createTruthy(),
3305-
$scope,
3306-
)->setRootExpr($expr);
3307-
}
3294+
return $this->create(
3295+
$decimalValueExpr,
3296+
TypeCombinator::intersect($decimalValueType, new AccessoryDecimalIntegerStringType($context->falsey())),
3297+
TypeSpecifierContext::createTruthy(),
3298+
$scope,
3299+
)->setRootExpr($expr);
33083300
}
33093301
}
33103302

0 commit comments

Comments
 (0)