[go: nahoru, domu]

blob: 4d9dceadd5c8454335b092fa7b48fd005a0d47f7 [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed 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 androidx.room.compiler.processing
import androidx.kruth.assertThat
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.asMutableClassName
import androidx.room.compiler.processing.util.CONTINUATION_JCLASS_NAME
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.UNIT_JCLASS_NAME
import androidx.room.compiler.processing.util.getMethodByJvmName
import androidx.room.compiler.processing.util.runKspTest
import androidx.room.compiler.processing.util.runProcessorTest
import androidx.room.compiler.processing.util.typeName
import com.google.common.truth.Truth
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeVariableName
import com.squareup.javapoet.WildcardTypeName
import org.junit.Test
class XExecutableTypeTest {
@Test
fun constructorInheritanceResolution() {
runProcessorTest(
sources = listOf(
Source.kotlin(
"KotlinClass.kt",
"""
abstract class KotlinClass<T> constructor(t: T) {
abstract fun foo(): KotlinClass<String>
}
""".trimIndent()
),
Source.java(
"JavaClass",
"""
abstract class JavaClass<T> {
JavaClass(T t) {}
abstract JavaClass<String> foo();
}
""".trimIndent()
)
)
) { invocation ->
fun checkConstructor(className: String) {
val typeElement = invocation.processingEnv.requireTypeElement(className)
val constructorElement = typeElement.getConstructors().single()
val constructorType = constructorElement.executableType
assertThat(constructorType.parameterTypes.single().typeName)
.isEqualTo(TypeVariableName.get("T"))
val resolvedType = typeElement.getDeclaredMethods().single().returnType
val resolvedConstructorType = constructorElement.asMemberOf(resolvedType)
// Assert that the XConstructorType parameter is resolved type, String
assertThat(resolvedConstructorType.parameterTypes.single().typeName)
.isEqualTo(ClassName.get("java.lang", "String"))
}
checkConstructor("JavaClass")
checkConstructor("KotlinClass")
}
}
@Test
fun inheritanceResolution() {
val src = Source.kotlin(
"Foo.kt",
"""
interface MyInterface<T> {
fun getT(): T
fun setT(t:T): Unit
suspend fun suspendGetT(): T
suspend fun suspendSetT(t:T): Unit
}
abstract class Subject : MyInterface<String>
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
// helper method to get executable types both from sub class and also as direct child of
// the given type
fun checkMethods(
methodName: String,
vararg subjects: XTypeElement,
callback: (XMethodType) -> Unit
) {
Truth.assertThat(subjects).isNotEmpty() // Kruth doesn't support arrays yet
subjects.forEach {
callback(myInterface.getMethodByJvmName(methodName).asMemberOf(it.type))
callback(it.getMethodByJvmName(methodName).asMemberOf(it.type))
}
}
val subject = invocation.processingEnv.requireTypeElement("Subject")
checkMethods("getT", subject) { type ->
assertThat(type.parameterTypes).isEmpty()
assertThat(type.returnType.typeName).isEqualTo(String::class.typeName())
}
checkMethods("setT", subject) { type ->
assertThat(type.parameterTypes).containsExactly(
invocation.processingEnv.requireType(String::class)
)
assertThat(type.returnType.typeName).isEqualTo(TypeName.VOID)
}
checkMethods("suspendGetT", subject) { type ->
assertThat(type.parameterTypes.first().typeName).isEqualTo(
ParameterizedTypeName.get(
CONTINUATION_JCLASS_NAME,
WildcardTypeName.supertypeOf(String::class.java)
)
)
assertThat(type.returnType.typeName).isEqualTo(TypeName.OBJECT)
}
checkMethods("suspendSetT", subject) { type ->
assertThat(type.parameterTypes.first().typeName).isEqualTo(
String::class.typeName()
)
assertThat(type.parameterTypes[1].typeName).isEqualTo(
ParameterizedTypeName.get(
CONTINUATION_JCLASS_NAME,
WildcardTypeName.supertypeOf(UNIT_JCLASS_NAME)
)
)
assertThat(type.returnType.typeName).isEqualTo(TypeName.OBJECT)
}
}
}
@Test
fun isSameMethodTypeTest() {
val src = Source.kotlin(
"MyInterface.kt",
"""
interface MyInterface {
fun method(foo: Foo): Bar
fun methodWithDifferentName(foo: Foo): Bar
@kotlin.jvm.Throws(RuntimeException::class)
fun methodWithDifferentThrows(foo: Foo): Bar
fun methodWithDifferentReturn(foo: Foo): Unit
fun methodWithDifferentParameters(): Bar
fun methodWithDefault(foo: Foo): Bar = TODO()
}
class MyClass {
fun classMethod(foo: Foo): Bar = TODO()
companion object {
fun companionMethod(foo: Foo): Bar = TODO()
}
}
object MyObject {
fun objectMethod(foo: Foo): Bar = TODO()
@JvmStatic fun staticMethod(foo: Foo): Bar = TODO()
}
class Foo
class Bar
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
val method = myInterface.getMethodByJvmName("method")
val methodWithDifferentName =
myInterface.getMethodByJvmName("methodWithDifferentName")
val methodWithDifferentThrows =
myInterface.getMethodByJvmName("methodWithDifferentThrows")
val methodWithDifferentReturn =
myInterface.getMethodByJvmName("methodWithDifferentReturn")
val methodWithDifferentParameters =
myInterface.getMethodByJvmName("methodWithDifferentParameters")
val methodWithDefault = myInterface.getMethodByJvmName("methodWithDefault")
val myObject = invocation.processingEnv.requireTypeElement("MyObject")
val objectMethod = myObject.getMethodByJvmName("objectMethod")
val staticMethod = myObject.getMethodByJvmName("staticMethod")
val myClass = invocation.processingEnv.requireTypeElement("MyClass")
val classMethod = myClass.getMethodByJvmName("classMethod")
val companionObject = myClass.getEnclosedElements()
.mapNotNull { it as? XTypeElement }
.filter { it.isCompanionObject() }
.single()
val companionMethod = companionObject.getMethodByJvmName("companionMethod")
assertIsSameType(method, methodWithDifferentName)
assertIsSameType(method, methodWithDifferentThrows)
assertIsSameType(method, methodWithDefault)
assertIsSameType(method, objectMethod)
assertIsSameType(method, staticMethod)
assertIsSameType(method, objectMethod)
assertIsSameType(method, classMethod)
assertIsSameType(method, companionMethod)
// Assert that different return type or parameters result in different method types.
assertIsNotSameType(method, methodWithDifferentReturn)
assertIsNotSameType(method, methodWithDifferentParameters)
}
}
@Test
fun isSameGenericMethodTypeTest() {
val src = Source.kotlin(
"MyInterface.kt",
"""
interface FooBar : MyInterface<Foo, Bar>
interface BarFoo : MyInterface<Bar, Foo>
interface MyInterface<T1, T2> {
fun methodFooBar(foo: Foo): Bar
fun methodBarFoo(bar: Bar): Foo
fun method(t1: T1): T2
}
class Foo
class Bar
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
val fooBar = invocation.processingEnv.requireTypeElement("FooBar")
val barFoo = invocation.processingEnv.requireTypeElement("BarFoo")
val myInterfaceMethod = myInterface.getMethodByJvmName("method")
val myInterfaceMethodFooBar = myInterface.getMethodByJvmName("methodFooBar")
val myInterfaceMethodBarFoo = myInterface.getMethodByJvmName("methodBarFoo")
assertIsNotSameType(myInterfaceMethodFooBar, myInterfaceMethod)
assertIsSameType(
myInterfaceMethodFooBar.executableType,
myInterfaceMethod.asMemberOf(fooBar.type)
)
assertIsNotSameType(myInterfaceMethodBarFoo, myInterfaceMethod)
assertIsSameType(
myInterfaceMethodBarFoo.executableType,
myInterfaceMethod.asMemberOf(barFoo.type)
)
}
}
@Test
fun isSameConstructorTypeTest() {
val src = Source.kotlin(
"MyInterface.kt",
"""
abstract class ClassFoo constructor(foo: Foo)
abstract class ClassBar constructor(bar: Bar)
abstract class OtherClassFoo constructor(foo: Foo)
abstract class SubClassBar constructor(bar: Bar) : ClassBar(bar)
class Foo
class Bar
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
val classFoo = invocation.processingEnv.requireTypeElement("ClassFoo")
val classBar = invocation.processingEnv.requireTypeElement("ClassBar")
val otherClassFoo = invocation.processingEnv.requireTypeElement("OtherClassFoo")
val subClassBar = invocation.processingEnv.requireTypeElement("SubClassBar")
assertThat(classFoo.getConstructors()).hasSize(1)
assertThat(classBar.getConstructors()).hasSize(1)
assertThat(otherClassFoo.getConstructors()).hasSize(1)
assertThat(subClassBar.getConstructors()).hasSize(1)
assertIsSameType(
classFoo.getConstructors().single(),
otherClassFoo.getConstructors().single()
)
assertIsSameType(
classBar.getConstructors().single(),
subClassBar.getConstructors().single()
)
assertIsNotSameType(
classFoo.getConstructors().single(),
classBar.getConstructors().single()
)
}
}
@Test
fun isSameConstructorTypeAndMethodTypeTest() {
val src = Source.kotlin(
"ClassFoo.kt",
"""
abstract class ClassFoo constructor(foo: Foo) {
abstract fun method(otherFoo: Foo)
}
class Foo
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
val classFoo = invocation.processingEnv.requireTypeElement("ClassFoo")
assertThat(classFoo.getConstructors()).hasSize(1)
// Interestingly, isSameType doesn't care if the type is a constructor or method as long
// as the parameter types and return type are the same (Note: constructors have return
// types of "void" in Javac).
assertIsSameType(
classFoo.getConstructors().single(),
classFoo.getMethodByJvmName("method")
)
}
}
@Test
fun isSameGenericConstructorTypeTest() {
val src = Source.kotlin(
"MyInterface.kt",
"""
abstract class GenericClass<T> constructor(t: T)
abstract class GenericClassFoo constructor(foo: Foo) : GenericClass<Foo>(foo)
abstract class ClassFoo constructor(foo: Foo)
class Foo
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
val genericClass = invocation.processingEnv.requireTypeElement("GenericClass")
val genericClassFoo = invocation.processingEnv.requireTypeElement("GenericClassFoo")
val classFoo = invocation.processingEnv.requireTypeElement("ClassFoo")
assertThat(genericClass.getConstructors()).hasSize(1)
assertThat(genericClassFoo.getConstructors()).hasSize(1)
assertThat(classFoo.getConstructors()).hasSize(1)
assertIsNotSameType(
genericClass.getConstructors().single(),
genericClassFoo.getConstructors().single()
)
assertIsNotSameType(
genericClass.getConstructors().single(),
classFoo.getConstructors().single()
)
assertIsSameType(
genericClassFoo.getConstructors().single(),
classFoo.getConstructors().single()
)
}
}
@Test
fun isSamePropertyMethodTypeTest() {
val src = Source.kotlin(
"MyInterface.kt",
"""
class MyClass {
var fooField: Foo = TODO()
var fooFieldWithDifferentName: Foo = TODO()
val fooFieldWithVal: Foo = TODO()
var barField: Bar = TODO()
fun fooMethodGetter(): Foo = TODO()
fun fooMethodSetter(foo: Foo) {}
}
class Foo
class Bar
""".trimIndent()
)
runProcessorTest(
sources = listOf(src)
) { invocation ->
val myClass = invocation.processingEnv.requireTypeElement("MyClass")
val fooFieldGetter = myClass.getMethodByJvmName("getFooField")
val fooFieldSetter = myClass.getMethodByJvmName("setFooField")
val fooFieldWithDifferentNameGetter =
myClass.getMethodByJvmName("getFooFieldWithDifferentName")
val fooFieldWithDifferentNameSetter =
myClass.getMethodByJvmName("setFooFieldWithDifferentName")
val fooFieldWithValGetter = myClass.getMethodByJvmName("getFooFieldWithVal")
val barFieldGetter = myClass.getMethodByJvmName("getBarField")
val barFieldSetter = myClass.getMethodByJvmName("setBarField")
val fooMethodGetter = myClass.getMethodByJvmName("fooMethodGetter")
val fooMethodSetter = myClass.getMethodByJvmName("fooMethodSetter")
assertIsSameType(fooFieldGetter, fooFieldWithDifferentNameGetter)
assertIsSameType(fooFieldGetter, fooFieldWithValGetter)
assertIsSameType(fooMethodGetter, fooFieldGetter)
assertIsSameType(fooFieldSetter, fooFieldWithDifferentNameSetter)
assertIsSameType(fooFieldSetter, fooMethodSetter)
assertIsNotSameType(fooFieldGetter, barFieldGetter)
assertIsNotSameType(fooFieldSetter, barFieldSetter)
}
}
private fun assertIsSameType(element1: XExecutableElement, element2: XExecutableElement) {
assertIsSameType(element1.executableType, element2.executableType)
}
private fun assertIsSameType(type1: XExecutableType, type2: XExecutableType) {
// Assert both directions to ensure isSameType is symmetric
assertThat(type1.isSameType(type2)).isTrue()
assertThat(type2.isSameType(type1)).isTrue()
}
private fun assertIsNotSameType(element1: XExecutableElement, element2: XExecutableElement) {
assertIsNotSameType(element1.executableType, element2.executableType)
}
private fun assertIsNotSameType(type1: XExecutableType, type2: XExecutableType) {
// Assert both directions to ensure isSameType is symmetric
assertThat(type1.isSameType(type2)).isFalse()
assertThat(type2.isSameType(type1)).isFalse()
}
@Test
fun kotlinPropertyInheritance() {
val src = Source.kotlin(
"Foo.kt",
"""
interface MyInterface<T> {
val immutableT: T
var mutableT: T?
val list: List<T>
val nullableList: List<T?>
}
abstract class Subject : MyInterface<String>
abstract class NullableSubject: MyInterface<String?>
""".trimIndent()
)
runProcessorTest(sources = listOf(src)) { invocation ->
val myInterface = invocation.processingEnv.requireTypeElement("MyInterface")
// helper method to get executable types both from sub class and also as direct child of
// the given type
fun checkMethods(
methodName: String,
vararg subjects: XTypeElement,
callback: (XMethodType) -> Unit
) {
Truth.assertThat(subjects).isNotEmpty() // Kruth doesn't support arrays yet
subjects.forEach {
callback(myInterface.getMethodByJvmName(methodName).asMemberOf(it.type))
callback(it.getMethodByJvmName(methodName).asMemberOf(it.type))
}
}
val subject = invocation.processingEnv.requireTypeElement("Subject")
checkMethods("getImmutableT", subject) { method ->
assertThat(method.returnType.typeName).isEqualTo(String::class.typeName())
if (invocation.isKsp) {
// we don't get proper nullable here for kapt
// partially related to b/169629272
assertThat(method.returnType.nullability).isEqualTo(XNullability.NONNULL)
}
assertThat(method.parameterTypes).isEmpty()
assertThat(method.typeVariables).isEmpty()
}
checkMethods("getMutableT", subject) { method ->
assertThat(method.returnType.typeName).isEqualTo(String::class.typeName())
if (invocation.isKsp) {
// we don't get proper nullable here for kapt
// partially related to b/169629272
assertThat(method.returnType.nullability).isEqualTo(XNullability.NULLABLE)
}
assertThat(method.parameterTypes).isEmpty()
assertThat(method.typeVariables).isEmpty()
}
checkMethods("setMutableT", subject) { method ->
assertThat(method.returnType.typeName).isEqualTo(TypeName.VOID)
assertThat(method.parameterTypes.first().nullability)
.isEqualTo(XNullability.NULLABLE)
assertThat(method.parameterTypes.first().typeName)
.isEqualTo(String::class.typeName())
assertThat(method.typeVariables).isEmpty()
}
checkMethods("getList", subject) { method ->
assertThat(method.returnType.typeName).isEqualTo(
ParameterizedTypeName.get(
List::class.java,
String::class.java
)
)
assertThat(method.returnType.nullability).isEqualTo(
XNullability.NONNULL
)
assertThat(method.returnType.typeArguments.first().extendsBound()).isNull()
if (invocation.isKsp) {
// kapt cannot read type parameter nullability yet
assertThat(
method.returnType.typeArguments.first().nullability
).isEqualTo(
XNullability.NONNULL
)
}
}
val nullableSubject = invocation.processingEnv.requireTypeElement("NullableSubject")
// check that nullability is inferred from type parameters as well
checkMethods("getImmutableT", nullableSubject) { method ->
assertThat(method.returnType.typeName).isEqualTo(String::class.typeName())
if (invocation.isKsp) {
// we don't get proper nullable here for kapt
// partially related to b/169629272
assertThat(method.returnType.nullability).isEqualTo(XNullability.NULLABLE)
}
assertThat(method.parameterTypes).isEmpty()
assertThat(method.typeVariables).isEmpty()
}
checkMethods("getMutableT", nullableSubject) { method ->
assertThat(method.returnType.typeName).isEqualTo(String::class.typeName())
if (invocation.isKsp) {
// we don't get proper nullable here for kapt
// partially related to b/169629272
assertThat(method.returnType.nullability).isEqualTo(XNullability.NULLABLE)
}
assertThat(method.parameterTypes).isEmpty()
assertThat(method.typeVariables).isEmpty()
}
checkMethods("setMutableT", nullableSubject) { method ->
assertThat(method.returnType.typeName).isEqualTo(TypeName.VOID)
assertThat(method.parameterTypes.first().nullability)
.isEqualTo(XNullability.NULLABLE)
assertThat(method.parameterTypes.first().typeName)
.isEqualTo(String::class.typeName())
assertThat(method.typeVariables).isEmpty()
}
checkMethods("getList", nullableSubject) { method ->
assertThat(method.returnType.typeName).isEqualTo(
ParameterizedTypeName.get(
List::class.java,
String::class.java
)
)
assertThat(method.returnType.nullability).isEqualTo(
XNullability.NONNULL
)
if (invocation.isKsp) {
assertThat(
method.returnType.typeArguments.first().nullability
).isEqualTo(
XNullability.NULLABLE
)
}
}
checkMethods("getNullableList", subject, nullableSubject) { method ->
assertThat(method.returnType.typeName).isEqualTo(
ParameterizedTypeName.get(
List::class.java,
String::class.java
)
)
assertThat(method.returnType.nullability).isEqualTo(
XNullability.NONNULL
)
if (invocation.isKsp) {
assertThat(
method.returnType.typeArguments.first().nullability
).isEqualTo(
XNullability.NULLABLE
)
}
}
}
}
@Test
fun typeVariableTest() {
val kotlinSrc = Source.kotlin(
"KotlinSubject.kt",
"""
class KotlinSubject {
fun <T> oneTypeVar(): Unit = TODO()
fun <T : MutableList<*>> oneBoundedTypeVar(): Unit = TODO()
fun <A, B> twoTypeVar(param: B): A = TODO()
}
""".trimIndent()
)
val javaSrc = Source.java(
"JavaSubject",
"""
import java.util.List;
class JavaSubject {
<T> void oneTypeVar() {}
<T extends List<?>> void oneBoundedTypeVar() { }
<A, B> A twoTypeVar(B param) { return null; }
}
""".trimIndent()
)
runKspTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
listOf("KotlinSubject", "JavaSubject",).forEach { subjectFqn ->
val subject = invocation.processingEnv.requireTypeElement(subjectFqn)
subject.getMethodByJvmName("oneTypeVar").let {
val typeVar = it.executableType.typeVariables.single()
assertThat(typeVar.asTypeName())
.isEqualTo(XTypeName.getTypeVariableName("T"))
assertThat(typeVar.superTypes.map { it.asTypeName() })
.containsExactly(XTypeName.ANY_OBJECT.copy(nullable = true))
assertThat(typeVar.typeArguments).isEmpty()
assertThat(typeVar.typeElement).isNull()
}
subject.getMethodByJvmName("oneBoundedTypeVar").let {
val typeVar = it.executableType.typeVariables.single()
assertThat(typeVar.asTypeName())
.isEqualTo(
XTypeName.getTypeVariableName(
name = "T",
bounds = listOf(
List::class.asMutableClassName()
.parametrizedBy(XTypeName.ANY_WILDCARD)
)
)
)
assertThat(typeVar.superTypes.map { it.asTypeName() })
.containsExactly(
XTypeName.ANY_OBJECT.copy(nullable = true),
List::class.asMutableClassName()
.parametrizedBy(XTypeName.ANY_WILDCARD)
)
assertThat(typeVar.typeArguments).isEmpty()
assertThat(typeVar.typeElement).isNull()
}
subject.getMethodByJvmName("twoTypeVar").let {
// TODO(b/294102849): Figure out origin JAVA bounds difference between type
// var declaration and usage.
if (invocation.isKsp && subjectFqn == "JavaSubject") {
return@let
}
val firstTypeVar = it.executableType.typeVariables[0]
assertThat(firstTypeVar.isSameType(it.returnType)).isTrue()
assertThat(firstTypeVar).isNotEqualTo(it.parameters.single().type)
val secondTypeVar = it.executableType.typeVariables[1].asTypeName()
assertThat(secondTypeVar).isNotEqualTo(it.returnType.asTypeName())
assertThat(secondTypeVar).isEqualTo(it.parameters.single().type.asTypeName())
}
}
}
}
}