Skip to content

Commit 65d16eb

Browse files
committed
GROOVY-11983: STC: unsound smart-cast in else branch of if (cond && !(x instanceof Y)) (port to 5.0.x)
1 parent 4f9d327 commit 65d16eb

2 files changed

Lines changed: 148 additions & 3 deletions

File tree

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

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.codehaus.groovy.ast.expr.AttributeExpression;
5656
import org.codehaus.groovy.ast.expr.BinaryExpression;
5757
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
58+
import org.codehaus.groovy.ast.expr.BooleanExpression;
5859
import org.codehaus.groovy.ast.expr.CastExpression;
5960
import org.codehaus.groovy.ast.expr.ClassExpression;
6061
import org.codehaus.groovy.ast.expr.ClosureExpression;
@@ -253,6 +254,9 @@
253254
import static org.codehaus.groovy.runtime.ArrayGroovyMethods.init;
254255
import static org.codehaus.groovy.runtime.ArrayGroovyMethods.last;
255256
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;
256260
import static org.codehaus.groovy.syntax.Types.COMPARE_EQUAL;
257261
import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_EQUAL;
258262
import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_IN;
@@ -267,6 +271,7 @@
267271
import static org.codehaus.groovy.syntax.Types.INTDIV_EQUAL;
268272
import static org.codehaus.groovy.syntax.Types.KEYWORD_IN;
269273
import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF;
274+
import static org.codehaus.groovy.syntax.Types.LOGICAL_AND;
270275
import static org.codehaus.groovy.syntax.Types.LOGICAL_OR;
271276
import static org.codehaus.groovy.syntax.Types.MINUS_MINUS;
272277
import static org.codehaus.groovy.syntax.Types.MOD;
@@ -4271,13 +4276,15 @@ public void visitIfElse(final IfStatement ifElse) {
42714276

42724277
// GROOVY-6429: reverse instanceof tracking
42734278
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);
42754282

42764283
elsePath.visit(this);
42774284

42784285
typeCheckingContext.popTemporaryTypeInfo();
42794286
// 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)) {
42814288
tti.forEach(this::putNotInstanceOfTypeInfo);
42824289
}
42834290
} finally {
@@ -4468,7 +4475,11 @@ public void visitTernaryExpression(final TernaryExpression expression) {
44684475
Map<Object, List<ClassNode>> tti = typeCheckingContext.temporaryIfBranchTypeInformation.pop();
44694476

44704477
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+
}
44724483
Expression falseExpression = expression.getFalseExpression();
44734484
ClassNode typeOfFalse = visitValueExpression(falseExpression);
44744485
typeCheckingContext.popTemporaryTypeInfo();
@@ -6239,6 +6250,49 @@ private void putNotInstanceOfTypeInfo(final Object key, final Collection<ClassNo
62396250
typeCheckingContext.temporaryIfBranchTypeInformation.peek().computeIfAbsent(notKey, x -> new LinkedList<>()).addAll(types);
62406251
}
62416252

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+
62426296
/**
62436297
* Computes the key to use for {@link TypeCheckingContext#temporaryIfBranchTypeInformation}.
62446298
*/

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,97 @@ class TypeInferenceSTCTest extends StaticTypeCheckingTestCase {
688688
}
689689
}
690690

691+
// GROOVY-11983: !instanceof inside && must not produce a positive smart-cast in the else branch
692+
void testNotInstanceof7() {
693+
for (test in ['!(x instanceof String)', 'x !instanceof String']) {
694+
// if/else form: a non-String x with cond=false lands in the else; if x were
695+
// smart-cast to String, the body would emit a checkcast and throw CCE
696+
assertScript """
697+
@groovy.transform.CompileStatic
698+
class T {
699+
static int f(Object x, boolean cond) {
700+
if (cond && $test) {
701+
return 1
702+
} else {
703+
return x.hashCode()
704+
}
705+
}
706+
}
707+
assert T.f(42, false) == Integer.valueOf(42).hashCode()
708+
assert T.f('s', false) == 's'.hashCode()
709+
assert T.f('s', true) == 's'.hashCode()
710+
assert T.f(42, true) == 1
711+
"""
712+
// ternary form
713+
assertScript """
714+
@groovy.transform.CompileStatic
715+
class T {
716+
static int g(Object x, boolean cond) {
717+
return (cond && $test) ? 1 : x.hashCode()
718+
}
719+
}
720+
assert T.g(42, false) == Integer.valueOf(42).hashCode()
721+
assert T.g('s', false) == 's'.hashCode()
722+
"""
723+
// early-return surrounding-scope form
724+
assertScript """
725+
@groovy.transform.CompileStatic
726+
class T {
727+
static int h(Object x, boolean cond) {
728+
if (cond && $test) return 1
729+
return x.hashCode()
730+
}
731+
}
732+
assert T.h(42, false) == Integer.valueOf(42).hashCode()
733+
assert T.h('s', false) == 's'.hashCode()
734+
"""
735+
}
736+
}
737+
738+
// GROOVY-11983: bitwise & / ^ between booleans, and bitwise | wrapping a logical-and,
739+
// must also disable the else-branch instanceof inversion
740+
void testNotInstanceof8() {
741+
// bitwise-AND: same unsoundness as logical-AND
742+
assertScript '''
743+
@groovy.transform.CompileStatic
744+
class T {
745+
static int f(Object x, boolean cond) {
746+
if (cond & !(x instanceof String)) return 1
747+
return x.hashCode()
748+
}
749+
}
750+
assert T.f(42, false) == Integer.valueOf(42).hashCode()
751+
assert T.f('s', false) == 's'.hashCode()
752+
assert T.f('s', true) == 's'.hashCode()
753+
assert T.f(42, true) == 1
754+
'''
755+
// bitwise-XOR: same unsoundness
756+
assertScript '''
757+
@groovy.transform.CompileStatic
758+
class T {
759+
static int g(Object x, boolean cond) {
760+
if (cond ^ (x instanceof String)) return 1
761+
return x.hashCode()
762+
}
763+
}
764+
assert T.g(42, false) == Integer.valueOf(42).hashCode()
765+
assert T.g('s', true) == 's'.hashCode()
766+
'''
767+
// bitwise-OR wrapping a logical-AND: still must disqualify
768+
assertScript '''
769+
@groovy.transform.CompileStatic
770+
class T {
771+
static int h(Object x, boolean a, boolean b) {
772+
if (a | (b && !(x instanceof String))) return 1
773+
return x.hashCode()
774+
}
775+
}
776+
assert T.h(42, false, false) == Integer.valueOf(42).hashCode()
777+
assert T.h('s', false, false) == 's'.hashCode()
778+
assert T.h(42, true, false) == 1
779+
'''
780+
}
781+
691782
// GROOVY-10217
692783
void testInstanceOfThenSubscriptOperator() {
693784
assertScript '''

0 commit comments

Comments
 (0)