Skip to content

Commit 0e0d5ed

Browse files
committed
Additional narrowing support
1 parent 466376f commit 0e0d5ed

5 files changed

Lines changed: 1888 additions & 370 deletions

File tree

example.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,83 @@ function getUnknownValue()
506506
assert($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

src/completion/builder.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ impl Backend {
9797
current_class_name: Option<&str>,
9898
) -> Vec<CompletionItem> {
9999
// Determine whether we are inside the same class as the target.
100-
let same_class = current_class_name
101-
.is_some_and(|name| name == target_class.name);
100+
let same_class = current_class_name.is_some_and(|name| name == target_class.name);
102101
// Inside *some* class (possibly a subclass) — show protected.
103102
let in_class = current_class_name.is_some();
104103
let mut items: Vec<CompletionItem> = Vec::new();

0 commit comments

Comments
 (0)