Skip to content

Commit 6435d8c

Browse files
committed
Apply instanceof narrowing inside ternary
1 parent 1eed3f8 commit 6435d8c

4 files changed

Lines changed: 532 additions & 1 deletion

File tree

example.php

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,52 @@ public function mixedTernaryAndMatch(bool $simple, int $mode): void {
11301130
}
11311131
}
11321132

1133+
// ═══════════════════════════════════════════════════════════════════
1134+
// 19b. instanceof narrowing in ternary expressions
1135+
// ═══════════════════════════════════════════════════════════════════
1136+
// When a ternary condition checks `$var instanceof ClassName`, the
1137+
// variable is narrowed in the then-branch and excluded in the
1138+
// else-branch — just like `if`/`else`. Negated conditions flip
1139+
// the polarity. Nested ternaries are also supported.
1140+
1141+
class TernaryInstanceofDemo
1142+
{
1143+
public function thenBranch(Model $item): void
1144+
{
1145+
// ── Then-branch narrowing ──
1146+
// $item is narrowed to User inside the then-branch:
1147+
$name = $item instanceof User ? $item->getEmail() : 'unknown';
1148+
// ↑ shows User methods (getEmail, getStatus, etc.)
1149+
}
1150+
1151+
public function elseBranch(Model $item): void
1152+
{
1153+
// ── Else-branch exclusion ──
1154+
// When $item is NOT User in the else-branch, User is excluded:
1155+
/** @param User|AdminUser $item */
1156+
$x = $item instanceof User ? null : $item->grantPermission();
1157+
// ↑ shows AdminUser methods only
1158+
}
1159+
1160+
public function negated(Model $item): void
1161+
{
1162+
// ── Negated condition ──
1163+
// `!$item instanceof User` excludes User in the then-branch:
1164+
/** @param User|AdminUser $item */
1165+
$x = !$item instanceof User ? $item->grantPermission() : null;
1166+
// ↑ shows AdminUser methods only
1167+
}
1168+
1169+
public function inAssignment(Model $item): void
1170+
{
1171+
// ── Inside assignment RHS ──
1172+
// Works when the ternary is the RHS of an assignment to a
1173+
// *different* variable:
1174+
$result = $item instanceof User ? $item->getEmail() : 'fallback';
1175+
// ↑ shows User methods
1176+
}
1177+
}
1178+
11331179
// ═══════════════════════════════════════════════════════════════════════
11341180
// §14 Property Chains on Non-$this Variables
11351181
// ═══════════════════════════════════════════════════════════════════════
@@ -2264,9 +2310,13 @@ public function staticSnippets(): void
22642310
public function newSnippets(): void
22652311
{
22662312
// `new` inserts class name + constructor params as snippet:
2267-
$u = new User($name, $email) // Inserted: User(${1:$name}, ${2:$email}) — 2 required
2313+
$u = new User($name, $email); // Inserted: User(${1:$name}, ${2:$email}) — 2 required
22682314
$r = new Response($statusCode); // Inserted: Response(${1:$statusCode}) — 1 required
22692315
$a = new AdminUser($name, $email); // Inserted: AdminUser(${1:$name}, ${2:$email}, ${3:$role})
2316+
// After `new`, only class names are suggested — no constants
2317+
// or functions. Loaded interfaces, traits, enums, and abstract
2318+
// classes are excluded. Unloaded names like AbstractHandler or
2319+
// LoggerInterface are demoted in the sort order.
22702320
}
22712321

22722322
public function parentConstructorSnippet(): void

src/completion/type_narrowing.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
/// - `assert($var instanceof ClassName)` — unconditional narrowing
1313
/// - `@phpstan-assert` / `@psalm-assert` — custom type guard functions
1414
/// - `match(true) { $var instanceof Foo => … }` — match-arm narrowing
15+
/// - `$var instanceof Foo ? $var->method() : …` — ternary narrowing
1516
use mago_span::HasSpan;
1617
use mago_syntax::ast::*;
1718

@@ -615,4 +616,104 @@ impl Backend {
615616
}
616617
}
617618
}
619+
620+
/// Apply `instanceof` narrowing inside ternary (`?:`) expressions.
621+
///
622+
/// When the cursor falls inside a ternary whose condition is
623+
/// `$var instanceof ClassName`:
624+
/// - **then-branch** → narrow to `ClassName`
625+
/// - **else-branch** → exclude `ClassName`
626+
///
627+
/// Negated conditions (`!$var instanceof Foo ? … : …`) flip the
628+
/// polarity, just like `if`/`else`.
629+
///
630+
/// The function recursively walks the expression tree so that nested
631+
/// ternaries and ternaries buried inside assignments, function
632+
/// arguments, etc. are all handled.
633+
pub(super) fn try_apply_ternary_instanceof_narrowing(
634+
expr: &Expression<'_>,
635+
ctx: &VarResolutionCtx<'_>,
636+
results: &mut Vec<ClassInfo>,
637+
) {
638+
match expr {
639+
Expression::Conditional(cond_expr) => {
640+
// Determine which branch (if any) the cursor is inside.
641+
let in_then = cond_expr.then.is_some_and(|then_expr| {
642+
let span = then_expr.span();
643+
ctx.cursor_offset >= span.start.offset && ctx.cursor_offset <= span.end.offset
644+
});
645+
let in_else = {
646+
let span = cond_expr.r#else.span();
647+
ctx.cursor_offset >= span.start.offset && ctx.cursor_offset <= span.end.offset
648+
};
649+
650+
if in_then {
651+
if let Some((cls_name, negated)) = Self::try_extract_instanceof_with_negation(
652+
cond_expr.condition,
653+
ctx.var_name,
654+
) {
655+
if negated {
656+
Self::apply_instanceof_exclusion(&cls_name, ctx, results);
657+
} else {
658+
Self::apply_instanceof_inclusion(&cls_name, ctx, results);
659+
}
660+
}
661+
} else if in_else
662+
&& let Some((cls_name, negated)) = Self::try_extract_instanceof_with_negation(
663+
cond_expr.condition,
664+
ctx.var_name,
665+
)
666+
{
667+
// Flip polarity for the else branch.
668+
if negated {
669+
Self::apply_instanceof_inclusion(&cls_name, ctx, results);
670+
} else {
671+
Self::apply_instanceof_exclusion(&cls_name, ctx, results);
672+
}
673+
}
674+
675+
// Recurse into whichever branch contains the cursor so
676+
// that nested ternaries are also narrowed.
677+
if let Some(then_expr) = cond_expr.then {
678+
Self::try_apply_ternary_instanceof_narrowing(then_expr, ctx, results);
679+
}
680+
Self::try_apply_ternary_instanceof_narrowing(cond_expr.r#else, ctx, results);
681+
}
682+
// Recurse through common wrapper expressions so ternaries
683+
// buried inside assignments, parentheses, binary ops, etc.
684+
// are still discovered.
685+
Expression::Parenthesized(inner) => {
686+
Self::try_apply_ternary_instanceof_narrowing(inner.expression, ctx, results);
687+
}
688+
Expression::Assignment(assign) => {
689+
Self::try_apply_ternary_instanceof_narrowing(assign.rhs, ctx, results);
690+
}
691+
Expression::Binary(bin) => {
692+
Self::try_apply_ternary_instanceof_narrowing(bin.lhs, ctx, results);
693+
Self::try_apply_ternary_instanceof_narrowing(bin.rhs, ctx, results);
694+
}
695+
Expression::UnaryPrefix(prefix) => {
696+
Self::try_apply_ternary_instanceof_narrowing(prefix.operand, ctx, results);
697+
}
698+
Expression::UnaryPostfix(postfix) => {
699+
Self::try_apply_ternary_instanceof_narrowing(postfix.operand, ctx, results);
700+
}
701+
Expression::Call(call) => {
702+
let args = match call {
703+
Call::Function(fc) => &fc.argument_list.arguments,
704+
Call::Method(mc) => &mc.argument_list.arguments,
705+
Call::NullSafeMethod(mc) => &mc.argument_list.arguments,
706+
Call::StaticMethod(sc) => &sc.argument_list.arguments,
707+
};
708+
for arg in args.iter() {
709+
let arg_expr = match arg {
710+
Argument::Positional(pos) => pos.value,
711+
Argument::Named(named) => named.value,
712+
};
713+
Self::try_apply_ternary_instanceof_narrowing(arg_expr, ctx, results);
714+
}
715+
}
716+
_ => {}
717+
}
718+
}
618719
}

src/completion/variable_resolution.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,16 @@ impl Backend {
327327

328328
// ── match(true) { $var instanceof Foo => … } narrowing ──
329329
Self::try_apply_match_true_narrowing(expr_stmt.expression, ctx, results);
330+
331+
// ── ternary instanceof narrowing ──
332+
// `$var instanceof Foo ? $var->method() : …`
333+
// When the cursor is inside a ternary whose condition
334+
// checks instanceof, narrow accordingly.
335+
Self::try_apply_ternary_instanceof_narrowing(
336+
expr_stmt.expression,
337+
ctx,
338+
results,
339+
);
330340
}
331341
// Recurse into blocks — these are just `{ … }` groupings,
332342
// not conditional, so preserve the current `conditional` flag.

0 commit comments

Comments
 (0)