|
55 | 55 | import org.codehaus.groovy.ast.expr.AttributeExpression; |
56 | 56 | import org.codehaus.groovy.ast.expr.BinaryExpression; |
57 | 57 | import org.codehaus.groovy.ast.expr.BitwiseNegationExpression; |
| 58 | +import org.codehaus.groovy.ast.expr.BooleanExpression; |
58 | 59 | import org.codehaus.groovy.ast.expr.CastExpression; |
59 | 60 | import org.codehaus.groovy.ast.expr.ClassExpression; |
60 | 61 | import org.codehaus.groovy.ast.expr.ClosureExpression; |
|
253 | 254 | import static org.codehaus.groovy.runtime.ArrayGroovyMethods.init; |
254 | 255 | import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last; |
255 | 256 | import static org.codehaus.groovy.syntax.Types.ASSIGN; |
| 257 | +import static org.codehaus.groovy.syntax.Types.BITWISE_AND; |
| 258 | +import static org.codehaus.groovy.syntax.Types.BITWISE_OR; |
| 259 | +import static org.codehaus.groovy.syntax.Types.BITWISE_XOR; |
256 | 260 | import static org.codehaus.groovy.syntax.Types.COMPARE_EQUAL; |
257 | 261 | import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_EQUAL; |
258 | 262 | import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_IN; |
|
267 | 271 | import static org.codehaus.groovy.syntax.Types.INTDIV_EQUAL; |
268 | 272 | import static org.codehaus.groovy.syntax.Types.KEYWORD_IN; |
269 | 273 | import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF; |
| 274 | +import static org.codehaus.groovy.syntax.Types.LOGICAL_AND; |
270 | 275 | import static org.codehaus.groovy.syntax.Types.LOGICAL_OR; |
271 | 276 | import static org.codehaus.groovy.syntax.Types.MINUS_MINUS; |
272 | 277 | import static org.codehaus.groovy.syntax.Types.MOD; |
@@ -4271,13 +4276,15 @@ public void visitIfElse(final IfStatement ifElse) { |
4271 | 4276 |
|
4272 | 4277 | // GROOVY-6429: reverse instanceof tracking |
4273 | 4278 | typeCheckingContext.pushTemporaryTypeInfo(); |
4274 | | - tti.forEach(this::putNotInstanceOfTypeInfo); |
| 4279 | + // GROOVY-11983: only invert when the boolean's falsity pins down each instanceof |
| 4280 | + boolean canInvert = canInvertNarrowingForElseBranch(ifElse.getBooleanExpression()); |
| 4281 | + if (canInvert) tti.forEach(this::putNotInstanceOfTypeInfo); |
4275 | 4282 |
|
4276 | 4283 | elsePath.visit(this); |
4277 | 4284 |
|
4278 | 4285 | typeCheckingContext.popTemporaryTypeInfo(); |
4279 | 4286 | // GROOVY-8523: propagate tracking to outer scope; keep simple for now |
4280 | | - if (elsePath.isEmpty() && !GeneralUtils.maybeFallsThrough(thenPath)) { |
| 4287 | + if (canInvert && elsePath.isEmpty() && !GeneralUtils.maybeFallsThrough(thenPath)) { |
4281 | 4288 | tti.forEach(this::putNotInstanceOfTypeInfo); |
4282 | 4289 | } |
4283 | 4290 | } finally { |
@@ -4468,7 +4475,11 @@ public void visitTernaryExpression(final TernaryExpression expression) { |
4468 | 4475 | Map<Object, List<ClassNode>> tti = typeCheckingContext.temporaryIfBranchTypeInformation.pop(); |
4469 | 4476 |
|
4470 | 4477 | typeCheckingContext.pushTemporaryTypeInfo(); |
4471 | | - tti.forEach(this::putNotInstanceOfTypeInfo); // GROOVY-8412 |
| 4478 | + // GROOVY-8412 / GROOVY-11983: only invert when sound (no AND-like op in condition) |
| 4479 | + if (!(expression instanceof ElvisOperatorExpression) |
| 4480 | + && canInvertNarrowingForElseBranch(expression.getBooleanExpression())) { |
| 4481 | + tti.forEach(this::putNotInstanceOfTypeInfo); |
| 4482 | + } |
4472 | 4483 | Expression falseExpression = expression.getFalseExpression(); |
4473 | 4484 | ClassNode typeOfFalse = visitValueExpression(falseExpression); |
4474 | 4485 | typeCheckingContext.popTemporaryTypeInfo(); |
@@ -6239,6 +6250,49 @@ private void putNotInstanceOfTypeInfo(final Object key, final Collection<ClassNo |
6239 | 6250 | typeCheckingContext.temporaryIfBranchTypeInformation.peek().computeIfAbsent(notKey, x -> new LinkedList<>()).addAll(types); |
6240 | 6251 | } |
6241 | 6252 |
|
| 6253 | + /** |
| 6254 | + * GROOVY-11983: Whether the temporary type info captured while visiting {@code expr} |
| 6255 | + * may be soundly inverted for the else branch of {@code if (expr) ... else ...} (or |
| 6256 | + * the false branch of a ternary, or the surrounding scope after an early-returning |
| 6257 | + * {@code if (expr) return}). Inversion is only sound when {@code !expr} pins down |
| 6258 | + * the negation of every individual instanceof / !instanceof check inside it. |
| 6259 | + * <p> |
| 6260 | + * Only OR-like operators ({@code ||}, and {@code |} on booleans) preserve that |
| 6261 | + * property — {@code !(L op R)} is equivalent to {@code !L && !R}, so each operand's |
| 6262 | + * negation is pinned down. AND-like and XOR-like operators ({@code &&}, {@code &}, |
| 6263 | + * {@code ^}) do not: {@code !(L && R)} only requires <em>at least one</em> operand |
| 6264 | + * to be false, so a {@code !instanceof} hidden inside one of them would otherwise |
| 6265 | + * be unwrapped into a positive smart-cast in the else branch, producing an unsound |
| 6266 | + * {@code checkcast} and a runtime ClassCastException. |
| 6267 | + */ |
| 6268 | + private static boolean canInvertNarrowingForElseBranch(final Expression expr) { |
| 6269 | + if (expr instanceof BinaryExpression) { |
| 6270 | + BinaryExpression be = (BinaryExpression) expr; |
| 6271 | + int op = be.getOperation().getType(); |
| 6272 | + // AND-like / XOR: !(L op R) doesn't pin down each operand's truth value |
| 6273 | + if (op == LOGICAL_AND || op == BITWISE_AND || op == BITWISE_XOR) return false; |
| 6274 | + // OR-like: !(L op R) <=> !L && !R, so recurse into both operands |
| 6275 | + if (op == LOGICAL_OR || op == BITWISE_OR) { |
| 6276 | + return canInvertNarrowingForElseBranch(be.getLeftExpression()) |
| 6277 | + && canInvertNarrowingForElseBranch(be.getRightExpression()); |
| 6278 | + } |
| 6279 | + return true; // instanceof, ==, comparisons, etc. |
| 6280 | + } |
| 6281 | + if (expr instanceof BooleanExpression) { |
| 6282 | + return canInvertNarrowingForElseBranch(((BooleanExpression) expr).getExpression()); |
| 6283 | + } |
| 6284 | + if (expr instanceof NotExpression) { |
| 6285 | + return canInvertNarrowingForElseBranch(((NotExpression) expr).getExpression()); |
| 6286 | + } |
| 6287 | + if (expr instanceof TernaryExpression) { |
| 6288 | + TernaryExpression te = (TernaryExpression) expr; |
| 6289 | + return canInvertNarrowingForElseBranch(te.getBooleanExpression()) |
| 6290 | + && canInvertNarrowingForElseBranch(te.getTrueExpression()) |
| 6291 | + && canInvertNarrowingForElseBranch(te.getFalseExpression()); |
| 6292 | + } |
| 6293 | + return true; |
| 6294 | + } |
| 6295 | + |
6242 | 6296 | /** |
6243 | 6297 | * Computes the key to use for {@link TypeCheckingContext#temporaryIfBranchTypeInformation}. |
6244 | 6298 | */ |
|
0 commit comments