|
56 | 56 | import org.codehaus.groovy.ast.expr.AttributeExpression; |
57 | 57 | import org.codehaus.groovy.ast.expr.BinaryExpression; |
58 | 58 | import org.codehaus.groovy.ast.expr.BitwiseNegationExpression; |
| 59 | +import org.codehaus.groovy.ast.expr.BooleanExpression; |
59 | 60 | import org.codehaus.groovy.ast.expr.CastExpression; |
60 | 61 | import org.codehaus.groovy.ast.expr.ClassExpression; |
61 | 62 | import org.codehaus.groovy.ast.expr.ClosureExpression; |
|
254 | 255 | import static org.codehaus.groovy.runtime.ArrayGroovyMethods.init; |
255 | 256 | import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last; |
256 | 257 | import static org.codehaus.groovy.syntax.Types.ASSIGN; |
| 258 | +import static org.codehaus.groovy.syntax.Types.BITWISE_AND; |
| 259 | +import static org.codehaus.groovy.syntax.Types.BITWISE_OR; |
| 260 | +import static org.codehaus.groovy.syntax.Types.BITWISE_XOR; |
257 | 261 | import static org.codehaus.groovy.syntax.Types.COMPARE_EQUAL; |
258 | 262 | import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_EQUAL; |
259 | 263 | import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_IN; |
|
268 | 272 | import static org.codehaus.groovy.syntax.Types.INTDIV_EQUAL; |
269 | 273 | import static org.codehaus.groovy.syntax.Types.KEYWORD_IN; |
270 | 274 | import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF; |
| 275 | +import static org.codehaus.groovy.syntax.Types.LOGICAL_AND; |
271 | 276 | import static org.codehaus.groovy.syntax.Types.LOGICAL_OR; |
272 | 277 | import static org.codehaus.groovy.syntax.Types.MINUS_MINUS; |
273 | 278 | import static org.codehaus.groovy.syntax.Types.MOD; |
@@ -4636,13 +4641,15 @@ public void visitIfElse(final IfStatement ifElse) { |
4636 | 4641 |
|
4637 | 4642 | // GROOVY-6429: reverse instanceof tracking |
4638 | 4643 | typeCheckingContext.pushTemporaryTypeInfo(); |
4639 | | - tti.forEach(this::putNotInstanceOfTypeInfo); |
| 4644 | + // GROOVY-11983: only invert when the boolean's falsity pins down each instanceof |
| 4645 | + boolean canInvert = canInvertNarrowingForElseBranch(ifElse.getBooleanExpression()); |
| 4646 | + if (canInvert) tti.forEach(this::putNotInstanceOfTypeInfo); |
4640 | 4647 |
|
4641 | 4648 | elsePath.visit(this); |
4642 | 4649 |
|
4643 | 4650 | typeCheckingContext.popTemporaryTypeInfo(); |
4644 | 4651 | // GROOVY-8523: propagate tracking to outer scope; keep simple for now |
4645 | | - if (elsePath.isEmpty() && !GeneralUtils.maybeFallsThrough(thenPath)) { |
| 4652 | + if (canInvert && elsePath.isEmpty() && !GeneralUtils.maybeFallsThrough(thenPath)) { |
4646 | 4653 | tti.forEach(this::putNotInstanceOfTypeInfo); |
4647 | 4654 | } |
4648 | 4655 | } finally { |
@@ -4837,7 +4844,11 @@ public void visitTernaryExpression(final TernaryExpression expression) { |
4837 | 4844 | Map<Object, List<ClassNode>> tti = typeCheckingContext.temporaryIfBranchTypeInformation.pop(); |
4838 | 4845 |
|
4839 | 4846 | typeCheckingContext.pushTemporaryTypeInfo(); |
4840 | | - tti.forEach(this::putNotInstanceOfTypeInfo); // GROOVY-8412 |
| 4847 | + // GROOVY-8412 / GROOVY-11983: only invert when sound (no LOGICAL_AND in condition) |
| 4848 | + if (!(expression instanceof ElvisOperatorExpression) |
| 4849 | + && canInvertNarrowingForElseBranch(expression.getBooleanExpression())) { |
| 4850 | + tti.forEach(this::putNotInstanceOfTypeInfo); |
| 4851 | + } |
4841 | 4852 | Expression falseExpression = expression.getFalseExpression(); |
4842 | 4853 | ClassNode typeOfFalse = visitValueExpression(falseExpression); |
4843 | 4854 | typeCheckingContext.popTemporaryTypeInfo(); |
@@ -6645,6 +6656,43 @@ private void putNotInstanceOfTypeInfo(final Object key, final Collection<ClassNo |
6645 | 6656 | tti.addAll(types); // stash negative type(s) |
6646 | 6657 | } |
6647 | 6658 |
|
| 6659 | + /** |
| 6660 | + * GROOVY-11983: Whether the temporary type info captured while visiting {@code expr} |
| 6661 | + * may be soundly inverted for the else branch of {@code if (expr) ... else ...} (or |
| 6662 | + * the false branch of a ternary, or the surrounding scope after an early-returning |
| 6663 | + * {@code if (expr) return}). Inversion is only sound when {@code !expr} pins down |
| 6664 | + * the negation of every individual instanceof / !instanceof check inside it. |
| 6665 | + * <p> |
| 6666 | + * Only OR-like operators ({@code ||}, and {@code |} on booleans) preserve that |
| 6667 | + * property — {@code !(L op R)} is equivalent to {@code !L && !R}, so each operand's |
| 6668 | + * negation is pinned down. AND-like and XOR-like operators ({@code &&}, {@code &}, |
| 6669 | + * {@code ^}) do not: {@code !(L && R)} only requires <em>at least one</em> operand |
| 6670 | + * to be false, so a {@code !instanceof} hidden inside one of them would otherwise |
| 6671 | + * be unwrapped into a positive smart-cast in the else branch, producing an unsound |
| 6672 | + * {@code checkcast} and a runtime ClassCastException. |
| 6673 | + */ |
| 6674 | + private static boolean canInvertNarrowingForElseBranch(final Expression expr) { |
| 6675 | + if (expr instanceof BinaryExpression be) { |
| 6676 | + int op = be.getOperation().getType(); |
| 6677 | + // AND-like / XOR: !(L op R) doesn't pin down each operand's truth value |
| 6678 | + if (op == LOGICAL_AND || op == BITWISE_AND || op == BITWISE_XOR) return false; |
| 6679 | + // OR-like: !(L op R) <=> !L && !R, so recurse into both operands |
| 6680 | + if (op == LOGICAL_OR || op == BITWISE_OR) { |
| 6681 | + return canInvertNarrowingForElseBranch(be.getLeftExpression()) |
| 6682 | + && canInvertNarrowingForElseBranch(be.getRightExpression()); |
| 6683 | + } |
| 6684 | + return true; // instanceof, ==, comparisons, etc. |
| 6685 | + } |
| 6686 | + if (expr instanceof BooleanExpression be) return canInvertNarrowingForElseBranch(be.getExpression()); |
| 6687 | + if (expr instanceof NotExpression ne) return canInvertNarrowingForElseBranch(ne.getExpression()); |
| 6688 | + if (expr instanceof TernaryExpression te) { |
| 6689 | + return canInvertNarrowingForElseBranch(te.getBooleanExpression()) |
| 6690 | + && canInvertNarrowingForElseBranch(te.getTrueExpression()) |
| 6691 | + && canInvertNarrowingForElseBranch(te.getFalseExpression()); |
| 6692 | + } |
| 6693 | + return true; |
| 6694 | + } |
| 6695 | + |
6648 | 6696 | private boolean optInstanceOfTypeInfo(final Expression expression, final ClassNode type) { |
6649 | 6697 | var tti = typeCheckingContext.temporaryIfBranchTypeInformation.peek().get(extractTemporaryTypeInfoKey(expression)); |
6650 | 6698 | if (tti != null) { assert !tti.isEmpty(); |
|
0 commit comments