blob: 6a89299929c522c5c3a887a013e9332f5dd5dd01 [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 org.codehaus.groovy.antlr.AntlrParserPluginFactory
/**
* Units tests aimed at testing the behavior of {@link DelegatesTo} in combination
* with static type checking.
*/
class DelegatesToSTCTest extends StaticTypeCheckingTestCase {
void testShouldChooseMethodFromOwner() {
assertScript '''
class Delegate {
int foo() { 2 }
}
class Owner {
int foo() { 1 }
int doIt(@DelegatesTo(Delegate) Closure cl) {
cl.delegate = new Delegate()
cl() as int
}
int test() {
doIt {
@ASTTest(phase=INSTRUCTION_SELECTION, value={
node = node.rightExpression
def target = node.getNodeMetaData(DIRECT_METHOD_CALL_TARGET)
assert target != null
assert target.declaringClass.name == 'Owner'
})
def x = foo() // as the delegation strategy is owner first, should return 1
x
}
}
}
def o = new Owner()
assert o.test() == 1
'''
}
void testShouldChooseMethodFromDelegate() {
assertScript '''
class Delegate {
int foo() { 2 }
}
class Owner {
int foo() { 1 }
int doIt(@DelegatesTo(strategy=Closure.DELEGATE_FIRST, value=Delegate) Closure cl) {
cl.delegate = new Delegate()
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl() as int
}
int test() {
doIt {
@ASTTest(phase=INSTRUCTION_SELECTION, value={
node = node.rightExpression
def target = node.getNodeMetaData(DIRECT_METHOD_CALL_TARGET)
assert target != null
assert target.declaringClass.name == 'Delegate'
})
def x = foo() // as the delegation strategy is delegate first, should return 2
x
}
}
}
def o = new Owner()
assert o.test() == 2
'''
}
void testShouldAcceptMethodCall() {
assertScript '''
class ExecSpec {
boolean called = false
void foo() {
called = true
}
}
ExecSpec spec = new ExecSpec()
void exec(ExecSpec spec, @DelegatesTo(value=ExecSpec, strategy=Closure.DELEGATE_FIRST) Closure param) {
param.delegate = spec
param()
}
exec(spec) {
foo() // should be recognized because param is annotated with @DelegatesTo(ExecSpec)
}
assert spec.isCalled()
'''
}
void testCallMethodFromOwner() {
assertScript '''
class Xml {
boolean called = false
void bar() { called = true }
void foo(@DelegatesTo(Xml)Closure cl) { cl.delegate=this;cl() }
}
def mylist = [1]
def xml = new Xml()
xml.foo {
mylist.each { bar() }
}
assert xml.called
'''
}
void testEmbeddedWithAndShadowing() {
assertScript '''
class A {
int foo() { 1 }
}
class B {
int foo() { 2 }
}
def a = new A()
def b = new B()
int result
a.with {
b.with {
result = foo()
}
}
assert result == 2
'''
}
void testEmbeddedWithAndShadowing2() {
assertScript '''
class A {
int foo() { 1 }
}
class B {
int foo() { 2 }
}
def a = new A()
def b = new B()
int result
b.with {
a.with {
result = foo()
}
}
assert result == 1
'''
}
void testEmbeddedWithAndShadowing3() {
assertScript '''
class A {
int foo() { 1 }
}
class B {
int foo() { 2 }
}
class C {
int foo() { 3 }
void test() {
def a = new A()
def b = new B()
int result
b.with {
a.with {
result = foo()
}
}
assert result == 1
}
}
new C().test()
'''
}
void testEmbeddedWithAndShadowing4() {
assertScript '''
class A {
int foo() { 1 }
}
class B {
int foo() { 2 }
}
class C {
int foo() { 3 }
void test() {
def a = new A()
def b = new B()
int result
b.with {
a.with {
result = this.foo()
}
}
assert result == 3
}
}
new C().test()
'''
}
void testShouldDelegateToParameter() {
assertScript '''
class Foo {
boolean called = false
def foo() { called = true }
}
def with(@DelegatesTo.Target Object target, @DelegatesTo Closure arg) {
arg.delegate = target
arg()
}
def test() {
def obj = new Foo()
with(obj) { foo() }
assert obj.called
}
test()
'''
}
void testShouldDelegateToParameterUsingExplicitId() {
assertScript '''
class Foo {
boolean called = false
def foo() { called = true }
}
def with(@DelegatesTo.Target('target') Object target, @DelegatesTo(target='target') Closure arg) {
arg.delegate = target
arg()
}
def test() {
def obj = new Foo()
with(obj) { foo() }
assert obj.called
}
test()
'''
}
void testShouldFailDelegateToParameterUsingWrongId() {
shouldFailWithMessages '''
class Foo {
boolean called = false
def foo() { called = true }
}
def with(@DelegatesTo.Target('target') Object target, @DelegatesTo(target='wrongTarget') Closure arg) {
arg.delegate = target
arg()
}
def test() {
def obj = new Foo()
with(obj) { foo() }
assert obj.called
}
test()
''', 'Not enough arguments found for a @DelegatesTo method call', 'Cannot find matching method'
}
void testShouldFailDelegateToParameterIfNoTargetSpecified() {
shouldFailWithMessages '''
class Foo {
boolean called = false
def foo() { called = true }
}
def with(Object target, @DelegatesTo Closure arg) {
arg.delegate = target
arg()
}
def test() {
def obj = new Foo()
with(obj) { foo() }
assert obj.called
}
test()
''', 'Not enough arguments found for a @DelegatesTo method call', 'Cannot find matching method'
}
void testDelegatesToWithSetter() {
assertScript '''
class Item {
int x
}
class Builder {
private Item item = new Item()
void setConstraints(@DelegatesTo(Item) Closure cl) {
def copy = cl.rehydrate(item, this, this)
copy()
}
int value() { item.x }
}
def b = new Builder()
b.setConstraints {
x = 5
}
assert b.value() == 5
'''
}
void testDelegatesToWithSetterUsedAsProperty() {
assertScript '''
class Item {
int x
}
class Builder {
private Item item = new Item()
void setConstraints(@DelegatesTo(Item) Closure cl) {
def copy = cl.rehydrate(item, this, this)
copy()
}
int value() { item.x }
}
def b = new Builder()
b.constraints = {
x = 5
}
assert b.value() == 5
'''
}
void testDelegatesToWithSetterUsedAsPropertyAndErrorInPropertyName() {
shouldFailWithMessages '''
class Item {
int x
}
class Builder {
private Item item = new Item()
void setConstraints(@DelegatesTo(Item) Closure cl) {
def copy = cl.rehydrate(item, this, this)
copy()
}
int value() { item.x }
}
def b = new Builder()
b.constraints = {
y = 5
}
assert b.value() == 5
''', '[Static type checking] - The variable [y] is undeclared.'
}
void testDelegatesToWithSetterUsedAsPropertyAndWith() {
assertScript '''
class Item {
int x
}
class Builder {
private Item item = new Item()
void setConstraints(@DelegatesTo(Item) Closure cl) {
def copy = cl.rehydrate(item, this, this)
copy()
}
int value() { item.x }
}
def b = new Builder()
b.with {
constraints = {
x = 5
}
}
assert b.value() == 5
'''
}
// GROOVY-6055
void testDelegatesToInStaticContext() {
assertScript '''
class Person {
String name
public static List<Person> findBy(@DelegatesTo(Person) Closure criteria) { [] }
}
List<Person> persons = Person.findBy { getName() == 'Cedric' }
persons = Person.findBy { name == 'Cedric' }
'''
}
void testDelegatesToInStaticContext2() {
assertScript '''
class QueryBuilder {
List<?> where(Map params) {[]}
}
class Person {
String name
public static List<Person> findBy(@DelegatesTo(QueryBuilder) Closure criteria) { [] }
}
List<Person> persons = Person.findBy { where name:'Cédric' }
'''
}
// GROOVY-6021
void testShouldEnsureLastIsRecognizedAndCompiledProperly() {
assertScript '''
def with(@DelegatesTo.Target Object target, @DelegatesTo(strategy = Closure.DELEGATE_FIRST) Closure arg) {
arg.delegate = target
arg.setResolveStrategy(Closure.DELEGATE_FIRST)
arg()
}
def test() {
def obj = [1, 2]
with(obj) {
print(last()) //error is here
}
}
test()
'''
}
// GROOVY-6091
void testExplicitUseOfDelegateProperty() {
assertScript '''
def with(@DelegatesTo.Target Object target, @DelegatesTo(strategy = Closure.DELEGATE_FIRST) Closure arg) {
arg.delegate = target
arg.setResolveStrategy(Closure.DELEGATE_FIRST)
arg()
}
def test() {
def obj = [1, 2]
with(obj) {
print(delegate.last()) //error is here
}
}
test()
'''
}
// GROOVY-6091
void testExplicitUseOfDelegateMethod() {
assertScript '''
def with(@DelegatesTo.Target Object target, @DelegatesTo(strategy = Closure.DELEGATE_FIRST) Closure arg) {
arg.delegate = target
arg.setResolveStrategy(Closure.DELEGATE_FIRST)
arg()
}
def test() {
def obj = [1, 2]
with(obj) {
print(getDelegate().last()) //error is here
}
}
test()
'''
}
void testDelegatesToGenericTypeArgument() {
assertScript '''
public <T> Object map(@DelegatesTo.Target List<T> target, @DelegatesTo(genericTypeIndex=0) Closure arg) {
arg.delegate = target.join('')
arg()
}
def test() {
def result
map(['f','o','o']) {
result = toUpperCase()
}
result
}
assert 'FOO'==test()
'''
}
void testDelegatesToGenericTypeArgumentAndActualArgumentNotUsingGenerics() {
assertScript '''import groovy.transform.InheritConstructors
@InheritConstructors
class MyList extends LinkedList<String> {}
public <T> Object map(@DelegatesTo.Target List<T> target, @DelegatesTo(genericTypeIndex=0) Closure arg) {
arg.delegate = target.join('')
arg()
}
def test() {
def result
def mylist = new MyList(['f','o','o'])
map(mylist) {
result = toUpperCase()
}
result
}
assert 'FOO'==test()
'''
}
void testDelegatesToGenericTypeArgumentWithMissingGenerics() {
shouldFailWithMessages '''
public Object map(@DelegatesTo.Target List target, @DelegatesTo(genericTypeIndex=0) Closure arg) {
arg.delegate = target.join('')
arg()
}
def test() {
def result
map(['f','o','o']) {
result = toUpperCase()
}
result
}
assert 'FOO'==test()
''', 'Cannot use @DelegatesTo(genericTypeIndex=0) with a type that doesn\'t use generics', 'Cannot find matching method'
}
void testDelegatesToGenericTypeArgumentOutOfBounds() {
shouldFailWithMessages '''
public <T> Object map(@DelegatesTo.Target List<T> target, @DelegatesTo(genericTypeIndex=1) Closure arg) {
arg.delegate = target.join('')
arg()
}
def test() {
def result
map(['f','o','o']) {
result = toUpperCase()
}
result
}
assert 'FOO'==test()
''', 'Index of generic type @DelegatesTo(genericTypeIndex=1) greater than those of the selected type', 'Cannot find matching method'
}
void testDelegatesToGenericTypeArgumentWithNegativeIndex() {
shouldFailWithMessages '''
public <T> Object map(@DelegatesTo.Target List<T> target, @DelegatesTo(genericTypeIndex=-1) Closure arg) {
arg.delegate = target.join('')
arg()
}
def test() {
def result
map(['f','o','o']) {
result = toUpperCase()
}
result
}
assert 'FOO'==test()
''', 'Index of generic type @DelegatesTo(genericTypeIndex=-1) lower than those of the selected type', 'Cannot find matching method'
}
void testDelegatesToGenericTypeArgumentUsingMap() {
assertScript '''
public <K,V> void transform(@DelegatesTo.Target Map<K, V> map, @DelegatesTo(genericTypeIndex = 1) Closure<?> closure) {
map.keySet().each {
closure.delegate = map[it]
map[it] = closure()
}
}
def map = [1: 'a', 2: 'b', 3: 'c']
transform(map) {
toUpperCase()
}
assert map == [1: 'A', 2: 'B', 3: 'C']
'''
}
void testDelegatesToGenericTypeArgumentUsingMapAndWrongIndex() {
shouldFailWithMessages '''
public <K,V> void transform(@DelegatesTo.Target Map<K, V> map, @DelegatesTo(genericTypeIndex = 0) Closure<?> closure) {
map.keySet().each {
closure.delegate = map[it]
map[it] = closure()
}
}
def map = [1: 'a', 2: 'b', 3: 'c']
transform(map) {
toUpperCase()
}
assert map == [1: 'A', 2: 'B', 3: 'C']
''', 'Cannot find matching method'
}
// GROOVY-6165
void testDelegatesToGenericArgumentTypeAndTypo() {
String code = '''import groovy.transform.*
@TupleConstructor
class Person { String name }
public <T> List<T> names(
@DelegatesTo.Target List<T> list,
@DelegatesTo(genericTypeIndex = 0) Closure modify) {
list.collect {
modify.delegate = it
modify()
}
}
def test(List<Person> persons) {
def names = names(persons) {
getname().toUpperCase()
}
assert names == ['GUILLAUME', 'CEDRIC']
}
test([new Person('Guillaume'), new Person('Cedric')])
'''
String msg = 'Cannot find matching method'
if (config.pluginFactory instanceof AntlrParserPluginFactory) {
shouldFailWithMessages code, msg
} else {
/*
* Because the Parrot parser provides more accurate node position information,
* org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor.addError will not be interfered by wrong node position.
*
* 1) TestScripttestDelegatesToGenericArgumentTypeAndTypo0.groovy: 17: [Static type checking] - Cannot find matching method TestScripttestDelegatesToGenericArgumentTypeAndTypo0#getname(). Please check if the declared type is correct and if the method exists.
* 2) TestScripttestDelegatesToGenericArgumentTypeAndTypo0.groovy: 17: [Static type checking] - Cannot find matching method java.lang.Object#toUpperCase(). Please check if the declared type is correct and if the method exists.
*
*/
shouldFailWithMessages code, msg, msg
}
}
// GROOVY-6323, GROOVY-6325, GROOVY-6332
void testStaticContextAndProperty() {
assertScript '''
class MyCar {
String brand
String model
}
class MyCarMain {
MyCar configureCar(@DelegatesTo(MyCar) Closure closure) {
def car = new MyCar()
closure.delegate = car
closure.resolveStrategy = Closure.DELEGATE_FIRST
closure()
car
}
static void main(String[] args) {
def main = new MyCarMain()
def car = main.configureCar {
brand = "BMW"
model = brand + " X5"
}
assert car.model == "BMW X5"
}
}
MyCarMain.main()
'''
assertScript '''
class MyCar {
private String _brand
private String _model
String getBrand() {
return _brand
}
void setBrand(String brand) {
_brand = brand
}
String getModel() {
return _model
}
void setModel(String model) {
_model = model
}
}
class MyCarMain {
MyCar configureCar(@DelegatesTo(value = MyCar, strategy = Closure.DELEGATE_FIRST) Closure closure) {
def car = new MyCar()
closure.delegate = car
closure.resolveStrategy = Closure.DELEGATE_FIRST
closure()
car
}
static void main(String[] args) {
def main = new MyCarMain()
def car = main.configureCar {
brand = "BMW"
model = brand
}
assert car.model == "BMW"
}
}
MyCarMain.main()
'''
assertScript '''
class Car {
private String _brand
String getBrand() { _brand }
void setBrand(String brand) { _brand = brand }
}
class Builder {
def <T> T configure(@DelegatesTo.Target Class<T> target, @DelegatesTo(genericTypeIndex=0) Closure cl) {
def obj = target.newInstance()
cl.delegate = obj
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl.call()
obj
}
}
class Main {
void run() {
def builder = new Builder()
def car = builder.configure(Car) {
brand = brand
}
}
}
new Main().run()
'''
}
// GROOVY-5998
void testSubscriptOperatorOnPropertiesWithBuilder() {
assertScript '''
import static groovy.lang.Closure.*
class DatasourceBuilder {
Map<String,String> attrs = [:]
}
void datasource(@DelegatesTo(strategy = DELEGATE_FIRST, value = DatasourceBuilder) Closure c) {}
void foo() {
datasource {
attrs['some'] = 'foo'
}
}
foo()
'''
}
void testDelegatesToNestedGenericType() {
assertScript '''
trait Configurable<ConfigObject> {
ConfigObject configObject
void configure(Closure<Void> configSpec) {
configSpec.resolveStrategy = Closure.DELEGATE_FIRST
configSpec.delegate = configObject
configSpec()
}
}
public <T,U extends Configurable<T>> U configure(Class<U> clazz, @DelegatesTo(type="T") Closure configSpec) {
Configurable<T> obj = (Configurable<T>) clazz.newInstance()
obj.configure(configSpec)
obj
}
class Module implements Configurable<ModuleConfig> {
String value
Module(){
configObject = new ModuleConfig()
}
@Override
void configure(Closure<Void> configSpec) {
Configurable.super.configure(configSpec)
value = "${configObject.name}-${configObject.version}"
}
}
class ModuleConfig {
String name
String version
}
def module = configure(Module) {
name = 'test'
version = '1.0'
}
assert module.value == 'test-1.0'
'''
}
void testDelegatesToWithType2() {
assertScript '''
public <T> boolean evalAsSet(List<T> list, @DelegatesTo(type="Set<T>") Closure<Boolean> cl) {
cl.delegate = list as Set
cl()
}
assert evalAsSet([1,1,2,3]) {
size() == 3
}
'''
}
}