| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package groovy.transform.stc |
| |
| import org.codehaus.groovy.ast.MethodNode |
| import org.codehaus.groovy.control.customizers.CompilationCustomizer |
| import org.codehaus.groovy.control.CompilePhase |
| import org.codehaus.groovy.control.SourceUnit |
| import org.codehaus.groovy.classgen.GeneratorContext |
| import org.codehaus.groovy.ast.ClassNode |
| import org.codehaus.groovy.ast.ClassHelper |
| import org.codehaus.groovy.transform.stc.StaticTypesMarker |
| import org.codehaus.groovy.ast.tools.WideningCategories |
| |
| /** |
| * Unit tests for static type checking : type inference. |
| */ |
| class TypeInferenceSTCTest extends StaticTypeCheckingTestCase { |
| |
| void testStringToInteger() { |
| assertScript """ |
| def name = "123" // we want type inference |
| name.toInteger() // toInteger() is defined by DGM |
| """ |
| } |
| |
| void testGStringMethods() { |
| assertScript ''' |
| def myname = 'Cedric' |
| "My upper case name is ${myname.toUpperCase()}" |
| println "My upper case name is ${myname}".toUpperCase() |
| ''' |
| } |
| |
| void testAnnotationOnSingleMethod() { |
| GroovyShell shell = new GroovyShell() |
| shell.evaluate ''' |
| // calling a method which has got some dynamic stuff in it |
| |
| import groovy.transform.TypeChecked |
| import groovy.xml.MarkupBuilder |
| |
| class Greeter { |
| @TypeChecked |
| String greeting(String name) { |
| generateMarkup(name.toUpperCase()) |
| } |
| |
| // MarkupBuilder is dynamic so we won't do typechecking here |
| String generateMarkup(String name) { |
| def sw = new StringWriter() |
| def mkp = new MarkupBuilder() |
| mkp.html { |
| body { |
| div name |
| } |
| } |
| sw |
| } |
| } |
| |
| def g = new Greeter() |
| g.greeting("Guillaume") |
| |
| ''' |
| } |
| |
| void testInstanceOf() { |
| assertScript """ |
| Object o |
| if (o instanceof String) o.toUpperCase() |
| """ |
| } |
| |
| void testEmbeddedInstanceOf() { |
| assertScript """ |
| Object o |
| if (o instanceof Object) { |
| if (o instanceof String) { |
| o.toUpperCase() |
| } |
| } |
| """ |
| } |
| |
| void testEmbeddedInstanceOf2() { |
| assertScript """ |
| Object o |
| if (o instanceof String) { |
| if (true) { |
| o.toUpperCase() |
| } |
| } |
| """ |
| } |
| |
| void testEmbeddedInstanceOf3() { |
| shouldFailWithMessages ''' |
| Object o |
| if (o instanceof String) { |
| if (o instanceof Object) { // causes the inferred type of 'o' to be overwritten |
| o.toUpperCase() |
| } |
| } |
| ''', 'Cannot find matching method java.lang.Object#toUpperCase()' |
| } |
| |
| void testInstanceOfAfterEach() { |
| shouldFailWithMessages ''' |
| Object o |
| if (o instanceof String) { |
| o.toUpperCase() |
| } |
| o.toUpperCase() // ensure that type information is lost after if() |
| ''', 'Cannot find matching method java.lang.Object#toUpperCase()' |
| } |
| |
| void testInstanceOfInElseBranch() { |
| shouldFailWithMessages ''' |
| Object o |
| if (o instanceof String) { |
| o.toUpperCase() |
| } else { |
| o.toUpperCase() // ensure that type information is lost in else() |
| } |
| ''', 'Cannot find matching method java.lang.Object#toUpperCase()' |
| } |
| |
| void testMultipleInstanceOf() { |
| assertScript ''' |
| class A { |
| void foo() { println 'ok' } |
| } |
| |
| class B { |
| void foo() { println 'ok' } |
| void foo2() { println 'ok 2' } |
| } |
| |
| |
| def o = new A() |
| |
| if (o instanceof A) { |
| o.foo() |
| } |
| |
| if (o instanceof B) { |
| o.foo() |
| } |
| |
| if (o instanceof A || o instanceof B) { |
| o.foo() |
| } |
| |
| ''' |
| } |
| |
| void testInstanceOfInTernaryOp() { |
| assertScript ''' |
| class A { |
| int foo() { 1 } |
| } |
| |
| class B { |
| int foo2() { 2 } |
| } |
| |
| |
| def o = new A() |
| |
| int result = o instanceof A?o.foo():(o instanceof B?o.foo2():3) |
| |
| ''' |
| } |
| |
| void testShouldNotAllowDynamicVariable() { |
| shouldFailWithMessages ''' |
| String name = 'Guillaume' |
| println naamme |
| ''', 'The variable [naamme] is undeclared' |
| } |
| |
| void testInstanceOfInferenceWithImplicitIt() { |
| assertScript ''' |
| ['a', 'b', 'c'].each { |
| if (it instanceof String) { |
| println it.toUpperCase() |
| } |
| } |
| ''' |
| } |
| |
| void testInstanceOfTypeInferenceWithDef() { |
| assertScript ''' |
| def profile = ['Guillaume', 34, true] |
| def item = profile[0] |
| if (item instanceof String) { |
| println item.toUpperCase() |
| } |
| ''' |
| } |
| |
| void testInstanceOfTypeInferenceWithoutDef() { |
| assertScript ''' |
| def profile = ['Guillaume', 34, true] |
| if (profile[0] instanceof String) { |
| println profile[0].toUpperCase() |
| } |
| ''' |
| } |
| |
| void testInstanceOfInferenceWithField() { |
| assertScript ''' |
| class A { |
| int x |
| } |
| def a |
| if (a instanceof A) { |
| a.x = 2 |
| } |
| ''' |
| } |
| |
| void testInstanceOfInferenceWithFieldAndAssignment() { |
| shouldFailWithMessages ''' |
| class A { |
| int x |
| } |
| def a = new A() |
| if (a instanceof A) { |
| a.x = '2' |
| } |
| ''', 'Cannot assign value of type java.lang.String to variable of type int' |
| } |
| |
| void testInstanceOfInferenceWithMissingField() { |
| shouldFailWithMessages ''' |
| class A { |
| int x |
| } |
| def a |
| if (a instanceof A) { |
| a.y = 2 |
| } |
| ''', 'No such property: y for class: A' |
| } |
| |
| void testShouldNotFailWithWith() { |
| assertScript ''' |
| class A { |
| int x |
| } |
| def a = new A() |
| a.with { |
| x = 2 // should be recognized as a.x at compile time |
| } |
| assert a.x == 2 |
| ''' |
| } |
| |
| void testShouldFailWithWith() { |
| shouldFailWithMessages ''' |
| class A { |
| int x |
| } |
| def a = new A() |
| a.with { |
| x = '2' // should be recognized as a.x at compile time and fail because of wrong type |
| } |
| ''', 'Cannot assign value of type java.lang.String to variable of type int' |
| } |
| |
| void testShouldNotFailWithWithTwoClasses() { |
| // we must make sure that type inference engine in this case |
| // takes the same property as at runtime |
| assertScript ''' |
| class A { |
| int x |
| } |
| class B { |
| String x |
| } |
| def a = new A() |
| def b = new B() |
| a.with { |
| b.with { |
| x = '2' // should be recognized as b.x at compile time |
| } |
| } |
| assert a.x == 0 |
| assert b.x == '2' |
| ''' |
| } |
| |
| void testShouldNotFailWithWithAndImplicitIt() { |
| assertScript ''' |
| class A { |
| int x |
| } |
| def a = new A() |
| a.with { |
| it.x = 2 // should be recognized as a.x at compile time |
| } |
| assert a.x == 2 |
| ''' |
| } |
| |
| void testShouldNotFailWithWithAndExplicitIt() { |
| assertScript ''' |
| class A { |
| int x |
| } |
| def a = new A() |
| a.with { it -> |
| it.x = 2 // should be recognized as a.x at compile time |
| } |
| assert a.x == 2 |
| ''' |
| } |
| |
| void testShouldNotFailWithWithAndExplicitTypedIt() { |
| shouldFailWithMessages ''' |
| class A { |
| int x |
| } |
| def a = new A() |
| a.with { String it -> |
| it.x = 2 // should be recognized as a.x at compile time |
| } |
| ''', 'Expected parameter of type A but got java.lang.String' |
| } |
| |
| void testShouldNotFailWithInheritanceAndWith() { |
| assertScript ''' |
| class A { |
| int x |
| void method() { println x } |
| } |
| class B extends A { |
| } |
| def b = new B() |
| b.with { |
| x = 2 // should be recognized as b.x at compile time |
| } |
| assert b.x == 2 |
| ''' |
| } |
| |
| void testCallMethodInWithContext() { |
| assertScript ''' |
| class A { |
| int method() { return 1 } |
| } |
| def a = new A() |
| a.with { |
| method() |
| } |
| ''' |
| } |
| |
| |
| void testCallMethodInWithContextAndShadowing() { |
| // make sure that the method which is found in 'with' is actually the one from class A |
| // which returns a String |
| assertScript ''' |
| class A { |
| String method() { return 'Cedric' } |
| } |
| |
| int method() { 1 } |
| |
| def a = new A() |
| a.with { |
| method().toUpperCase() |
| } |
| ''' |
| // check that if we switch signatures, it fails |
| shouldFailWithMessages ''' |
| class A { |
| int method() { 1 } |
| } |
| |
| String method() { 'Cedric' } |
| |
| def a = new A() |
| a.with { |
| method().toUpperCase() |
| } |
| ''', 'Cannot find matching method int#toUpperCase()' |
| } |
| |
| void testDeclarationTypeInference() { |
| MethodNode method |
| config.addCompilationCustomizers(new CompilationCustomizer(CompilePhase.CLASS_GENERATION) { |
| @Override |
| void call(SourceUnit source, GeneratorContext context, ClassNode classNode) { |
| method = classNode.methods.find { it.name == 'method' } |
| } |
| |
| }) |
| assertScript ''' |
| void method() { |
| def o |
| o = 1 |
| o = 'String' |
| } |
| ''' |
| def inft = method.code.statements[0].expression.leftExpression.getNodeMetaData(StaticTypesMarker.DECLARATION_INFERRED_TYPE) |
| assert inft instanceof WideningCategories.LowestUpperBoundClassNode |
| [Comparable, Serializable].each { |
| assert ClassHelper.make(it) in inft.interfaces |
| } |
| |
| assertScript ''' |
| void method() { |
| def o |
| o = 1 |
| o = 2 |
| } |
| ''' |
| assert method.code.statements[0].expression.leftExpression.getNodeMetaData(StaticTypesMarker.DECLARATION_INFERRED_TYPE) == ClassHelper.int_TYPE |
| |
| assertScript ''' |
| void method() { |
| def o |
| o = 1L |
| o = 2 |
| } |
| ''' |
| inft = method.code.statements[0].expression.leftExpression.getNodeMetaData(StaticTypesMarker.DECLARATION_INFERRED_TYPE) |
| assert inft == ClassHelper.long_TYPE |
| |
| assertScript ''' |
| void method() { |
| def o |
| o = new HashSet() |
| o = new LinkedHashSet() |
| } |
| ''' |
| assert method.code.statements[0].expression.leftExpression.getNodeMetaData(StaticTypesMarker.DECLARATION_INFERRED_TYPE) == ClassHelper.make(HashSet) |
| |
| |
| } |
| |
| void testChooseMethodWithTypeInference() { |
| assertScript ''' |
| void method(Object o) { println 'Object' } |
| void method(int i) { println 'int' } |
| def obj = 1 |
| method(obj) |
| ''' |
| } |
| |
| void testStarOperatorOnMap() { |
| assertScript ''' |
| List keys = [x:1,y:2,z:3]*.key |
| List values = [x:1,y:2,z:3]*.value |
| ''' |
| } |
| |
| void testStarOperatorOnMap2() { |
| assertScript ''' |
| List keys = [x:1,y:2,z:3]*.key |
| List values = [x:'1',y:'2',z:'3']*.value |
| keys*.toUpperCase() |
| values*.toUpperCase() |
| ''' |
| |
| shouldFailWithMessages ''' |
| List values = [x:1,y:2,z:3]*.value |
| values*.toUpperCase() |
| ''', 'Cannot find matching method java.lang.Integer#toUpperCase()' |
| } |
| |
| void testStarOperatorOnMap3() { |
| assertScript ''' |
| def keys = [x:1,y:2,z:3]*.key |
| def values = [x:'1',y:'2',z:'3']*.value |
| keys*.toUpperCase() |
| values*.toUpperCase() |
| ''' |
| |
| shouldFailWithMessages ''' |
| def values = [x:1,y:2,z:3]*.value |
| values*.toUpperCase() |
| ''', 'Cannot find matching method java.lang.Integer#toUpperCase()' |
| } |
| |
| void testFlowTypingWithStringVariable() { |
| // as anything can be assigned to a string, flow typing engine |
| // could "erase" the type of the original variable although is must not |
| assertScript ''' |
| String str = new Object() // type checker will not complain, anything assignable to a String |
| str.toUpperCase() // should not complain |
| ''' |
| } |
| |
| void testDefTypeAfterLongThenIntAssignments() { |
| assertScript ''' |
| def o |
| o = 1L |
| o = 2 |
| @ASTTest(phase=INSTRUCTION_SELECTION, value= { |
| assert node.rightExpression.accessedVariable.getNodeMetaData(DECLARATION_INFERRED_TYPE) == long_TYPE |
| }) |
| def z = o |
| ''' |
| } |
| |
| // GROOVY-5519 |
| void testInferThrowable() { |
| assertScript ''' |
| try { |
| throw new RuntimeException('ok') |
| } catch (e) { |
| handleError(e) |
| } |
| void handleError(Throwable e) { |
| assert e.message == 'ok' |
| } |
| ''' |
| } |
| |
| void testInferMapValueType() { |
| assertScript ''' |
| Map<String, Integer> map = new HashMap<String,Integer>() |
| map['foo'] = 123 |
| map['bar'] = 246 |
| Integer foo = map['foo'] |
| assert foo == 123 |
| Integer bar = map.get('bar') |
| assert bar == 246 |
| ''' |
| } |
| |
| // GROOVY-5522 |
| void testTypeInferenceWithArrayAndFind() { |
| assertScript ''' |
| File findFile() { |
| new File[0].find { File f -> f.hidden } |
| } |
| findFile() |
| ''' |
| } |
| |
| void testShouldNotThrowIncompatibleArgToFunVerifyError() { |
| assertScript ''' |
| Object convertValueToType(Object value, Class targetType) { |
| if (value instanceof CharSequence) { |
| value = value.toString() |
| } |
| if (value instanceof String) { |
| String strValue = value.trim() |
| } |
| } |
| convertValueToType('foo', String) |
| ''' |
| } |
| |
| void testSwitchCaseAnalysis() { |
| assertScript '''import org.codehaus.groovy.ast.tools.WideningCategories.LowestUpperBoundClassNode as LUB |
| |
| def method(int x) { |
| def returnValue= new Date() |
| switch (x) { |
| case 1: |
| returnValue = 'string' |
| break; |
| case 2: |
| returnValue = 1; |
| break; |
| } |
| @ASTTest(phase=INSTRUCTION_SELECTION,value={ |
| def ift = node.getNodeMetaData(INFERRED_TYPE) |
| assert ift instanceof LUB |
| assert ift.name == 'java.io.Serializable' |
| }) |
| def val = returnValue |
| |
| returnValue |
| } |
| ''' |
| } |
| |
| void testGroovy6215() { |
| assertScript ''' |
| def processNumber(int x) { |
| def value = getValueForNumber(x) |
| value |
| } |
| |
| def getValueForNumber(int x) { |
| def valueToReturn |
| switch(x) { |
| case 1: |
| valueToReturn = 'One' |
| break |
| case 2: |
| valueToReturn = [] |
| valueToReturn << 'Two' |
| break |
| } |
| valueToReturn |
| } |
| def v1 = getValueForNumber(1) |
| def v2 = getValueForNumber(2) |
| def v3 = getValueForNumber(3) |
| assert v1 == 'One' |
| assert v2 == ['Two'] |
| assert v3 == null |
| ''' |
| } |
| |
| void testNumberPrefixPlusPlusInference() { |
| [Byte:'Integer', |
| Character: 'Character', |
| Short: 'Integer', |
| Integer: 'Integer', |
| Long: 'Long', |
| Float: 'Double', |
| Double: 'Double', |
| BigDecimal: 'BigDecimal', |
| BigInteger: 'BigInteger' |
| ].each { orig, dest -> |
| assertScript """ |
| $orig b = 65 as $orig |
| @ASTTest(phase=INSTRUCTION_SELECTION, value={ |
| def rit = node.rightExpression.getNodeMetaData(INFERRED_TYPE) |
| assert rit == make($dest) |
| }) |
| def pp = ++b |
| println '++${orig} -> ' + pp.class + ' ' + pp |
| assert pp.class == ${dest} |
| """ |
| } |
| } |
| |
| void testNumberPostfixPlusPlusInference() { |
| [Byte:'Byte', |
| Character: 'Character', |
| Short: 'Short', |
| Integer: 'Integer', |
| Long: 'Long', |
| Float: 'Float', |
| Double: 'Double', |
| BigDecimal: 'BigDecimal', |
| BigInteger: 'BigInteger' |
| ].each { orig, dest -> |
| assertScript """ |
| $orig b = 65 as $orig |
| @ASTTest(phase=INSTRUCTION_SELECTION, value={ |
| def rit = node.rightExpression.getNodeMetaData(INFERRED_TYPE) |
| assert rit == make($dest) |
| }) |
| def pp = b++ |
| println '${orig}++ -> ' + pp.class + ' ' + pp |
| assert pp.class == ${dest} |
| """ |
| } |
| } |
| |
| // GROOVY-6522 |
| void testInferenceWithImplicitClosureCoercion() { |
| assertScript ''' |
| interface CustomCallable<T> { |
| T call() |
| } |
| |
| class Thing { |
| static <T> T customType(CustomCallable<T> callable) { |
| callable.call() |
| } |
| |
| @ASTTest(phase=INSTRUCTION_SELECTION,value={ |
| lookup('test').each { |
| def call = it.expression |
| def irt = call.getNodeMetaData(INFERRED_TYPE) |
| assert irt == LIST_TYPE |
| } |
| }) |
| static void run() { |
| test: customType { [] } // return type is not inferred - fails compile |
| } |
| } |
| |
| Thing.run() |
| ''' |
| } |
| |
| void testInferenceWithImplicitClosureCoercionAndArrayReturn() { |
| assertScript ''' |
| interface ArrayFactory<T> { T[] array() } |
| |
| public <T> T[] intArray(ArrayFactory<T> f) { |
| f.array() |
| } |
| @ASTTest(phase=INSTRUCTION_SELECTION,value={ |
| assert node.getNodeMetaData(INFERRED_TYPE) == Integer_TYPE.makeArray() |
| }) |
| def array = intArray { new Integer[8] } |
| assert array.length == 8 |
| ''' |
| } |
| |
| void testInferenceWithImplicitClosureCoercionAndListReturn() { |
| assertScript ''' |
| interface ListFactory<T> { List<T> list() } |
| |
| public <T> List<T> list(ListFactory<T> f) { |
| f.list() |
| } |
| |
| @ASTTest(phase=INSTRUCTION_SELECTION,value={ |
| def irt = node.getNodeMetaData(INFERRED_TYPE) |
| assert irt == LIST_TYPE |
| assert irt.genericsTypes[0].type == Integer_TYPE |
| }) |
| def res = list { new LinkedList<Integer>() } |
| assert res.size() == 0 |
| ''' |
| } |
| |
| // GROOVY-6835 |
| void testFlowTypingWithInstanceofAndInterfaceTypes() { |
| assertScript ''' |
| class ShowUnionTypeBug { |
| Map<String, Object> instanceMap = (Map<String,Object>)['a': 'Hello World'] |
| def findInstance(String key) { |
| Set<? extends CharSequence> allInstances = [] as Set |
| def instance = instanceMap.get(key) |
| if(instance instanceof CharSequence) { |
| allInstances.add(instance) |
| } |
| allInstances |
| } |
| } |
| assert new ShowUnionTypeBug().findInstance('a') == ['Hello World'] as Set |
| ''' |
| } |
| |
| void testInferenceWithImplicitClosureCoercionAndGenericTypeAsParameter() { |
| assertScript ''' |
| interface Action<T> { void execute(T t) } |
| |
| public <T> void exec(T t, Action<T> f) { |
| f.execute(t) |
| } |
| |
| exec('foo') { println it.toUpperCase() } |
| ''' |
| } |
| |
| // GROOVY-6574 |
| void testShouldInferPrimitiveBoolean() { |
| assertScript ''' |
| def foo(Boolean o) { |
| @ASTTest(phase=INSTRUCTION_SELECTION,value={ |
| assert node.getNodeMetaData(INFERRED_TYPE) == boolean_TYPE |
| }) |
| boolean b = o |
| println b |
| } |
| ''' |
| } |
| |
| // GROOVY-9077 |
| void testInferredTypeForPropertyThatResolvesToMethod() { |
| assertScript ''' |
| import groovy.transform.* |
| import static org.codehaus.groovy.transform.stc.StaticTypesMarker.DIRECT_METHOD_CALL_TARGET |
| |
| @CompileStatic |
| void meth() { |
| def items = [1, 2] as LinkedList |
| |
| @ASTTest(phase=INSTRUCTION_SELECTION, value={ |
| node = node.rightExpression |
| assert node.class.name.contains('PropertyExpression') |
| def target = node.getNodeMetaData(DIRECT_METHOD_CALL_TARGET) |
| assert target != null |
| assert target.declaringClass.name == 'java.util.LinkedList' |
| }) |
| def one = items.first |
| |
| @ASTTest(phase=CLASS_GENERATION, value={ |
| node = node.rightExpression |
| assert node.class.name.contains('MethodCallExpression') |
| def target = node.getNodeMetaData(DIRECT_METHOD_CALL_TARGET) |
| assert target != null |
| assert target.declaringClass.name == 'java.util.LinkedList' |
| }) |
| def alsoOne = items.peek() |
| } |
| |
| meth() |
| ''' |
| } |
| } |