Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GROOVY-7971: @CS flow typing incorrectly casting to map at runtime #1293

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,11 @@ dependencies {
tools "com.thoughtworks.qdox:qdox:$qdoxVersion"

testImplementation project(':groovy-ant')
testImplementation project(':groovy-xml')
testImplementation project(':groovy-dateutil')
testImplementation project(':groovy-json')
testImplementation project(':groovy-test')
testImplementation project(':groovy-macro')
testImplementation project(':groovy-xml')
}

ext.generatedDirectory = "${buildDir}/generated/sources"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,10 +408,17 @@ private static void memorizeInitialExpressions(final MethodNode node) {
public void visitMethodCallExpression(final MethodCallExpression call) {
super.visitMethodCallExpression(call);

MethodNode target = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
if (target != null) {
call.setMethodTarget(target);
memorizeInitialExpressions(target);
MethodNode newTarget = call.getNodeMetaData(DIRECT_METHOD_CALL_TARGET);
if (newTarget != null) {
MethodNode existingTarget = call.getMethodTarget();
if (existingTarget != null)
if (!(newTarget.getDeclaringClass().equals(existingTarget.getDeclaringClass())) || !(newTarget.getTypeDescriptor().equals(existingTarget.getTypeDescriptor()))) {
// avoid cast class exception
newTarget.putNodeMetaData(DYNAMIC_RESOLUTION, Boolean.TRUE);
call.putNodeMetaData(DYNAMIC_RESOLUTION, OBJECT_TYPE);
}
call.setMethodTarget(newTarget);
memorizeInitialExpressions(newTarget);
}

if (call.getMethodTarget() == null && call.getLineNumber() > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.codehaus.groovy.ast.expr.AttributeExpression;
import org.codehaus.groovy.ast.expr.BinaryExpression;
import org.codehaus.groovy.ast.expr.BitwiseNegationExpression;
import org.codehaus.groovy.ast.expr.BooleanExpression;
import org.codehaus.groovy.ast.expr.CastExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ClosureExpression;
Expand Down Expand Up @@ -184,6 +185,7 @@
import static org.codehaus.groovy.ast.tools.GeneralUtils.castX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
import static org.codehaus.groovy.ast.tools.GeneralUtils.ifElseS;
import static org.codehaus.groovy.ast.tools.GeneralUtils.isOrImplements;
import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
Expand Down Expand Up @@ -218,6 +220,7 @@
import static org.codehaus.groovy.syntax.Types.KEYWORD_IN;
import static org.codehaus.groovy.syntax.Types.KEYWORD_INSTANCEOF;
import static org.codehaus.groovy.syntax.Types.LEFT_SQUARE_BRACKET;
import static org.codehaus.groovy.syntax.Types.LOGICAL_OR;
import static org.codehaus.groovy.syntax.Types.MINUS_MINUS;
import static org.codehaus.groovy.syntax.Types.MOD;
import static org.codehaus.groovy.syntax.Types.MOD_EQUAL;
Expand Down Expand Up @@ -3798,21 +3801,8 @@ protected void typeCheckClosureCall(final Expression callArguments, final ClassN
@Override
public void visitIfElse(final IfStatement ifElse) {
Map<VariableExpression, List<ClassNode>> oldTracker = pushAssignmentTracking();

try {
// create a new temporary element in the if-then-else type info
typeCheckingContext.pushTemporaryTypeInfo();
visitStatement(ifElse);
ifElse.getBooleanExpression().visit(this);
ifElse.getIfBlock().visit(this);

// pop if-then-else temporary type info
typeCheckingContext.popTemporaryTypeInfo();

// GROOVY-6099: restore assignment info as before the if branch
restoreTypeBeforeConditional();

ifElse.getElseBlock().visit(this);
visitIfElseMaybeOrBranches(ifElse, true);
} finally {
popAssignmentTracking(oldTracker);
}
Expand All @@ -3827,6 +3817,52 @@ public void visitIfElse(final IfStatement ifElse) {
}
}

private void visitIfElseMaybeOrBranches(IfStatement ifElse, boolean topLevel) {
BooleanExpression condition = ifElse.getBooleanExpression();
BinaryExpression lor = null;
if (condition.getExpression() instanceof BinaryExpression) {
lor = (BinaryExpression) condition.getExpression();
if (lor.getOperation().getType() != LOGICAL_OR) {
lor = null;
}
}
// for logical OR, any one branch may be true branch, so traverse separately
if (lor != null) {
IfStatement left = ifElseS(lor.getLeftExpression(), ifElse.getIfBlock(), ifElse.getElseBlock());
// left.setSourcePosition(ifElse);
typeCheckingContext.pushTemporaryTypeInfo();
visitIfElseMaybeOrBranches(left, false);
typeCheckingContext.popTemporaryTypeInfo();
restoreTypeBeforeConditional();
IfStatement right = ifElseS(lor.getRightExpression(), ifElse.getIfBlock(), ifElse.getElseBlock());
// right.setSourcePosition(ifElse);
typeCheckingContext.pushTemporaryTypeInfo();
visitIfElseMaybeOrBranches(right, false);
typeCheckingContext.popTemporaryTypeInfo();
restoreTypeBeforeConditional();
}
if (topLevel || lor == null) {
// do it all again to get correct union type for casting (hush warnings?)
visitIfElseBranches(ifElse);
}
}

private void visitIfElseBranches(IfStatement ifElse) {
// create a new temporary element in the if-then-else type info
typeCheckingContext.pushTemporaryTypeInfo();
visitStatement(ifElse);
ifElse.getBooleanExpression().visit(this);
ifElse.getIfBlock().visit(this);

// pop if-then-else temporary type info
typeCheckingContext.popTemporaryTypeInfo();

// GROOVY-6099: restore assignment info as before the if branch
restoreTypeBeforeConditional();

ifElse.getElseBlock().visit(this);
}

protected void visitInstanceofNot(final BinaryExpression be) {
BlockStatement currentBlock = typeCheckingContext.enclosingBlocks.getFirst();
assert currentBlock != null;
Expand Down
50 changes: 50 additions & 0 deletions src/test/groovy/transform/stc/MethodCallsSTCTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,56 @@ class MethodCallsSTCTest extends StaticTypeCheckingTestCase {
''', 'Reference to method is ambiguous'
}

// GROOVY-7971
void testShouldNotFailWithMultiplePossibleMethods() {
assertScript '''
import groovy.json.JsonOutput

def test(value) {
def out = new StringBuilder()
def isString = value.class == String
if (isString || value instanceof Map) {
out.append(JsonOutput.toJson(value))
}
return out.toString()
}

def string = test('two')
assert string == '"two"'
'''
}

// GROOVY-7971
void testShouldNotFailWithUnionTypeLUB() {
assertScript '''
class Foo extends AbstractCollection {
def peek() { 3 }
int size() { -1 }
void clear() { }
Iterator iterator() { null }
}

def method(idx) {
def component = idx == 1 ? new ArrayDeque() : new Stack()
if (idx == 3) component = new Foo()
component.clear() // 'clear' in LUB (AbstractCollection or Serializable or Cloneable)
if (component instanceof ArrayDeque) {
component.addFirst(1) // 'addFirst' only in ArrayDeque
} else if (component instanceof Stack) {
component.addElement(2) // 'addElement' only in Stack
}
if (component instanceof Foo || component instanceof ArrayDeque || component instanceof Stack) {
// checked duck typing
assert component.peek() in 1..3 // 'peek' in ArrayDeque and Stack but not LUB
}
}

method(1)
method(2)
method(3)
'''
}

void testInstanceOfOnExplicitParameter() {
assertScript '''
1.with { obj ->
Expand Down