Skip to content

Commit d2378e6

Browse files
committed
GROOVY-11983: STC: unsound smart-cast in else branch of if (cond && !(x instanceof Y))
1 parent 88ca738 commit d2378e6

2 files changed

Lines changed: 91 additions & 3 deletions

File tree

src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.codehaus.groovy.ast.expr.AttributeExpression;
5757
import org.codehaus.groovy.ast.expr.BinaryExpression;
5858
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
59+
import org.codehaus.groovy.ast.expr.BooleanExpression;
5960
import org.codehaus.groovy.ast.expr.CastExpression;
6061
import org.codehaus.groovy.ast.expr.ClassExpression;
6162
import org.codehaus.groovy.ast.expr.ClosureExpression;
@@ -268,6 +269,7 @@
268269
import static org.codehaus.groovy.syntax.Types.INTDIV_EQUAL;
269270
import static org.codehaus.groovy.syntax.Types.KEYWORD_IN;
270271
import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF;
272+
import static org.codehaus.groovy.syntax.Types.LOGICAL_AND;
271273
import static org.codehaus.groovy.syntax.Types.LOGICAL_OR;
272274
import static org.codehaus.groovy.syntax.Types.MINUS_MINUS;
273275
import static org.codehaus.groovy.syntax.Types.MOD;
@@ -4636,13 +4638,15 @@ public void visitIfElse(final IfStatement ifElse) {
46364638

46374639
// GROOVY-6429: reverse instanceof tracking
46384640
typeCheckingContext.pushTemporaryTypeInfo();
4639-
tti.forEach(this::putNotInstanceOfTypeInfo);
4641+
// GROOVY-11983: only invert when the boolean's falsity pins down each instanceof
4642+
boolean canInvert = canInvertNarrowingForElseBranch(ifElse.getBooleanExpression());
4643+
if (canInvert) tti.forEach(this::putNotInstanceOfTypeInfo);
46404644

46414645
elsePath.visit(this);
46424646

46434647
typeCheckingContext.popTemporaryTypeInfo();
46444648
// GROOVY-8523: propagate tracking to outer scope; keep simple for now
4645-
if (elsePath.isEmpty() && !GeneralUtils.maybeFallsThrough(thenPath)) {
4649+
if (canInvert && elsePath.isEmpty() && !GeneralUtils.maybeFallsThrough(thenPath)) {
46464650
tti.forEach(this::putNotInstanceOfTypeInfo);
46474651
}
46484652
} finally {
@@ -4837,7 +4841,11 @@ public void visitTernaryExpression(final TernaryExpression expression) {
48374841
Map<Object, List<ClassNode>> tti = typeCheckingContext.temporaryIfBranchTypeInformation.pop();
48384842

48394843
typeCheckingContext.pushTemporaryTypeInfo();
4840-
tti.forEach(this::putNotInstanceOfTypeInfo); // GROOVY-8412
4844+
// GROOVY-8412 / GROOVY-11983: only invert when sound (no LOGICAL_AND in condition)
4845+
if (!(expression instanceof ElvisOperatorExpression)
4846+
&& canInvertNarrowingForElseBranch(expression.getBooleanExpression())) {
4847+
tti.forEach(this::putNotInstanceOfTypeInfo);
4848+
}
48414849
Expression falseExpression = expression.getFalseExpression();
48424850
ClassNode typeOfFalse = visitValueExpression(falseExpression);
48434851
typeCheckingContext.popTemporaryTypeInfo();
@@ -6645,6 +6653,38 @@ private void putNotInstanceOfTypeInfo(final Object key, final Collection<ClassNo
66456653
tti.addAll(types); // stash negative type(s)
66466654
}
66476655

6656+
/**
6657+
* GROOVY-11983: Whether the temporary type info captured while visiting {@code expr}
6658+
* may be soundly inverted for the else branch of {@code if (expr) ... else ...} (or
6659+
* the false branch of a ternary, or the surrounding scope after an early-returning
6660+
* {@code if (expr) return}). Inversion is only sound when {@code !expr} pins down
6661+
* the negation of every individual instanceof / !instanceof check inside it. A
6662+
* logical-AND breaks that: the else of {@code A && B} is reached when at least one
6663+
* of {@code A}, {@code B} is false, not necessarily both — so an instanceof inside
6664+
* either operand cannot be safely inverted. In particular, a {@code !instanceof}
6665+
* inside an AND would otherwise be unwrapped into a positive smart-cast, producing
6666+
* an unsound {@code checkcast} and a runtime ClassCastException.
6667+
*/
6668+
private static boolean canInvertNarrowingForElseBranch(final Expression expr) {
6669+
if (expr instanceof BinaryExpression be) {
6670+
int op = be.getOperation().getType();
6671+
if (op == LOGICAL_AND) return false;
6672+
if (op == LOGICAL_OR) {
6673+
return canInvertNarrowingForElseBranch(be.getLeftExpression())
6674+
&& canInvertNarrowingForElseBranch(be.getRightExpression());
6675+
}
6676+
return true; // instanceof, ==, comparisons, etc.
6677+
}
6678+
if (expr instanceof BooleanExpression be) return canInvertNarrowingForElseBranch(be.getExpression());
6679+
if (expr instanceof NotExpression ne) return canInvertNarrowingForElseBranch(ne.getExpression());
6680+
if (expr instanceof TernaryExpression te) {
6681+
return canInvertNarrowingForElseBranch(te.getBooleanExpression())
6682+
&& canInvertNarrowingForElseBranch(te.getTrueExpression())
6683+
&& canInvertNarrowingForElseBranch(te.getFalseExpression());
6684+
}
6685+
return true;
6686+
}
6687+
66486688
private boolean optInstanceOfTypeInfo(final Expression expression, final ClassNode type) {
66496689
var tti = typeCheckingContext.temporaryIfBranchTypeInformation.peek().get(extractTemporaryTypeInfoKey(expression));
66506690
if (tti != null) { assert !tti.isEmpty();

src/test/groovy/groovy/transform/stc/TypeInferenceSTCTest.groovy

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,54 @@ class TypeInferenceSTCTest extends StaticTypeCheckingTestCase {
10311031
}
10321032
}
10331033

1034+
// GROOVY-11983: !instanceof inside && must not produce a positive smart-cast in the else branch
1035+
@Test
1036+
void testNotInstanceof7() {
1037+
for (test in ['!(x instanceof String)', 'x !instanceof String']) {
1038+
// if/else form: a non-String x with cond=false lands in the else; if x were
1039+
// smart-cast to String, the body would emit a checkcast and throw CCE
1040+
assertScript """
1041+
@groovy.transform.CompileStatic
1042+
class T {
1043+
static int f(Object x, boolean cond) {
1044+
if (cond && $test) {
1045+
return 1
1046+
} else {
1047+
return x.hashCode()
1048+
}
1049+
}
1050+
}
1051+
assert T.f(42, false) == Integer.valueOf(42).hashCode()
1052+
assert T.f('s', false) == 's'.hashCode()
1053+
assert T.f('s', true) == 's'.hashCode()
1054+
assert T.f(42, true) == 1
1055+
"""
1056+
// ternary form
1057+
assertScript """
1058+
@groovy.transform.CompileStatic
1059+
class T {
1060+
static int g(Object x, boolean cond) {
1061+
return (cond && $test) ? 1 : x.hashCode()
1062+
}
1063+
}
1064+
assert T.g(42, false) == Integer.valueOf(42).hashCode()
1065+
assert T.g('s', false) == 's'.hashCode()
1066+
"""
1067+
// early-return surrounding-scope form
1068+
assertScript """
1069+
@groovy.transform.CompileStatic
1070+
class T {
1071+
static int h(Object x, boolean cond) {
1072+
if (cond && $test) return 1
1073+
return x.hashCode()
1074+
}
1075+
}
1076+
assert T.h(42, false) == Integer.valueOf(42).hashCode()
1077+
assert T.h('s', false) == 's'.hashCode()
1078+
"""
1079+
}
1080+
}
1081+
10341082
// GROOVY-10217
10351083
@Test
10361084
void testInstanceOfThenSubscriptOperator() {

0 commit comments

Comments
 (0)