blob: ef84f0d1c30dec1909b858a189c6edd5f03bd739 [file] [log] [blame]
/*
* 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 groovy.test.NotYetImplemented
/**
* Unit tests for static type checking : closures.
*/
class ClosuresSTCTest extends StaticTypeCheckingTestCase {
void testCallClosure1() {
assertScript '''
def c = { return 'foo' }
assert c() == 'foo'
'''
}
void testCallClosure2() {
assertScript '''
def c = { -> return 'foo' }
assert c() == 'foo'
'''
}
@NotYetImplemented
void testCallClosure3() {
shouldFailWithMessages '''
def c = { -> }
c("")
''',
'Cannot call closure that accepts [] with [java.lang.String]'
}
void testCallClosure4() {
assertScript '''
def c = { int a, int b -> a + b }
assert c(5, 7) == 12
'''
}
void testCallClosure5() {
shouldFailWithMessages '''
def c = { int a, int b -> a + b }
c('5', '7')
''',
'Cannot call closure that accepts [int, int] with [java.lang.String, java.lang.String]'
}
void testCallClosure6() {
assertScript '''
def result = { int a, int b -> a + b }(5, 7)
assert result == 12
'''
}
void testCallClosure7() {
shouldFailWithMessages '''
{ int a, int b -> a + b }('5', 7)
''',
'Cannot call closure that accepts [int, int] with [java.lang.String, int]'
}
// GROOVY-6365
void testCallClosure8() {
assertScript '''
def c = { Object[] args -> args.length }
assert c('one', 'two') == 2
'''
}
// GROOVY-10071
void testCallClosure9() {
assertScript '''
def c = { ... zeroOrMore -> return 'foo' + zeroOrMore }
assert c('bar', 'baz') == 'foo[bar, baz]'
assert c('bar') == 'foo[bar]'
assert c() == 'foo[]'
'''
}
// GROOVY-10072
void testCallClosure10() {
assertScript '''
def c = { p = 'foo' -> return p }
assert c('bar') == 'bar'
assert c() == 'foo'
'''
}
// GROOVY-10072
void testCallClosure11() {
assertScript '''
def c = { Number n, Number total = 41 -> total += n }
assert c(1) == 42
'''
}
// GROOVY-10072
void testCallClosure12() {
shouldFailWithMessages '''
def c = { Number n, Number total = new Date() -> total += n }
''',
'Cannot assign value of type java.util.Date to variable of type java.lang.Number'
}
// GROOVY-10636
void testCallClosure13() {
assertScript '''
def f(Closure<Number>... closures) {
closures*.call().sum()
}
Object result = f({->1},{->2})
assert result == 3
'''
}
// GROOVY-10636
void testCallClosure14() {
shouldFailWithMessages '''
def f(Closure<Number>... closures) {
}
f({->1},{->'x'})
''',
'Cannot return value of type java.lang.String for closure expecting java.lang.Number'
}
// GROOVY-11023
void testCallClosure15() {
assertScript '''
def c = { p, q = p.toString() -> '' + p + q }
assert c('foo', 'bar') == 'foobar'
assert c('foo') == 'foofoo'
'''
}
//
void testClosureReturnTypeInference1() {
assertScript '''
def c = { int a, int b -> return a + b }
int total = c(2, 3)
assert total == 5
'''
}
void testClosureReturnTypeInference2() {
assertScript '''
int total = { int a, int b -> return a + b }(2, 3)
'''
}
void testClosureReturnTypeInference3() {
shouldFailWithMessages '''
def c = { int x ->
if (x == 0) {
1L
} else {
x
}
}
byte res = c(0)
''',
'Possible loss of precision from long to byte'
}
// GROOVY-9907
void testClosureReturnTypeInference4() {
assertScript '''
Integer foo(x) {
if (x instanceof Integer) {
def bar = { -> return x }
return bar.call()
}
return 0
}
assert foo(1) == 1
'''
}
// GROOVY-9971
void testClosureReturnTypeInference5() {
assertScript '''
def m(Closure<String> c) {
c.call()
}
final x = 123
Closure<String> c = { -> "x=$x" }
String type = c.call().class.name
assert type == 'java.lang.String'
type = (m { -> "x=$x" }).class.name
assert type == 'java.lang.String' // not GStringImpl
'''
}
// GROOVY-10082, GROOVY-10277
void testClosureReturnTypeInference6() {
assertScript '''
class A {}
class B extends A {}
Closure<A> c = { -> new B() }
def result = c()
assert result instanceof A
assert result instanceof B
'''
shouldFailWithMessages '''
Closure<String> c = { -> 42 }
''',
'Cannot return value of type int for closure expecting java.lang.String'
}
// GROOVY-10091, GROOVY-10277
void testClosureReturnTypeInference7() {
shouldFailWithMessages '''
class A<T> {}
class B extends A<Number> {}
class X extends A<String> {}
class Y<Z> extends A<Number> {}
Closure<A<Number>> c
c = { -> return new B() }
c = { -> return new X() }
c = { -> return new Y<String>() }
''',
'Cannot return value of type X for closure expecting A<java.lang.Number>'
}
// GROOVY-10792
void testClosureReturnTypeInference8() {
shouldFailWithMessages '''
void proc(Closure<Boolean> c) {
boolean result = c().booleanValue()
assert !result
}
def list = []
proc {
list
}
''',
'Cannot return value of type java.util.ArrayList<java.lang.Object> for closure expecting java.lang.Boolean'
}
// GROOVY-8202
void testClosureReturnTypeInference9() {
assertScript '''
void proc() {
}
String test0(flag) {
if (flag) {
'foo'
} else {
proc()
}
}
String test1(flag) {
Closure<String> c = { ->
if (flag) {
'bar'
} else {
proc()
null
}
}
c.call()
}
String test2(flag) {
Closure<String> c = { -> // Cannot assign Closure<Object> to Closure<String>
if (flag) {
'baz'
} else {
proc()
}
}
c.call()
}
assert test0(true) == 'foo'
assert test1(true) == 'bar'
assert test2(true) == 'baz'
assert test0(false) == null
assert test1(false) == null
assert test2(false) == null
'''
assertScript '''
Closure<Void> c = { flag ->
if (flag) {
print 'true'
} else {
print 'false'
}
}
'''
}
// GROOVY-5145
void testCollect() {
assertScript '''
List<String> strings = [1,2,3].collect { it.toString() }
'''
}
// GROOVY-5145
void testCollectWithSubclass() {
assertScript '''
class StringClosure extends Closure<String> {
StringClosure() { super(null,null) }
void doCall(int x) { x }
}
List<String> strings = [1,2,3].collect(new StringClosure())
'''
}
// GROOVY-7701
void testWithDelegateVsOwnerField() {
assertScript '''
class Foo {
List type
}
class Bar {
int type = 10
@Lazy
List<Foo> something = { ->
List<Foo> tmp = []
def foo = new Foo()
foo.with {
type = ['String']
// ^^^^ should be Foo.type, not Bar.type
}
tmp.add(foo)
tmp
}()
}
def bar = new Bar()
assert bar.type == 10
assert bar.something*.type == [['String']]
assert bar.type == 10
'''
}
void testClosureSharedVariable1() {
assertScript '''
def x = '123';
{ -> x = new StringBuffer() }
x.charAt(0) // available in (CharSequence & ...)
'''
}
void testClosureSharedVariable2() {
shouldFailWithMessages '''
def x = '123';
{ -> x = 123 }
x.charAt(0) // not available in (Serializable & ...)
''',
'Cannot find matching method (java.io.Serializable & ','#charAt(int)'
}
// GROOVY-9516
void testClosureSharedVariable3() {
shouldFailWithMessages '''
class A {}
class B extends A { def m() {} }
class C extends A {}
void test() {
def x = new B();
{ -> x = new C() }();
def c = x
c.m()
}
''',
'Cannot find matching method A#m()'
}
// GROOVY-10356
void testClosureSharedVariable4() {
assertScript '''
interface A {
void m()
}
def a = (A) null
def x = { ->
a = null
}
a?.m()
'''
}
// GROOVY-10052
void testClosureSharedVariable5() {
assertScript '''
String x
def f = { ->
x = Optional.of('x').orElseThrow{ new Exception() }
}
assert f() == 'x'
assert x == 'x'
'''
}
// GROOVY-10052
void testClosureSharedVariable6() {
assertScript '''
def x
def f = { ->
x = Optional.of('x').orElseThrow{ new Exception() }
}
assert f() == 'x'
assert x == 'x'
'''
}
// GROOVY-10052
void testNotClosureSharedVariable() {
assertScript '''
String x = Optional.of('x').orElseThrow{ new Exception() }
def f = { ->
String y = Optional.of('y').orElseThrow{ new Exception() }
}
assert x == 'x'
assert f() == 'y'
'''
}
void testRecurseClosureCallAsMethod() {
assertScript '''
Closure<Integer> cl
cl = { int x -> x == 0 ? x : 1+cl(x-1) }
'''
}
void testFibClosureCallAsMethod() {
assertScript '''
Closure<Integer> fib
fib = { int x-> x<1?x:fib(x-1)+fib(x-2) }
fib(2)
'''
}
void testFibClosureCallAsMethodFromWithinClass() {
assertScript '''
class FibUtil {
private Closure<Integer> fibo
FibUtil() {
fibo = { int x-> x<1?x:fibo(x-1)+fibo(x-2) }
}
int fib(int n) { fibo(n) }
}
FibUtil fib = new FibUtil()
fib.fib(2)
'''
}
void testClosureRecursionWithoutClosureTypeArgument() {
shouldFailWithMessages '''
Closure fib
fib = { int n -> n<2?n:fib(n-1)+fib(n-2) }
''',
'Cannot find matching method java.lang.Object#plus(java.lang.Object)'
}
void testClosureRecursionWithDef() {
shouldFailWithMessages '''
def fib
fib = { int n -> n<2?n:fib(n-1)+fib(n-2) }
''',
'Cannot find matching method java.lang.Object#plus(java.lang.Object)',
'Cannot find matching method java.lang.Object#call(int)',
'Cannot find matching method java.lang.Object#call(int)'
}
void testClosureRecursionWithClosureTypeArgument() {
assertScript '''
Closure<Integer> fib
fib = { int n -> n<2?n:fib(n-1)+fib(n-2) }
'''
}
void testClosureMemoizeWithClosureTypeArgument() {
assertScript '''
Closure<Integer> fib
fib = { int n -> n<2?n:fib(n-1)+fib(n-2) }
def memoized = fib.memoizeAtMost(2)
assert fib(5) == memoized(5)
'''
}
// GROOVY-5639
void testShouldNotThrowClosureSharedVariableError() {
assertScript '''
Closure<Void> c = {
List<String> list = new ArrayList<String>()
String s = "foo"
10.times {
list.add(s)
}
}
'''
}
// from Grails
void testShouldNotThrowClosureSharedVariableError2() {
assertScript '''
class AntPathMatcher {
boolean match(String x, String y) { true }
}
private String relativePath() { '' }
def foo() {
AntPathMatcher pathMatcher = new AntPathMatcher()
def relPath = relativePath()
def cl = { String it ->
pathMatcher.match(it, relPath)
}
cl('foo')
}
foo()
'''
}
// GROOVY-5693
void testClosureArgumentCheckWithFlowTyping() {
assertScript '''
Closure a = { int i ->
println "First closure "+ i
}
Closure b = { String s ->
println "Second closure "+ s
}
a(5)
Closure c = a
a=b
a("Testing!")
a = c
a(5)
a=a
a(5)
a = b
a('Testing!')
'''
}
// GROOVY-10221
void testClosureArgumentCheckWithFlowTyping2() {
assertScript '''
class C<T1, T2 extends T1> {
void test() {
def one = { T2 x -> "" }
Closure<T2> two = { T2 x -> x }
one(two((T2) null))
}
}
new C<Number,Integer>().test()
'''
}
// GROOVY-5705
void testNPEWhenCallingClosureFromAField() {
assertScript '''
class Test {
Closure c = { it }
void test() {
c("123")
}
}
new Test().test()
'''
}
// GROOVY-6219, GROOVY-10277
void testClosureReturnDoesNotMatchTarget() {
shouldFailWithMessages '''
void printMessage(Closure<String> messageProvider) {
println "Received message : ${messageProvider()}"
}
void testMessage() {
printMessage { int x, int y -> x+y }
}
''',
'Cannot return value of type int for closure expecting java.lang.String'
}
void testSAMsInMethodSelection1() {
assertScript '''
interface MySAM {
def someMethod()
}
def foo(MySAM sam) {
sam.someMethod()
}
assert foo { -> 1 } == 1
assert foo(() -> 1) == 1
'''
}
// GROOVY-6189, GROOVY-9852
void testSAMsInMethodSelection2() {
// overloads with classes implemented by Closure
[
'groovy.lang.Closure' : 'not',
'groovy.lang.GroovyCallable' : 'not',
'groovy.lang.GroovyObject' : 'not',
'groovy.lang.GroovyObjectSupport': 'not',
'java.lang.Object' : 'sam',
'java.lang.Runnable' : 'not',
'java.lang.Cloneable' : 'not',
'java.io.Serializable' : 'not',
'java.util.concurrent.Callable' : 'not',
].each { type, which ->
assertScript """
interface MySAM {
def someMethod()
}
def foo($type ref) { 'not' }
def foo(MySAM sam) { sam.someMethod() }
assert foo { 'sam' } == '$which' : '$type'
assert foo(() -> 'sam') == '$which' : '$type'
"""
}
}
// GROOVY-9881
void testSAMsInMethodSelection3() {
// Closure implements both and Runnable is "closer"
assertScript '''
import java.util.concurrent.Callable
def foo(Callable c) { 'call' }
def foo(Runnable r) { 'run' }
def which = foo {
print 'bar'
}
assert which == 'run'
'''
}
void testSAMsInMethodSelection4() {
shouldFailWithMessages '''
interface One { void m() }
interface Two { void m() }
def foo(One one) { one.m() }
def foo(Two two) { two.m() }
foo {
print 'bar'
}
''',
'Reference to method is ambiguous. Cannot choose between'
}
void testSAMsInMethodSelection5() {
for (spec in ['','x,y ->']) {
shouldFailWithMessages """
import java.util.function.Function
import java.util.function.Supplier
def foo(Function f) { f.apply(0) }
def foo(Supplier s) { s.get() }
foo { $spec
'bar'
}
""",
'Reference to method is ambiguous. Cannot choose between'
}
}
void testSAMsInMethodSelection6() {
for (spec in ['->','x ->']) {
assertScript """
import java.util.function.Function
import java.util.function.Supplier
def foo(Function f) { f.apply(0) }
def foo(Supplier s) { s.get() }
def ret = foo { $spec
'bar'
}
assert ret == 'bar'
"""
}
}
// GROOVY-6238
void testDirectMethodCallOnClosureExpression() {
assertScript '''
@ASTTest(phase=INSTRUCTION_SELECTION,value={
def dit = node.getNodeMetaData(INFERRED_TYPE)
def irt = node.rightExpression.getNodeMetaData(INFERRED_TYPE)
assert irt == CLOSURE_TYPE
assert dit == CLOSURE_TYPE
})
def cl = { it }.curry(42)
def val = cl.call()
assert val == 42
'''
}
// GROOVY-6343
void testAccessStaticFieldFromNestedClosure() {
assertScript '''
class A {
public static final CONST = "a"
static List doSomething() {
return (0..1).collect { int x ->
(0..1).collect { int y ->
return CONST
}
}
}
}
def result = A.doSomething()
assert result == [['a','a'],['a','a']]
'''
}
// GROOVY-11360
void testLexicalScopeVersusGetDynamicProperty() {
config.warningLevel = org.codehaus.groovy.control.messages.WarningMessage.POSSIBLE_ERRORS
config.targetDirectory = File.createTempDir()
def parentDir = File.createTempDir()
try {
def c = new File(parentDir, 'C.groovy')
c.write '''
class C {
private static final value = "C"
def m(D d) {
d.with {
return value
}
}
}
'''
def d = new File(parentDir, 'D.groovy')
d.write '''
class D {
def get(String name) {
if (name == "value") "D"
}
}
'''
def e = new File(parentDir, 'E.groovy')
e.write '''
String result = new C().m(new D())
assert result == 'D'
'''
def loader = new GroovyClassLoader(this.class.classLoader)
new org.codehaus.groovy.control.CompilationUnit(config, null, loader).with {
addSources(c, d, e)
compile()
assert errorCollector.hasWarnings()
assert errorCollector.warnings[0].message.startsWith(
'The field: value of class: C is hidden by a dynamic property.')
}
loader.addClasspath(config.targetDirectory.absolutePath)
loader.loadClass('E', true).main()
} finally {
config.targetDirectory.deleteDir()
parentDir.deleteDir()
}
}
// GROOVY-9089
void testOwnerVersusDelegateFromNestedClosure() {
String declarations = '''
class A {
def p = 'outer delegate'
def m() { return this.p }
}
class B {
def p = 'inner delegate'
def m() { return this.p }
}
void outer(@DelegatesTo(value=A, strategy=Closure.DELEGATE_FIRST) Closure block) {
new A().with(block)
}
void inner(@DelegatesTo(value=B, strategy=Closure.DELEGATE_FIRST) Closure block) {
new B().with(block)
}
'''
assertScript declarations + '''
outer {
inner {
assert m() == 'inner delegate'
assert owner.m() == 'outer delegate'
assert delegate.m() == 'inner delegate'
}
}
'''
assertScript declarations + '''
outer {
inner {
assert p == 'inner delegate'
assert owner.p == 'outer delegate'
assert delegate.p == 'inner delegate'
}
}
'''
}
// GROOVY-9558
void testPutAtClosureDelegateProperty() {
assertScript '''
def config = new org.codehaus.groovy.control.CompilerConfiguration()
config.tap {
optimizationOptions['indy'] = true
optimizationOptions.indy = true
}
'''
}
// GROOVY-9652
void testDelegatePropertyAndCharCompareOptimization() {
['String', 'Character', 'char'].each { type ->
assertScript """
class Node {
String name
${type} text
}
class Root implements Iterable<Node> {
@Override
Iterator<Node> iterator() {
return [
new Node(name: 'term', text: (${type}) 'a'),
new Node(name: 'dash', text: (${type}) '-'),
new Node(name: 'term', text: (${type}) 'b')
].iterator()
}
}
void test() {
Root root = new Root()
root[0].with {
assert name == 'term'
assert text == 'a' // GroovyCastException: Cannot cast object 'script@b91d8c4' with class 'script' to class 'bugs.Node'
}
}
test()
"""
}
}
void testTypeCheckingOfClosureInMapConstructorCalls() {
shouldFailWithMessages '''
class Foo {
Number bar
Object baz
}
new Foo(bar: 123, baz: { ->
int i = Integer
})
''',
'Cannot assign value of type java.lang.Class<java.lang.Integer> to variable of type int'
shouldFailWithMessages '''
import groovy.transform.NamedParam
class Foo {
Foo(@NamedParam(value='bar',type=Number) Map<String,?> named, Object positional) {
}
}
new Foo(bar: Number, { ->
int i = Integer
})
''',
'Cannot assign value of type java.lang.Class<java.lang.Integer> to variable of type int',
"named param 'bar' has type 'java.lang.Class<java.lang.Number>' but expected 'java.lang.Number'"
}
// GROOVY-10602
void testMethodAndClosureParametersDefaultArguments() {
assertScript '''import java.util.function.*
String test(Closure one = { p ->
Closure two = { Supplier s = { -> p } ->
s.get()
}
two()
}) {
one('foo')
}
String result = test()
assert result == 'foo'
'''
}
}