Skip to content

Commit d10e044

Browse files
committed
GROOVY-7971: @cs flow typing incorrectly casting to map at runtime
1 parent 0b886e9 commit d10e044

File tree

4 files changed

+113
-19
lines changed

4 files changed

+113
-19
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,11 @@ dependencies {
201201
tools "com.thoughtworks.qdox:qdox:$qdoxVersion"
202202

203203
testImplementation project(':groovy-ant')
204-
testImplementation project(':groovy-xml')
205204
testImplementation project(':groovy-dateutil')
205+
testImplementation project(':groovy-json')
206206
testImplementation project(':groovy-test')
207207
testImplementation project(':groovy-macro')
208+
testImplementation project(':groovy-xml')
208209
}
209210

210211
ext.generatedDirectory = "${buildDir}/generated/sources"

src/main/java/org/codehaus/groovy/transform/sc/StaticCompilationVisitor.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,10 +408,17 @@ private static void memorizeInitialExpressions(final MethodNode node) {
408408
public void visitMethodCallExpression(final MethodCallExpression call) {
409409
super.visitMethodCallExpression(call);
410410

411-
MethodNode target = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
412-
if (target != null) {
413-
call.setMethodTarget(target);
414-
memorizeInitialExpressions(target);
411+
MethodNode newTarget = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
412+
if (newTarget != null) {
413+
MethodNode existingTarget = call.getMethodTarget();
414+
if (existingTarget != null)
415+
if (!(newTarget.getDeclaringClass().equals(existingTarget.getDeclaringClass())) || !(newTarget.getTypeDescriptor().equals(existingTarget.getTypeDescriptor()))) {
416+
// avoid cast class exception
417+
newTarget.putNodeMetaData(DYNAMIC_RESOLUTION, Boolean.TRUE);
418+
call.putNodeMetaData(DYNAMIC_RESOLUTION, OBJECT_TYPE);
419+
}
420+
call.setMethodTarget(newTarget);
421+
memorizeInitialExpressions(newTarget);
415422
}
416423

417424
if (call.getMethodTarget() == null && call.getLineNumber() > 0) {

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

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.codehaus.groovy.ast.expr.AttributeExpression;
5353
import org.codehaus.groovy.ast.expr.BinaryExpression;
5454
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
55+
import org.codehaus.groovy.ast.expr.BooleanExpression;
5556
import org.codehaus.groovy.ast.expr.CastExpression;
5657
import org.codehaus.groovy.ast.expr.ClassExpression;
5758
import org.codehaus.groovy.ast.expr.ClosureExpression;
@@ -185,6 +186,7 @@
185186
import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
186187
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
187188
import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
189+
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS;
188190
import static org.codehaus.groovy.ast.tools.GeneralUtils.isOrImplements;
189191
import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
190192
import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
@@ -219,6 +221,7 @@
219221
import static org.codehaus.groovy.syntax.Types.KEYWORD_IN;
220222
import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF;
221223
import static org.codehaus.groovy.syntax.Types.LEFT_SQUARE_BRACKET;
224+
import static org.codehaus.groovy.syntax.Types.LOGICAL_OR;
222225
import static org.codehaus.groovy.syntax.Types.MINUS_MINUS;
223226
import static org.codehaus.groovy.syntax.Types.MOD;
224227
import static org.codehaus.groovy.syntax.Types.MOD_EQUAL;
@@ -3793,21 +3796,8 @@ protected void typeCheckClosureCall(final Expression callArguments, final ClassN
37933796
@Override
37943797
public void visitIfElse(final IfStatement ifElse) {
37953798
Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
3796-
37973799
try {
3798-
// create a new temporary element in the if-then-else type info
3799-
typeCheckingContext.pushTemporaryTypeInfo();
3800-
visitStatement(ifElse);
3801-
ifElse.getBooleanExpression().visit(this);
3802-
ifElse.getIfBlock().visit(this);
3803-
3804-
// pop if-then-else temporary type info
3805-
typeCheckingContext.popTemporaryTypeInfo();
3806-
3807-
// GROOVY-6099: restore assignment info as before the if branch
3808-
restoreTypeBeforeConditional();
3809-
3810-
ifElse.getElseBlock().visit(this);
3800+
visitIfElseMaybeOrBranches(ifElse, true);
38113801
} finally {
38123802
popAssignmentTracking(oldTracker);
38133803
}
@@ -3822,6 +3812,52 @@ public void visitIfElse(final IfStatement ifElse) {
38223812
}
38233813
}
38243814

3815+
private void visitIfElseMaybeOrBranches(IfStatement ifElse, boolean topLevel) {
3816+
BooleanExpression condition = ifElse.getBooleanExpression();
3817+
BinaryExpression lor = null;
3818+
if (condition.getExpression() instanceof BinaryExpression) {
3819+
lor = (BinaryExpression) condition.getExpression();
3820+
if (lor.getOperation().getType() != LOGICAL_OR) {
3821+
lor = null;
3822+
}
3823+
}
3824+
// for logical OR, any one branch may be true branch, so traverse separately
3825+
if (lor != null) {
3826+
IfStatement left = ifElseS(lor.getLeftExpression(), ifElse.getIfBlock(), ifElse.getElseBlock());
3827+
// left.setSourcePosition(ifElse);
3828+
typeCheckingContext.pushTemporaryTypeInfo();
3829+
visitIfElseMaybeOrBranches(left, false);
3830+
typeCheckingContext.popTemporaryTypeInfo();
3831+
restoreTypeBeforeConditional();
3832+
IfStatement right = ifElseS(lor.getRightExpression(), ifElse.getIfBlock(), ifElse.getElseBlock());
3833+
// right.setSourcePosition(ifElse);
3834+
typeCheckingContext.pushTemporaryTypeInfo();
3835+
visitIfElseMaybeOrBranches(right, false);
3836+
typeCheckingContext.popTemporaryTypeInfo();
3837+
restoreTypeBeforeConditional();
3838+
}
3839+
if (topLevel || lor == null) {
3840+
// do it all again to get correct union type for casting (hush warnings?)
3841+
visitIfElseBranches(ifElse);
3842+
}
3843+
}
3844+
3845+
private void visitIfElseBranches(IfStatement ifElse) {
3846+
// create a new temporary element in the if-then-else type info
3847+
typeCheckingContext.pushTemporaryTypeInfo();
3848+
visitStatement(ifElse);
3849+
ifElse.getBooleanExpression().visit(this);
3850+
ifElse.getIfBlock().visit(this);
3851+
3852+
// pop if-then-else temporary type info
3853+
typeCheckingContext.popTemporaryTypeInfo();
3854+
3855+
// GROOVY-6099: restore assignment info as before the if branch
3856+
restoreTypeBeforeConditional();
3857+
3858+
ifElse.getElseBlock().visit(this);
3859+
}
3860+
38253861
protected void visitInstanceofNot(final BinaryExpression be) {
38263862
BlockStatement currentBlock = typeCheckingContext.enclosingBlocks.getFirst();
38273863
assert currentBlock != null;

src/test/groovy/transform/stc/MethodCallsSTCTest.groovy

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,56 @@ class MethodCallsSTCTest extends StaticTypeCheckingTestCase {
423423
''', 'Reference to method is ambiguous'
424424
}
425425

426+
// GROOVY-7971
427+
void testShouldNotFailWithMultiplePossibleMethods() {
428+
assertScript '''
429+
import groovy.json.JsonOutput
430+
431+
def test(value) {
432+
def out = new StringBuilder()
433+
def isString = value.class == String
434+
if (isString || value instanceof Map) {
435+
out.append(JsonOutput.toJson(value))
436+
}
437+
return out.toString()
438+
}
439+
440+
def string = test('two')
441+
assert string == '"two"'
442+
'''
443+
}
444+
445+
// GROOVY-7971
446+
void testShouldNotFailWithUnionTypeLUB() {
447+
assertScript '''
448+
class Foo extends AbstractCollection {
449+
def peek() { 3 }
450+
int size() { -1 }
451+
void clear() { }
452+
Iterator iterator() { null }
453+
}
454+
455+
def method(idx) {
456+
def component = idx == 1 ? new ArrayDeque() : new Stack()
457+
if (idx == 3) component = new Foo()
458+
component.clear() // 'clear' in LUB (AbstractCollection or Serializable or Cloneable)
459+
if (component instanceof ArrayDeque) {
460+
component.addFirst(1) // 'addFirst' only in ArrayDeque
461+
} else if (component instanceof Stack) {
462+
component.addElement(2) // 'addElement' only in Stack
463+
}
464+
if (component instanceof Foo || component instanceof ArrayDeque || component instanceof Stack) {
465+
// checked duck typing
466+
assert component.peek() in 1..3 // 'peek' in ArrayDeque and Stack but not LUB
467+
}
468+
}
469+
470+
method(1)
471+
method(2)
472+
method(3)
473+
'''
474+
}
475+
426476
void testInstanceOfOnExplicitParameter() {
427477
assertScript '''
428478
1.with { obj ->

0 commit comments

Comments
 (0)