|
12 | 12 | /// - `assert($var instanceof ClassName)` — unconditional narrowing |
13 | 13 | /// - `@phpstan-assert` / `@psalm-assert` — custom type guard functions |
14 | 14 | /// - `match(true) { $var instanceof Foo => … }` — match-arm narrowing |
| 15 | +/// - `$var instanceof Foo ? $var->method() : …` — ternary narrowing |
15 | 16 | use mago_span::HasSpan; |
16 | 17 | use mago_syntax::ast::*; |
17 | 18 |
|
@@ -615,4 +616,104 @@ impl Backend { |
615 | 616 | } |
616 | 617 | } |
617 | 618 | } |
| 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 | + } |
618 | 719 | } |
0 commit comments