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