@@ -506,6 +506,83 @@ function getUnknownValue()
506506assert ($ asserted instanceof User);
507507$ asserted ->addRoles ();
508508
509+ // ─── Type Narrowing ────────────────────────────────────────────────────────
510+ //
511+ // PHPantomLSP narrows union types based on runtime checks so that
512+ // completion only shows the relevant members.
513+
514+ // while-loop instanceof narrowing
515+ // Inside the loop body, $found is narrowed to User because the condition
516+ // guarantees it on every iteration.
517+ $ found2 = findOrFail (1 ); // User|AdminUser
518+ while ($ found2 instanceof User) {
519+ $ found2 ->getEmail (); // ✅ User members only
520+ break ;
521+ }
522+
523+ // is_a() — treated the same as instanceof
524+ $ pet = findOrFail (1 ); // User|AdminUser
525+ if (is_a ($ pet , AdminUser::class)) {
526+ $ pet ->grantPermission ('edit ' ); // ✅ narrowed to AdminUser
527+ }
528+
529+ // Negated is_a() — excludes the checked class
530+ $ pet2 = findOrFail (1 ); // User|AdminUser
531+ if (!is_a ($ pet2 , AdminUser::class)) {
532+ $ pet2 ->getEmail (); // ✅ narrowed to User (AdminUser excluded)
533+ }
534+
535+ // assert() with is_a() — unconditional narrowing
536+ $ pet3 = findOrFail (1 ); // User|AdminUser
537+ assert (is_a ($ pet3 , AdminUser::class));
538+ $ pet3 ->grantPermission ('delete ' ); // ✅ narrowed to AdminUser
539+
540+ // get_class() === ClassName::class — exact class identity
541+ $ entity = findOrFail (1 ); // User|AdminUser
542+ if (get_class ($ entity ) === User::class) {
543+ $ entity ->getEmail (); // ✅ narrowed to exactly User
544+ }
545+
546+ // $var::class === ClassName::class — modern exact class identity (PHP 8.0+)
547+ $ entity2 = findOrFail (1 ); // User|AdminUser
548+ if ($ entity2 ::class === AdminUser::class) {
549+ $ entity2 ->grantPermission ('manage ' ); // ✅ narrowed to AdminUser
550+ }
551+
552+ // Negated class identity — excludes the matched class
553+ $ entity3 = findOrFail (1 ); // User|AdminUser
554+ if (get_class ($ entity3 ) !== User::class) {
555+ $ entity3 ->grantPermission ('review ' ); // ✅ narrowed to AdminUser (User excluded)
556+ }
557+
558+ // Reversed operand order also works
559+ $ entity4 = findOrFail (1 ); // User|AdminUser
560+ if (User::class === $ entity4 ::class) {
561+ $ entity4 ->getEmail (); // ✅ narrowed to User
562+ }
563+
564+ // match(true) with instanceof — narrowing inside match arm bodies
565+ $ value = findOrFail (1 ); // User|AdminUser
566+ $ result = match (true ) {
567+ $ value instanceof AdminUser => $ value ->grantPermission ('approve ' ), // ✅ narrowed to AdminUser
568+ default => null ,
569+ };
570+
571+ // match(true) with is_a() — also works
572+ $ value2 = findOrFail (1 ); // User|AdminUser
573+ $ result2 = match (true ) {
574+ is_a ($ value2 , AdminUser::class) => $ value2 ->grantPermission ('deploy ' ), // ✅ narrowed to AdminUser
575+ default => null ,
576+ };
577+
578+ // Else-branch narrowing — the inverse type is used
579+ $ check = findOrFail (1 ); // User|AdminUser
580+ if ($ check instanceof AdminUser) {
581+ $ check ->grantPermission ('sudo ' ); // ✅ narrowed to AdminUser
582+ } else {
583+ $ check ->getEmail (); // ✅ narrowed to User (AdminUser excluded)
584+ }
585+
509586// Go-to-definition targets:
510587// - Hover over `User` to jump to its class definition
511588// - Hover over `getEmail` to jump to its method definition
0 commit comments