@@ -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