blob: 43a0b276b5ee37fafce8a50188892875b9785124 [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.room.compiler.processing.javac.JavacMethodElement
import androidx.room.compiler.processing.javac.JavacTypeElement
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.compileFiles
import androidx.room.compiler.processing.util.javaTypeUtils
import androidx.room.compiler.processing.util.runKaptTest
import androidx.room.compiler.processing.util.runProcessorTest
import com.google.auto.common.MoreTypes
import com.google.common.truth.Truth.assertThat
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.io.File
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Types
@RunWith(Parameterized::class)
class MethodSpecHelperTest(
// if true, pre-compile sources then run the test to account for changes between .class files
// and source files
val preCompiledCode: Boolean
) {
@Test
fun javaOverrides() {
// check our override impl matches javapoet
val source = Source.java(
"foo.bar.Baz",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.OtherAnnotation;
public class Baz {
public void method1() {
}
public void method2(int x) {
}
public int parameterAnnotation(@OtherAnnotation("x") int y) {
return 3;
}
@OtherAnnotation("x")
public int methodAnnotation(int y) {
return 3;
}
public int varargMethod(int... y) {
return 3;
}
protected <R> R typeArgs(R r) {
return r;
}
protected void throwsException() throws Exception {
}
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun kotlinOverrides() {
// check our override impl matches javapoet
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar;
import androidx.room.compiler.processing.testcode.OtherAnnotation;
object MyObject
abstract class Baz {
open fun method1() {
}
open fun method2(x:Int) {
}
open fun parameterAnnotation(@OtherAnnotation("x") y:Int): Int {
return 3;
}
@OtherAnnotation("x")
open fun methodAnnotation(y: Int): Int {
return 3;
}
open fun varargMethod(vararg y:Int): Int {
return 3;
}
open fun boxedLongArrayReturn(): Array<Long> {
TODO();
}
open fun boxedIntArrayReturn(): Array<Int> {
TODO();
}
protected open fun listArg(r:List<String>) {
}
protected open fun listOfUnitArg(r:List<Unit>) {
}
protected open fun listOfCustomObjectArg(r:List<MyObject>) {
}
protected open fun listOfAnyArg(r:List<Any>) {
}
protected open fun listOfVoidArg(r:List<Void>) {
}
open suspend fun suspendUnitFun() {
}
protected open suspend fun suspendBasic(p0:Int):String {
TODO()
}
protected open suspend fun suspendVarArg(p0:Int, vararg p1:String):Long {
TODO()
}
protected open fun <R> typeArgs(r:R): R {
return r;
}
internal open fun internalFun() {
}
@Throws(Exception::class)
protected open fun throwsException() {
}
// keep these at the end to match the order w/ KAPT because we fake them in KSP
internal abstract val abstractVal: String
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun kotlinParametersAsFunction() {
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar;
interface MyInterface
interface Baz {
fun noArg_returnsUnit(operation: () -> Unit) {
}
fun singleArg_returnsUnit(operation: (Int) -> Unit) {
}
fun singleInterfaceArg_returnsUnit(operation: (MyInterface) -> Unit) {
}
fun singleReceiverArg_returnsUnit(operation: Int.() -> Unit) {
}
fun singleInterfaceReceiverArg_returnsUnit(operation: MyInterface.() -> Unit) {
}
fun noArg_returnsInt(operation: () -> Int) {
}
fun singleArg_returnsInterface(operation: (Int) -> MyInterface) {
}
fun noArg_suspend_returnsUnit(operation: suspend () -> Unit) {
}
suspend fun suspend_noArg_suspend_returnsUnit(operation: suspend () -> Unit) {
}
suspend fun suspend_no_arg_suspend_returnsString(operation: suspend () -> String) {
}
suspend fun suspend_singleArg_suspend_returnsUnit(operation: suspend (arg: String) -> Unit) {
}
suspend fun suspend_threeArgs_suspend_returnsUnit(operation: suspend (one: String, two: Int, three: Boolean) -> Unit) {
}
suspend fun suspend_singleArg_suspend_returnsString(operation: suspend (arg: String) -> String) {
}
suspend fun suspend_threeArgs_suspend_returnsString(operation: suspend (one: String, two: Int, three: Boolean) -> String) {
}
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun variance() {
// check our override impl matches javapoet
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar;
interface MyInterface<T> {
suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
}
interface I1<in T>
interface I2<out T>
interface I3<T>
enum class Lang {
ES,
EN;
}
class Box<out T>(val value: T)
interface Base
class Derived : Base
interface Baz : MyInterface<String> {
fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value
fun unboxString(box: Box<String>): String = box.value
fun findByLanguages(langs: Set<Lang>): List<String>
fun f1(args : I1<String>): I1<String>
fun f2(args : I2<String>): I2<String>
fun f3(args : I3<String>): I3<String>
suspend fun s1(args : I1<String>): I1<String>
suspend fun s2(args : I2<String>): I2<String>
suspend fun s3(args : I3<String>): I3<String>
suspend fun s4(args : I1<String>): String
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun inheritedVariance_openType() {
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar;
interface MyInterface<T> {
fun receiveList(argsInParent : List<T>):Unit
suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
}
open class Book(val id:Int)
interface Baz : MyInterface<Book> {
fun myList(args: List<Book>):Unit
override fun receiveList(argsInParent : List<Book>):Unit
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun inheritedVariance_finalType() {
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar;
interface MyInterface<T> {
fun receiveList(argsInParent : List<T>):Unit
suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
}
interface Baz : MyInterface<String> {
fun myList(args: List<String>):Unit
override fun receiveList(argsInParent : List<String>):Unit
}
""".trimIndent()
)
overridesCheck(source, ignoreInheritedMethods = true)
}
@Test
fun inheritedVariance_enumType() {
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar;
enum class EnumType {
FOO,
BAR;
}
interface MyInterface<T> {
fun receiveList(argsInParent : List<T>):Unit
suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
}
interface Baz : MyInterface<EnumType> {
fun myList(args: List<EnumType>):Unit
override fun receiveList(argsInParent : List<EnumType>):Unit
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun inheritedVariance_multiLevel() {
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar;
interface GrandParent<T> {
fun receiveList(list : List<T>): Unit
suspend fun suspendReceiveList(list : List<T>): Unit
suspend fun suspendReturnList(): List<T>
}
interface Parent: GrandParent<Number> {
}
interface Baz : Parent {
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun primitiveOverrides() {
val source = Source.kotlin(
"Foo.kt",
"""
package foo.bar
data class LongFoo(val id: Long, val description: String)
/* Interface with generics only */
interface MyInterface<Key, Value> {
fun getItem(id: Key): Value?
//fun delete(id: Key)
//fun getFirstItemId(): Key
}
/* Interface with non-generics and generics */
interface Baz : MyInterface<Long, LongFoo> {
override fun getItem(id: Long): LongFoo?
//override fun delete(id: Long)
//fun insert(item: LongFoo)
//override fun getFirstItemId(): Long
}
""".trimIndent()
)
overridesCheck(source)
}
@Test
fun javaOverridesWithVariance() {
val source = Source.java(
"foo.bar.Base",
"""
package foo.bar;
import java.util.List;
interface Base<T> {
void genericT(List<T> t);
void singleT(T t);
void varargT(T... t);
void arrayT(T[] t);
}
"""
)
val impl = Source.java(
"foo.bar.Baz",
"""
package foo.bar;
public interface Baz extends Base<Integer> {
}
""".trimIndent()
)
overridesCheck(source, impl)
}
@Test
fun javaOverridesKotlinProperty() {
val myInterface = Source.kotlin(
"MyInterface.kt",
"""
package foo.bar
interface MyInterface {
val x:Int
var y:Int
}
""".trimIndent()
)
val javaImpl = Source.java(
"foo.bar.Baz",
"""
package foo.bar;
class Baz implements MyInterface {
public int getX() {
return 1;
}
public int getY() {
return 1;
}
public void setY(int value) {
}
}
""".trimIndent()
)
overridesCheck(myInterface, javaImpl)
}
@Test
fun kotlinOverridesKotlinProperty() {
val source = Source.kotlin(
"MyInterface.kt",
"""
package foo.bar
interface MyInterface {
var x:Int
}
open class Baz : MyInterface {
override var x: Int
get() = TODO("not implemented")
set(value) {}
}
""".trimIndent()
)
overridesCheck(source)
}
@Suppress("NAME_SHADOWING") // intentional
private fun overridesCheck(
vararg sources: Source,
ignoreInheritedMethods: Boolean = false
) {
val (sources: List<Source>, classpath: List<File>) = if (preCompiledCode) {
emptyList<Source>() to compileFiles(sources.toList())
} else {
sources.toList() to emptyList()
}
// first build golden image with Java processor so we can use JavaPoet's API
val golden = buildMethodsViaJavaPoet(
sources = sources,
classpath = classpath,
ignoreInheritedMethods = ignoreInheritedMethods
)
runProcessorTest(
sources = sources + Source.kotlin("Placeholder.kt", ""),
classpath = classpath
) { invocation ->
val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
methods.forEachIndexed { index, method ->
if (invocation.isKsp && method.name == "throwsException" && preCompiledCode) {
// TODO b/171572318
// https://github.com/google/ksp/issues/507
} else {
val subject = MethodSpecHelper.overridingWithFinalParams(
method,
target.type
).toSignature()
assertThat(subject).isEqualTo(golden[index])
}
}
}
}
private fun buildMethodsViaJavaPoet(
sources: List<Source>,
classpath: List<File>,
ignoreInheritedMethods: Boolean
): List<String> {
lateinit var result: List<String>
runKaptTest(
sources = sources,
classpath = classpath
) { invocation ->
val (target, methods) = invocation.getOverrideTestTargets(
ignoreInheritedMethods
)
val element = (target as JavacTypeElement).element
result = methods
.map {
(it as JavacMethodElement).element
}.map {
generateFromJavapoet(
it,
MoreTypes.asDeclared(element.asType()),
invocation.javaTypeUtils
).toSignature()
}
}
return result
}
/**
* Get test targets. There is an edge case where it is not possible to implement an interface
* in java, b/174313780. [ignoreInheritedMethods] helps avoid that case.
*/
private fun XTestInvocation.getOverrideTestTargets(
ignoreInheritedMethods: Boolean
): Pair<XTypeElement, List<XMethodElement>> {
val objectMethodNames = processingEnv
.requireTypeElement("java.lang.Object")
.getAllNonPrivateInstanceMethods()
.map {
it.name
}
val target = processingEnv.requireTypeElement("foo.bar.Baz")
val methods = if (ignoreInheritedMethods) {
target.getDeclaredMethods().filter { !it.isStatic() }
} else {
target.getAllNonPrivateInstanceMethods().toList()
}
val selectedMethods = methods.filter {
it.isOverrideableIgnoringContainer()
}.filterNot {
it.name in objectMethodNames
}
return target to selectedMethods
}
private fun generateFromJavapoet(
method: ExecutableElement,
owner: DeclaredType,
typeUtils: Types
): MethodSpec.Builder {
return overrideWithoutAnnotations(
elm = method,
owner = owner,
typeUtils = typeUtils
)
}
private fun MethodSpec.Builder.toSignature(): String {
if (preCompiledCode) {
// remove parameter names as they are not always read properly but doesn't matter
// here much
val backup = this.parameters.toList()
parameters.clear()
backup.forEachIndexed { index, spec ->
addParameter(spec.rename("arg$index"))
}
}
return build().toString()
}
private fun ParameterSpec.rename(newName: String): ParameterSpec {
return ParameterSpec
.builder(
type,
newName
).addModifiers(modifiers)
.addAnnotations(annotations)
.build()
}
/**
* Copied from DaoWriter for backwards compatibility
*/
private fun overrideWithoutAnnotations(
elm: ExecutableElement,
owner: DeclaredType,
typeUtils: Types
): MethodSpec.Builder {
val baseSpec = MethodSpec.overriding(elm, owner, typeUtils)
.build()
// make all the params final
val params = baseSpec.parameters.map {
it.toBuilder().addModifiers(Modifier.FINAL).build()
}
return MethodSpec.methodBuilder(baseSpec.name).apply {
addAnnotation(Override::class.java)
addModifiers(baseSpec.modifiers)
addParameters(params)
addTypeVariables(baseSpec.typeVariables)
addExceptions(baseSpec.exceptions)
varargs(baseSpec.varargs)
returns(baseSpec.returnType)
}
}
companion object {
@JvmStatic
@Parameterized.Parameters(name = "preCompiledCode={0}")
fun params() = listOf(false, true)
}
}