203
203
use function is_array ;
204
204
use function is_int ;
205
205
use function is_string ;
206
+ use function ksort ;
206
207
use function sprintf ;
207
208
use function str_starts_with ;
208
209
use function strtolower ;
209
210
use function trim ;
210
211
use const PHP_VERSION_ID ;
212
+ use const SORT_NUMERIC ;
211
213
212
214
class NodeScopeResolver
213
215
{
@@ -3372,6 +3374,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
3372
3374
$ hasYield = true ;
3373
3375
} elseif ($ expr instanceof Expr \Match_) {
3374
3376
$ deepContext = $ context ->enterDeep ();
3377
+ $ condType = $ scope ->getType ($ expr ->cond );
3375
3378
$ condResult = $ this ->processExprNode ($ stmt , $ expr ->cond , $ scope , $ nodeCallback , $ deepContext );
3376
3379
$ scope = $ condResult ->getScope ();
3377
3380
$ hasYield = $ condResult ->hasYield ();
@@ -3381,11 +3384,137 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
3381
3384
$ armNodes = [];
3382
3385
$ hasDefaultCond = false ;
3383
3386
$ hasAlwaysTrueCond = false ;
3384
- foreach ($ expr ->arms as $ arm ) {
3387
+ $ arms = $ expr ->arms ;
3388
+ if ($ condType ->isEnum ()->yes ()) {
3389
+ // enum match analysis would work even without this if branch
3390
+ // but would be much slower
3391
+ // this avoids using ObjectType::$subtractedType which is slow for huge enums
3392
+ // because of repeated union type normalization
3393
+ $ enumCases = $ condType ->getEnumCases ();
3394
+ if (count ($ enumCases ) > 0 ) {
3395
+ $ indexedEnumCases = [];
3396
+ foreach ($ enumCases as $ enumCase ) {
3397
+ $ indexedEnumCases [strtolower ($ enumCase ->getClassName ())][$ enumCase ->getEnumCaseName ()] = $ enumCase ;
3398
+ }
3399
+ $ unusedIndexedEnumCases = $ indexedEnumCases ;
3400
+ foreach ($ arms as $ i => $ arm ) {
3401
+ if ($ arm ->conds === null ) {
3402
+ continue ;
3403
+ }
3404
+
3405
+ $ condNodes = [];
3406
+ $ conditionCases = [];
3407
+ foreach ($ arm ->conds as $ cond ) {
3408
+ if (!$ cond instanceof Expr \ClassConstFetch) {
3409
+ continue 2 ;
3410
+ }
3411
+ if (!$ cond ->class instanceof Name) {
3412
+ continue 2 ;
3413
+ }
3414
+ if (!$ cond ->name instanceof Node \Identifier) {
3415
+ continue 2 ;
3416
+ }
3417
+ $ fetchedClassName = $ scope ->resolveName ($ cond ->class );
3418
+ $ loweredFetchedClassName = strtolower ($ fetchedClassName );
3419
+ if (!array_key_exists ($ loweredFetchedClassName , $ indexedEnumCases )) {
3420
+ continue 2 ;
3421
+ }
3422
+
3423
+ if (!array_key_exists ($ loweredFetchedClassName , $ unusedIndexedEnumCases )) {
3424
+ throw new ShouldNotHappenException ();
3425
+ }
3426
+
3427
+ $ caseName = $ cond ->name ->toString ();
3428
+ if (!array_key_exists ($ caseName , $ indexedEnumCases [$ loweredFetchedClassName ])) {
3429
+ continue 2 ;
3430
+ }
3431
+
3432
+ $ enumCase = $ indexedEnumCases [$ loweredFetchedClassName ][$ caseName ];
3433
+ $ conditionCases [] = $ enumCase ;
3434
+ $ armConditionScope = $ matchScope ;
3435
+ if (!array_key_exists ($ caseName , $ unusedIndexedEnumCases [$ loweredFetchedClassName ])) {
3436
+ // force "always false"
3437
+ $ armConditionScope = $ armConditionScope ->removeTypeFromExpression (
3438
+ $ expr ->cond ,
3439
+ $ enumCase ,
3440
+ );
3441
+ } elseif (count ($ unusedIndexedEnumCases [$ loweredFetchedClassName ]) === 1 ) {
3442
+ $ hasAlwaysTrueCond = true ;
3443
+
3444
+ // force "always true"
3445
+ $ armConditionScope = $ armConditionScope ->addTypeToExpression (
3446
+ $ expr ->cond ,
3447
+ $ enumCase ,
3448
+ );
3449
+ }
3450
+
3451
+ $ this ->processExprNode ($ stmt , $ cond , $ armConditionScope , $ nodeCallback , $ deepContext );
3452
+
3453
+ $ condNodes [] = new MatchExpressionArmCondition (
3454
+ $ cond ,
3455
+ $ armConditionScope ,
3456
+ $ cond ->getStartLine (),
3457
+ );
3458
+
3459
+ unset($ unusedIndexedEnumCases [$ loweredFetchedClassName ][$ caseName ]);
3460
+ }
3461
+
3462
+ $ conditionCasesCount = count ($ conditionCases );
3463
+ if ($ conditionCasesCount === 0 ) {
3464
+ throw new ShouldNotHappenException ();
3465
+ } elseif ($ conditionCasesCount === 1 ) {
3466
+ $ conditionCaseType = $ conditionCases [0 ];
3467
+ } else {
3468
+ $ conditionCaseType = new UnionType ($ conditionCases );
3469
+ }
3470
+
3471
+ $ matchArmBodyScope = $ matchScope ->addTypeToExpression (
3472
+ $ expr ->cond ,
3473
+ $ conditionCaseType ,
3474
+ );
3475
+ $ matchArmBody = new MatchExpressionArmBody ($ matchArmBodyScope , $ arm ->body );
3476
+ $ armNodes [$ i ] = new MatchExpressionArm ($ matchArmBody , $ condNodes , $ arm ->getStartLine ());
3477
+
3478
+ $ armResult = $ this ->processExprNode (
3479
+ $ stmt ,
3480
+ $ arm ->body ,
3481
+ $ matchArmBodyScope ,
3482
+ $ nodeCallback ,
3483
+ ExpressionContext::createTopLevel (),
3484
+ );
3485
+ $ armScope = $ armResult ->getScope ();
3486
+ $ scope = $ scope ->mergeWith ($ armScope );
3487
+ $ hasYield = $ hasYield || $ armResult ->hasYield ();
3488
+ $ throwPoints = array_merge ($ throwPoints , $ armResult ->getThrowPoints ());
3489
+ $ impurePoints = array_merge ($ impurePoints , $ armResult ->getImpurePoints ());
3490
+
3491
+ unset($ arms [$ i ]);
3492
+ }
3493
+
3494
+ $ remainingCases = [];
3495
+ foreach ($ unusedIndexedEnumCases as $ cases ) {
3496
+ foreach ($ cases as $ case ) {
3497
+ $ remainingCases [] = $ case ;
3498
+ }
3499
+ }
3500
+
3501
+ $ remainingCasesCount = count ($ remainingCases );
3502
+ if ($ remainingCasesCount === 0 ) {
3503
+ $ remainingType = new NeverType ();
3504
+ } elseif ($ remainingCasesCount === 1 ) {
3505
+ $ remainingType = $ remainingCases [0 ];
3506
+ } else {
3507
+ $ remainingType = new UnionType ($ remainingCases );
3508
+ }
3509
+
3510
+ $ matchScope = $ matchScope ->addTypeToExpression ($ expr ->cond , $ remainingType );
3511
+ }
3512
+ }
3513
+ foreach ($ arms as $ i => $ arm ) {
3385
3514
if ($ arm ->conds === null ) {
3386
3515
$ hasDefaultCond = true ;
3387
3516
$ matchArmBody = new MatchExpressionArmBody ($ matchScope , $ arm ->body );
3388
- $ armNodes [] = new MatchExpressionArm ($ matchArmBody , [], $ arm ->getStartLine ());
3517
+ $ armNodes [$ i ] = new MatchExpressionArm ($ matchArmBody , [], $ arm ->getStartLine ());
3389
3518
$ armResult = $ this ->processExprNode ($ stmt , $ arm ->body , $ matchScope , $ nodeCallback , ExpressionContext::createTopLevel ());
3390
3519
$ matchScope = $ armResult ->getScope ();
3391
3520
$ hasYield = $ hasYield || $ armResult ->hasYield ();
@@ -3438,7 +3567,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
3438
3567
$ bodyScope = $ this ->processExprNode ($ stmt , $ filteringExpr , $ matchScope , static function (): void {
3439
3568
}, $ deepContext )->getTruthyScope ();
3440
3569
$ matchArmBody = new MatchExpressionArmBody ($ bodyScope , $ arm ->body );
3441
- $ armNodes [] = new MatchExpressionArm ($ matchArmBody , $ condNodes , $ arm ->getStartLine ());
3570
+ $ armNodes [$ i ] = new MatchExpressionArm ($ matchArmBody , $ condNodes , $ arm ->getStartLine ());
3442
3571
3443
3572
$ armResult = $ this ->processExprNode (
3444
3573
$ stmt ,
@@ -3460,7 +3589,9 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
3460
3589
$ throwPoints [] = ThrowPoint::createExplicit ($ scope , new ObjectType (UnhandledMatchError::class), $ expr , false );
3461
3590
}
3462
3591
3463
- $ nodeCallback (new MatchExpressionNode ($ expr ->cond , $ armNodes , $ expr , $ matchScope ), $ scope );
3592
+ ksort ($ armNodes , SORT_NUMERIC );
3593
+
3594
+ $ nodeCallback (new MatchExpressionNode ($ expr ->cond , array_values ($ armNodes ), $ expr , $ matchScope ), $ scope );
3464
3595
} elseif ($ expr instanceof AlwaysRememberedExpr) {
3465
3596
$ result = $ this ->processExprNode ($ stmt , $ expr ->getExpr (), $ scope , $ nodeCallback , $ context );
3466
3597
$ hasYield = $ result ->hasYield ();
0 commit comments