Skip to content

Commit 52f6291

Browse files
committed
GROOVY-7971: @cs flow typing incorrectly casting to map at runtime (closes #1293)
1 parent 9116ffb commit 52f6291

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;
@@ -184,6 +185,7 @@
184185
import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
185186
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
186187
import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
188+
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS;
187189
import static org.codehaus.groovy.ast.tools.GeneralUtils.isOrImplements;
188190
import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
189191
import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
@@ -218,6 +220,7 @@
218220
import static org.codehaus.groovy.syntax.Types.KEYWORD_IN;
219221
import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF;
220222
import static org.codehaus.groovy.syntax.Types.LEFT_SQUARE_BRACKET;
223+
import static org.codehaus.groovy.syntax.Types.LOGICAL_OR;
221224
import static org.codehaus.groovy.syntax.Types.MINUS_MINUS;
222225
import static org.codehaus.groovy.syntax.Types.MOD;
223226
import static org.codehaus.groovy.syntax.Types.MOD_EQUAL;
@@ -3798,21 +3801,8 @@ protected void typeCheckClosureCall(final Expression callArguments, final ClassN
37983801
@Override
37993802
public void visitIfElse(final IfStatement ifElse) {
38003803
Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();
3801-
38023804
try {
3803-
// create a new temporary element in the if-then-else type info
3804-
typeCheckingContext.pushTemporaryTypeInfo();
3805-
visitStatement(ifElse);
3806-
ifElse.getBooleanExpression().visit(this);
3807-
ifElse.getIfBlock().visit(this);
3808-
3809-
// pop if-then-else temporary type info
3810-
typeCheckingContext.popTemporaryTypeInfo();
3811-
3812-
// GROOVY-6099: restore assignment info as before the if branch
3813-
restoreTypeBeforeConditional();
3814-
3815-
ifElse.getElseBlock().visit(this);
3805+
visitIfElseMaybeOrBranches(ifElse, true);
38163806
} finally {
38173807
popAssignmentTracking(oldTracker);
38183808
}
@@ -3827,6 +3817,52 @@ public void visitIfElse(final IfStatement ifElse) {
38273817
}
38283818
}
38293819

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