blob: 24449d4ef55ef21c1dc4a5b26f93b2c44df5f7a7 [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.codegen.XClassName
import androidx.room.compiler.codegen.XTypeName
import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.processing.javac.JavacType
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
import androidx.room.compiler.processing.util.asKClassName
import androidx.room.compiler.processing.util.asMutableKClassName
import androidx.room.compiler.processing.util.compileFiles
import androidx.room.compiler.processing.util.createXTypeVariableName
import androidx.room.compiler.processing.util.getAllFieldNames
import androidx.room.compiler.processing.util.getDeclaredField
import androidx.room.compiler.processing.util.getField
import androidx.room.compiler.processing.util.getMethodByJvmName
import androidx.room.compiler.processing.util.runProcessorTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.javapoet.JClassName
import com.squareup.kotlinpoet.javapoet.JParameterizedTypeName
import com.squareup.kotlinpoet.javapoet.JTypeName
import com.squareup.kotlinpoet.javapoet.JTypeVariableName
import com.squareup.kotlinpoet.javapoet.KClassName
import com.squareup.kotlinpoet.javapoet.KTypeVariableName
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@RunWith(Parameterized::class)
class XTypeElementTest(
private val isPreCompiled: Boolean,
) {
private fun runTest(
sources: List<Source>,
handler: (XTestInvocation) -> Unit
) {
if (isPreCompiled) {
val compiled = compileFiles(sources)
val hasKotlinSources = sources.any {
it is Source.KotlinSource
}
val kotlinSources = if (hasKotlinSources) {
listOf(
Source.kotlin("placeholder.kt", "class PlaceholderKotlin")
)
} else {
emptyList()
}
val newSources = kotlinSources + Source.java(
"PlaceholderJava",
"public class " +
"PlaceholderJava {}"
)
runProcessorTest(
sources = newSources,
handler = handler,
classpath = compiled
)
} else {
runProcessorTest(
sources = sources,
handler = handler
)
}
}
@Test
fun qualifiedNames() {
val src1 = Source.kotlin(
"Foo.kt",
"""
class TopLevel
""".trimIndent()
)
val src2 = Source.kotlin(
"Bar.kt",
"""
package foo.bar
class InFooBar
""".trimIndent()
)
val src3 = Source.java(
"foo.bar.Outer",
"""
package foo.bar;
public class Outer {
public static class Nested {
}
}
"""
)
runTest(
sources = listOf(src1, src2, src3)
) { invocation ->
invocation.processingEnv.requireTypeElement("TopLevel").let {
assertThat(it.packageName).isEqualTo("")
assertThat(it.name).isEqualTo("TopLevel")
assertThat(it.qualifiedName).isEqualTo("TopLevel")
assertThat(it.asClassName().java)
.isEqualTo(JClassName.get("", "TopLevel"))
if (invocation.isKsp) {
assertThat(it.asClassName().kotlin)
.isEqualTo(KClassName("", "TopLevel"))
} else {
// In javac / kapt we don't have KotlinPoet names
assertThat(it.asClassName().kotlin)
.isEqualTo(XTypeName.UNAVAILABLE_KTYPE_NAME)
}
}
invocation.processingEnv.requireTypeElement("foo.bar.InFooBar").let {
assertThat(it.packageName).isEqualTo("foo.bar")
assertThat(it.name).isEqualTo("InFooBar")
assertThat(it.qualifiedName).isEqualTo("foo.bar.InFooBar")
assertThat(it.asClassName()).isEqualTo(XClassName.get("foo.bar", "InFooBar"))
}
invocation.processingEnv.requireTypeElement("foo.bar.Outer").let {
assertThat(it.packageName).isEqualTo("foo.bar")
assertThat(it.name).isEqualTo("Outer")
assertThat(it.qualifiedName).isEqualTo("foo.bar.Outer")
assertThat(it.asClassName()).isEqualTo(XClassName.get("foo.bar", "Outer"))
}
invocation.processingEnv.requireTypeElement("foo.bar.Outer.Nested").let {
assertThat(it.packageName).isEqualTo("foo.bar")
assertThat(it.name).isEqualTo("Nested")
assertThat(it.qualifiedName).isEqualTo("foo.bar.Outer.Nested")
assertThat(it.asClassName())
.isEqualTo(XClassName.get("foo.bar", "Outer", "Nested"))
}
if (invocation.isKsp) {
// these are KSP specific tests, typenames are tested elsewhere
invocation.processingEnv.requireTypeElement("java.lang.Integer").let {
// always return kotlin types, this is what compiler does
assertThat(it.packageName).isEqualTo("kotlin")
assertThat(it.name).isEqualTo("Int")
assertThat(it.qualifiedName).isEqualTo("kotlin.Int")
assertThat(it.asClassName().java).isEqualTo(JTypeName.INT.box())
assertThat(it.asClassName().kotlin).isEqualTo(INT)
}
invocation.processingEnv.requireTypeElement("kotlin.Int").let {
assertThat(it.packageName).isEqualTo("kotlin")
assertThat(it.name).isEqualTo("Int")
assertThat(it.qualifiedName).isEqualTo("kotlin.Int")
assertThat(it.asClassName().java).isEqualTo(JTypeName.INT.box())
assertThat(it.asClassName().kotlin).isEqualTo(INT)
}
}
}
}
@Test
fun typeAndSuperType() {
val src = Source.kotlin(
"foo.kt",
"""
package foo.bar
class Baz : MyInterface, AbstractClass()
abstract class AbstractClass
interface MyInterface
interface AnotherInterface : MyInterface
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
assertThat(it.superClass!!.asTypeName()).isEqualTo(
invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName()
)
assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName(),
invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
)
assertThat(it.type.asTypeName()).isEqualTo(
invocation.processingEnv.requireType("foo.bar.Baz").asTypeName()
)
assertThat(it.isInterface()).isFalse()
assertThat(it.isKotlinObject()).isFalse()
assertThat(it.isAbstract()).isFalse()
}
invocation.processingEnv.requireTypeElement("foo.bar.AbstractClass").let {
assertThat(it.superClass!!.asTypeName()).isEqualTo(
invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
)
assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
)
assertThat(it.isAbstract()).isTrue()
assertThat(it.isInterface()).isFalse()
assertThat(it.type.asTypeName()).isEqualTo(
invocation.processingEnv.requireType("foo.bar.AbstractClass").asTypeName()
)
}
invocation.processingEnv.requireTypeElement("foo.bar.MyInterface").let {
assertThat(it.superClass).isNull()
assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
invocation.processingEnv.requireType(JTypeName.OBJECT).asTypeName()
)
assertThat(it.isInterface()).isTrue()
assertThat(it.type.asTypeName()).isEqualTo(
invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
)
}
invocation.processingEnv.requireTypeElement("foo.bar.AnotherInterface").let {
assertThat(it.superClass).isNull()
assertThat(it.type.superTypes.map(XType::asTypeName)).containsExactly(
invocation.processingEnv.requireType("java.lang.Object").asTypeName(),
invocation.processingEnv.requireType("foo.bar.MyInterface").asTypeName()
)
assertThat(it.isInterface()).isTrue()
assertThat(it.type.asTypeName()).isEqualTo(
invocation.processingEnv.requireType("foo.bar.AnotherInterface").asTypeName()
)
}
}
}
@Test
fun superInterfaces() {
val src = Source.kotlin(
"foo.kt",
"""
package foo.bar
class Baz : MyInterface<String>, AbstractClass()
abstract class AbstractClass
interface MyInterface<E>
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Baz").let {
assertThat(it.superInterfaces).hasSize(1)
val superInterface = it.superInterfaces.first { type ->
type.rawType.toString() == "foo.bar.MyInterface"
}
assertThat(superInterface.typeArguments).hasSize(1)
assertThat(superInterface.typeArguments[0].asTypeName().java)
.isEqualTo(JClassName.get("java.lang", "String"))
if (invocation.isKsp) {
assertThat(superInterface.typeArguments[0].asTypeName().kotlin)
.isEqualTo(KClassName("kotlin", "String"))
}
if (! invocation.isKsp) {
assertThat((superInterface as JavacType).kotlinType).isNotNull()
}
}
}
}
@Test
fun nestedClassName() {
val src = Source.kotlin(
"Foo.kt",
"""
package foo.bar
class Outer {
class Inner
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("foo.bar.Outer").let {
assertThat(it.asClassName())
.isEqualTo(XClassName.get("foo.bar", "Outer"))
assertThat(it.isNested()).isFalse()
assertThat(it.enclosingTypeElement).isNull()
}
invocation.processingEnv.requireTypeElement("foo.bar.Outer.Inner").let {
assertThat(it.asClassName())
.isEqualTo(XClassName.get("foo.bar", "Outer", "Inner"))
assertThat(it.packageName).isEqualTo("foo.bar")
assertThat(it.name).isEqualTo("Inner")
assertThat(it.isNested()).isTrue()
assertThat(it.enclosingTypeElement).isEqualTo(
invocation.processingEnv.requireTypeElement("foo.bar.Outer")
)
}
}
}
@Test
fun modifiers() {
val kotlinSrc = Source.kotlin(
"Foo.kt",
"""
open class OpenClass
abstract class AbstractClass
object MyObject
interface MyInterface
class Final
private class PrivateClass
class OuterKotlinClass {
inner class InnerKotlinClass
class NestedKotlinClass
}
annotation class KotlinAnnotation
data class DataClass(val foo: Int)
inline class InlineClass(val foo: Int)
fun interface FunInterface {
fun foo()
}
""".trimIndent()
)
val javaSrc = Source.java(
"OuterJavaClass",
"""
public class OuterJavaClass {
public class InnerJavaClass {}
public static class NestedJavaClass {}
}
""".trimIndent()
)
val javaAnnotationSrc = Source.java(
"JavaAnnotation",
"""
public @interface JavaAnnotation {
}
""".trimIndent()
)
runTest(
sources = listOf(kotlinSrc, javaSrc, javaAnnotationSrc)
) { invocation ->
fun getModifiers(element: XTypeElement): Set<String> {
val result = mutableSetOf<String>()
if (element.isAbstract()) result.add("abstract")
if (element.isFinal()) result.add("final")
if (element.isPrivate()) result.add("private")
if (element.isProtected()) result.add("protected")
if (element.isPublic()) result.add("public")
if (element.isKotlinObject()) result.add("object")
if (element.isCompanionObject()) result.add("companion")
if (element.isFunctionalInterface()) result.add("fun")
if (element.isClass()) result.add("class")
if (element.isDataClass()) result.add("data")
if (element.isValueClass()) result.add("value")
if (element.isExpect()) result.add("expect")
if (element.isInterface()) result.add("interface")
if (element.isStatic()) result.add("static")
if (element.isAnnotationClass()) result.add("annotation")
return result
}
fun getModifiers(qName: String): Set<String> = getModifiers(
invocation.processingEnv
.requireTypeElement(qName)
)
assertThat(getModifiers("OpenClass"))
.containsExactly("public", "class")
assertThat(getModifiers("AbstractClass"))
.containsExactly("abstract", "public", "class")
assertThat(getModifiers("MyObject"))
.containsExactly("final", "public", "object")
assertThat(getModifiers("MyInterface"))
.containsExactly("abstract", "interface", "public")
assertThat(getModifiers("Final"))
.containsExactly("final", "public", "class")
assertThat(getModifiers("PrivateClass"))
.containsExactlyElementsIn(
if (invocation.isKsp) {
listOf("private", "final", "class")
} else {
// java does not support top level private classes.
listOf("final", "class")
}
)
assertThat(getModifiers("OuterKotlinClass.InnerKotlinClass"))
.containsExactly("final", "public", "class")
assertThat(getModifiers("OuterKotlinClass.NestedKotlinClass"))
.containsExactly("final", "public", "static", "class")
assertThat(getModifiers("OuterJavaClass.InnerJavaClass"))
.containsExactly("public", "class")
assertThat(getModifiers("OuterJavaClass.NestedJavaClass"))
.containsExactly("public", "static", "class")
assertThat(getModifiers("JavaAnnotation")).apply {
// KSP vs KAPT metadata have a difference in final vs abstract modifiers
// for annotation types.
if (isPreCompiled && invocation.isKsp) {
containsExactly("final", "public", "annotation")
} else {
containsExactly("abstract", "public", "annotation")
}
}
assertThat(getModifiers("KotlinAnnotation")).apply {
// KSP vs KAPT metadata have a difference in final vs abstract modifiers
// for annotation types.
if (invocation.isKsp) {
containsExactly("final", "public", "annotation")
} else {
containsExactly("abstract", "public", "annotation")
}
}
assertThat(getModifiers("DataClass"))
.containsExactly("public", "final", "class", "data")
assertThat(getModifiers("InlineClass")).apply {
containsExactly("public", "final", "class", "value")
}
assertThat(getModifiers("FunInterface"))
.containsExactly("public", "abstract", "interface", "fun")
}
}
@Test
fun kindName() {
val kotlinSrc = Source.kotlin(
"Foo.kt",
"""
class KotlinClass
interface KotlinInterface
annotation class KotlinAnnotation
""".trimIndent()
)
val javaSrc = Source.java(
"Bar.java",
"""
class JavaClass {}
interface JavaInterface {}
@interface JavaAnnotation {}
""".trimIndent()
)
runTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
invocation.processingEnv.requireTypeElement("KotlinClass").let {
assertThat(it.kindName()).isEqualTo("class")
}
invocation.processingEnv.requireTypeElement("KotlinInterface").let {
assertThat(it.kindName()).isEqualTo("interface")
}
invocation.processingEnv.requireTypeElement("KotlinAnnotation").let {
// TODO(b/270557392): make the result consistent between KSP and JavaAP
if (invocation.isKsp) {
assertThat(it.kindName()).isEqualTo("annotation_class")
} else {
assertThat(it.kindName()).isEqualTo("annotation_type")
}
}
invocation.processingEnv.requireTypeElement("JavaClass").let {
assertThat(it.kindName()).isEqualTo("class")
}
invocation.processingEnv.requireTypeElement("JavaInterface").let {
assertThat(it.kindName()).isEqualTo("interface")
}
invocation.processingEnv.requireTypeElement("JavaAnnotation").let {
// TODO(b/270557392): make the result consistent between KSP and JavaAP
if (invocation.isKsp) {
assertThat(it.kindName()).isEqualTo("annotation_class")
} else {
assertThat(it.kindName()).isEqualTo("annotation_type")
}
}
}
}
@Test
fun fieldBasic() {
val src = Source.kotlin(
"Foo.kt",
"""
open class BaseClass<T>(val genericProp : T) {
fun baseMethod(input: T) {}
}
class SubClass(x : Int) : BaseClass<Int>(x) {
val subClassProp : String = "abc"
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
assertThat(baseClass.getAllFieldNames()).containsExactly("genericProp")
assertThat(baseClass.getDeclaredFields().map { it.name })
.containsExactly("genericProp")
val subClass = invocation.processingEnv.requireTypeElement("SubClass")
assertThat(subClass.getAllFieldNames()).containsExactly("genericProp", "subClassProp")
assertThat(subClass.getDeclaredFields().map { it.name })
.containsExactly("subClassProp")
val baseMethod = baseClass.getMethodByJvmName("baseMethod")
baseMethod.asMemberOf(subClass.type).let { methodType ->
val genericArg = methodType.parameterTypes.first()
assertThat(genericArg.asTypeName()).isEqualTo(Int::class.asClassName())
}
baseClass.getField("genericProp").let { field ->
assertThat(field.type.asTypeName()).isEqualTo(createXTypeVariableName("T"))
}
subClass.getField("genericProp").let { field ->
// this is tricky because even though it is non-null it, it should still be boxed
assertThat(field.asMemberOf(subClass.type).asTypeName())
.isEqualTo(Int::class.asClassName())
}
}
}
@Test
fun fieldsOverride() {
val src = Source.kotlin(
"Foo.kt",
"""
open class BaseClass(
open val value : List<Int>
)
class SubClass(
override val value : MutableList<Int>
) : BaseClass(value)
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val baseClass = invocation.processingEnv.requireTypeElement("BaseClass")
assertThat(baseClass.getAllFieldNames()).containsExactly("value")
val subClass = invocation.processingEnv.requireTypeElement("SubClass")
assertThat(subClass.getAllFieldNames()).containsExactly("value")
val baseFieldType = baseClass.getField("value").type
val subFieldType = subClass.getField("value").type
assertThat(baseFieldType.asTypeName().java).isEqualTo(
JParameterizedTypeName.get(List::class.java, Integer::class.java)
)
assertThat(subFieldType.asTypeName().java).isEqualTo(
JParameterizedTypeName.get(List::class.java, Integer::class.java)
)
if (invocation.isKsp) {
assertThat(baseFieldType.asTypeName().kotlin).isEqualTo(
List::class.asKClassName().parameterizedBy(INT)
)
assertThat(subFieldType.asTypeName().kotlin).isEqualTo(
List::class.asMutableKClassName().parameterizedBy(INT)
)
}
}
}
@Test
fun fieldsMethodsWithoutBacking() {
runTest(
sources = listOf(Source.kotlin(
"Foo.kt",
"""
package test
class Subject {
val realField: String = ""
get() = field
val noBackingVal: String
get() = ""
var noBackingVar: String
get() = ""
set(value) {}
companion object {
@JvmStatic
val staticRealField: String = ""
get() = field
@JvmStatic
val staticNoBackingVal: String
get() = ""
@JvmStatic
var staticNoBackingVar: String
get() = ""
set(value) {}
}
}
""".trimIndent()
)),
) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("test.Subject")
val declaredFields = subject.getDeclaredFields().map { it.name } -
listOf("Companion") // skip Companion, KAPT generates it
val expectedFields = listOf("realField", "staticRealField")
assertWithMessage(subject.qualifiedName)
.that(declaredFields)
.containsExactlyElementsIn(expectedFields)
val allFields = subject.getAllFieldsIncludingPrivateSupers().map { it.name } -
listOf("Companion") // skip Companion, KAPT generates it
assertWithMessage(subject.qualifiedName)
.that(allFields.toList())
.containsExactlyElementsIn(expectedFields)
val methodNames = subject.getDeclaredMethods().map { it.jvmName }
assertWithMessage(subject.qualifiedName)
.that(methodNames)
.containsAtLeast("getNoBackingVal", "getNoBackingVar", "setNoBackingVar")
assertWithMessage(subject.qualifiedName)
.that(methodNames)
.doesNotContain("setNoBackingVal")
}
}
@Test
fun abstractFields() {
runTest(
sources = listOf(Source.kotlin(
"Foo.kt",
"""
package test
abstract class Subject {
val value: String = ""
abstract val abstractValue: String
companion object {
var realCompanion: String = ""
@JvmStatic
var jvmStatic: String = ""
}
}
""".trimIndent()
)),
) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("test.Subject")
val declaredFields = subject.getDeclaredFields().map { it.name } -
listOf("Companion")
val expectedFields = listOf("value", "realCompanion", "jvmStatic")
assertWithMessage(subject.qualifiedName)
.that(declaredFields)
.containsExactlyElementsIn(expectedFields)
}
}
@Test
fun lateinitFields() {
runTest(
sources = listOf(Source.kotlin(
"Foo.kt",
"""
package test
class Subject {
lateinit var x:String
var y:String = "abc"
}
""".trimIndent()
)),
) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("test.Subject")
assertWithMessage(subject.fallbackLocationText)
.that(subject.getDeclaredFields().map { it.name })
.containsExactly(
"x", "y"
)
assertWithMessage(subject.fallbackLocationText)
.that(subject.getDeclaredMethods().map { it.jvmName })
.containsExactly(
"getX", "setX", "getY", "setY"
)
subject.getField("x").let { field ->
assertThat(field.isFinal()).isFalse()
assertThat(field.isPrivate()).isFalse()
}
}
}
@Test
fun fieldsInInterfaces() {
val src = Source.kotlin(
"Foo.kt",
"""
interface MyInterface {
var x:Int
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val element = invocation.processingEnv.requireTypeElement("MyInterface")
assertThat(element.getAllFieldsIncludingPrivateSupers().toList()).isEmpty()
element.getMethodByJvmName("getX").let {
assertThat(it.isAbstract()).isTrue()
}
element.getMethodByJvmName("setX").let {
assertThat(it.isAbstract()).isTrue()
}
}
}
@Test
fun fieldsInAbstractClass() {
val src = Source.kotlin(
"Foo.kt",
"""
abstract class MyAbstractClass {
@JvmField
var jvmVar: Int = 0
abstract var abstractVar: Int
var nonAbstractVar: Int = 0
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val element = invocation.processingEnv.requireTypeElement("MyAbstractClass")
assertThat(
element.getAllFieldNames()
).containsExactly(
"nonAbstractVar", "jvmVar"
)
assertThat(
element.getDeclaredMethods().map { it.jvmName }
).containsExactly(
"getAbstractVar", "setAbstractVar",
"getNonAbstractVar", "setNonAbstractVar"
)
element.getMethodByJvmName("getAbstractVar").let {
assertThat(it.isAbstract()).isTrue()
}
element.getMethodByJvmName("setAbstractVar").let {
assertThat(it.isAbstract()).isTrue()
}
element.getMethodByJvmName("getNonAbstractVar").let {
assertThat(it.isAbstract()).isFalse()
}
element.getMethodByJvmName("setNonAbstractVar").let {
assertThat(it.isAbstract()).isFalse()
}
}
}
@Test
fun propertyGettersSetters() {
runTest(
listOf(
Source.java(
"JavaSubject.java",
"""
class JavaSubject {
int myField;
private int mutable;
int immutable;
int getMutable() {return 3;}
void setMutable(int x) {}
int getImmutable() {return 3;}
}
""".trimIndent()
),
Source.kotlin(
"KotlinSubject.kt",
"""
class KotlinSubject {
private val myField = 0
var mutable: Int = 0
val immutable:Int = 0
}
""".trimIndent()
)
),
) { invocation ->
listOf("JavaSubject", "KotlinSubject",).map {
invocation.processingEnv.requireTypeElement(it)
}.forEach { subject ->
val methods = subject.getDeclaredMethods()
assertWithMessage(subject.qualifiedName)
.that(
methods.map {
it.jvmName
}
).containsExactly(
"getMutable", "setMutable", "getImmutable"
)
methods.forEach {
assertWithMessage("${subject.qualifiedName}.${it.jvmName}()")
.that(it.isKotlinPropertyMethod())
.apply {
if (subject.name.contains("Kotlin")) {
isTrue()
} else {
isFalse()
}
}
}
}
}
}
@Test
fun declaredAndInstanceMethods() {
val src = Source.kotlin(
"Foo.kt",
"""
open class Base(x:Int) {
open fun baseFun(): Int = TODO()
suspend fun suspendFun(): Int = TODO()
private fun privateBaseFun(): Int = TODO()
companion object {
@JvmStatic
fun staticBaseFun(): Int = TODO()
fun companionMethod(): Int = TODO()
}
}
open class SubClass : Base {
constructor(y:Int): super(y) {
}
constructor(x:Int, y:Int): super(y) {
}
override fun baseFun(): Int = TODO()
fun subFun(): Int = TODO()
private fun privateSubFun(): Int = TODO()
companion object {
@JvmStatic
fun staticFun(): Int = TODO()
}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("Base")
val objectMethodNames = invocation.objectMethodNames()
assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
"baseFun", "suspendFun", "privateBaseFun", "staticBaseFun"
)
val sub = invocation.processingEnv.requireTypeElement("SubClass")
assertThat(sub.getDeclaredMethods().jvmNames()).containsExactly(
"baseFun", "subFun", "privateSubFun", "staticFun"
)
assertThat(
sub.getAllNonPrivateInstanceMethods().jvmNames() - objectMethodNames
).containsExactly(
"baseFun", "suspendFun", "subFun"
)
}
}
@Test
fun diamondOverride() {
runTest(
sources = listOf(Source.kotlin(
"Foo.kt",
"""
package test
interface Parent<T> {
fun parent(t: T)
}
interface Child1<T> : Parent<T> {
fun child1(t: T)
}
interface Child2<T> : Parent<T> {
fun child2(t: T)
}
abstract class Subject1 : Child1<String>, Child2<String>, Parent<String>
abstract class Subject2 : Child1<String>, Parent<String>
abstract class Subject3 : Child1<String>, Parent<String> {
abstract override fun parent(t: String)
}
""".trimIndent()
)),
) { invocation ->
invocation.processingEnv.requireTypeElement("test.Subject1").let { subject ->
assertWithMessage(subject.qualifiedName).that(
invocation.nonObjectMethodSignatures(subject)
).containsExactly(
"child1(java.lang.String):void",
"child2(java.lang.String):void",
"parent(java.lang.String):void",
)
}
invocation.processingEnv.requireTypeElement("test.Subject2").let { subject ->
assertWithMessage(subject.qualifiedName).that(
invocation.nonObjectMethodSignatures(subject)
).containsExactly(
"child1(java.lang.String):void",
"parent(java.lang.String):void",
)
}
invocation.processingEnv.requireTypeElement("test.Subject3").let { subject ->
assertWithMessage(subject.qualifiedName).that(
invocation.nonObjectMethodSignatures(subject)
).containsExactly(
"child1(java.lang.String):void",
"parent(java.lang.String):void",
)
}
}
}
@Test
fun suspendOverride() {
val src = Source.kotlin(
"Foo.kt",
"""
interface Base<T> {
suspend fun get(): T
suspend fun getAll(): List<T>
suspend fun putAll(input: List<T>)
suspend fun getAllWithDefault(): List<T>
}
interface DerivedInterface : Base<String> {
override suspend fun get(): String
override suspend fun getAll(): List<String>
override suspend fun putAll(input: List<String>)
override suspend fun getAllWithDefault(): List<String> {
return emptyList()
}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("DerivedInterface")
val methodNames = base.getAllMethods().toList().jvmNames()
assertThat(methodNames).containsExactly(
"get", "getAll", "putAll", "getAllWithDefault"
)
}
}
@Test
fun suspendOverride_abstractClass() {
val src = Source.kotlin(
"Foo.kt",
"""
abstract class Base<T> {
abstract suspend fun get(): T
abstract suspend fun getAll(): List<T>
abstract suspend fun putAll(input: List<T>)
}
abstract class DerivedClass : Base<Int>() {
abstract override suspend fun get(): Int
abstract override suspend fun getAll(): List<Int>
override suspend fun putAll(input: List<Int>) {
}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val base = invocation.processingEnv.requireTypeElement("DerivedClass")
val methodNamesCount =
base.getAllMethods().toList().jvmNames().groupingBy { it }.eachCount()
assertThat(methodNamesCount["get"]).isEqualTo(1)
assertThat(methodNamesCount["getAll"]).isEqualTo(1)
assertThat(methodNamesCount["putAll"]).isEqualTo(1)
}
}
@Test
fun overrideMethodWithCovariantReturnType() {
val src = Source.kotlin(
"ParentWithExplicitOverride.kt",
"""
interface ParentWithExplicitOverride: ChildInterface, Child {
override fun child(): Child
}
interface ParentWithoutExplicitOverride: ChildInterface, Child
interface Child: ChildInterface {
override fun child(): Child
}
interface ChildInterface {
fun child(): ChildInterface
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement(
"ParentWithExplicitOverride"
).let { parent ->
assertWithMessage(parent.qualifiedName).that(
invocation.nonObjectMethodSignatures(parent)
).containsExactly(
"child():Child"
)
}
invocation.processingEnv.requireTypeElement(
"ParentWithoutExplicitOverride"
).let { parent ->
assertWithMessage(parent.qualifiedName).that(
invocation.nonObjectMethodSignatures(parent)
).containsExactly(
"child():Child"
)
}
}
}
@Test
fun allMethodsFiltersInAccessibleMethods() {
val srcs = listOf(
Source.java(
"foo.Foo",
"""
package foo;
public interface Foo {
void foo_Public();
}
""".trimIndent()
),
Source.java(
"foo.parent.FooParent",
"""
package foo.parent;
public abstract class FooParent implements foo.Foo {
public void fooParent_Public() {}
protected void fooParent_Protected() {}
private void fooParent_Private() {}
void fooParent_PackagePrivate() {}
}
""".trimIndent()
),
Source.java(
"foo.child.FooChild",
"""
package foo.child;
public abstract class FooChild extends foo.parent.FooParent {
public void fooChild_Public() {}
protected void fooChild_Protected() {}
private void fooChild_Private() {}
void fooChild_PackagePrivate() {}
}
""".trimIndent()
),
)
runTest(sources = srcs) { invocation ->
invocation.processingEnv.requireTypeElement("foo.child.FooChild")
.let { fooChild ->
assertWithMessage(fooChild.qualifiedName).that(
invocation.nonObjectMethodSignatures(fooChild)
).containsExactly(
"foo_Public():void",
"fooParent_Public():void",
"fooParent_Protected():void",
"fooChild_Public():void",
"fooChild_Protected():void",
"fooChild_Private():void",
"fooChild_PackagePrivate():void"
)
}
}
}
@Test
fun allMethods() {
val src = Source.kotlin(
"Foo.kt",
"""
open class Base(x:Int) {
constructor(x:Int, y:Int): this(x) {
}
fun baseMethod(): Int = TODO()
open fun overriddenMethod(): Int = TODO()
private fun privateBaseMethod(): Int = TODO()
companion object {
@JvmStatic
private fun privateBaseCompanionMethod(): Int = TODO()
@JvmStatic
fun baseCompanionMethod(): Int = TODO()
}
}
interface MyInterface {
fun interfaceMethod(): Int = TODO()
}
class SubClass : Base, MyInterface {
constructor(x:Int): super(x) {
}
constructor(x:Int, y:Int): super(y) {
}
fun subMethod(): Int = TODO()
fun privateSubMethod(): Int = TODO()
override fun overriddenMethod(): Int = TODO()
override fun interfaceMethod(): Int = TODO()
companion object {
fun dontSeeThisOne(): Int = TODO()
@JvmStatic
fun subCompanionMethod(): Int = TODO()
}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val objectMethodNames = invocation.objectMethodNames()
val klass = invocation.processingEnv.requireTypeElement("SubClass")
assertThat(
klass.getAllMethods().jvmNames() - objectMethodNames
).containsExactly(
"baseMethod", "overriddenMethod", "baseCompanionMethod",
"interfaceMethod", "subMethod", "privateSubMethod", "subCompanionMethod"
)
}
}
/**
* When JvmNames is used along with a suppression over the error, the behavior becomes
* complicated. Normally, JvmName annotation is not allowed in overrides and open methods, yet
* developers can still use it by putting a suppression over it. The compiler will generate a
* delegating method in these cases in the .class file, yet in KSP, we don't really see that
* method (also shouldn't ideally).
*
* This test is here to acknowledge that the behavior is inconsistent yet working as intended
* from XProcessing's perspective.
*
* Also see: https://youtrack.jetbrains.com/issue/KT-50782 as a sign why this suppression is
* not worth supporting :).
*/
@Test
fun allMethods_withJvmNames() {
runTest(
sources = listOf(
Source.kotlin(
"Foo.kt",
"""
package test
interface Interface {
fun f1()
@JvmName("notF2")
@Suppress("INAPPLICABLE_JVM_NAME")
fun f2()
}
abstract class Subject : Interface {
@JvmName("notF1")
@Suppress("INAPPLICABLE_JVM_NAME")
override fun f1() {
}
}
""".trimIndent()
)
),
) { invocation ->
val appSubject = invocation.processingEnv.requireTypeElement("test.Subject")
val methodNames = appSubject.getAllMethods().map { it.name }.toList()
val methodJvmNames = appSubject.getAllMethods().map { it.jvmName }.toList()
val objectMethodNames = invocation.objectMethodNames()
if (invocation.isKsp) {
assertThat(methodNames - objectMethodNames).containsExactly(
"f1", "f2"
)
assertThat(methodJvmNames - objectMethodNames).containsExactly(
"notF1", "notF2"
)
} else {
assertThat(methodNames - objectMethodNames).containsExactly(
"f1", "f1", "f2"
)
assertThat(methodJvmNames - objectMethodNames).containsExactly(
"f1", "notF1", "notF2"
)
}
}
}
@Test
fun gettersSetters() {
val src = Source.kotlin(
"Foo.kt",
"""
open class JustGetter(val x:Int) {
private val invisible:Int = TODO()
private var invisibleMutable:Int = TODO()
}
class GetterSetter(var y:Int) : JustGetter(y) {
private val subInvisible:Int = TODO()
private var subInvisibleMutable:Int = TODO()
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val objectMethodNames = invocation.objectMethodNames()
invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
"getX"
)
assertThat(base.getAllMethods().jvmNames() - objectMethodNames).containsExactly(
"getX"
)
assertThat(
base.getAllNonPrivateInstanceMethods().jvmNames() - objectMethodNames
).containsExactly(
"getX"
)
}
invocation.processingEnv.requireTypeElement("GetterSetter").let { sub ->
assertThat(sub.getDeclaredMethods().jvmNames()).containsExactly(
"getY", "setY"
)
assertThat(sub.getAllMethods().jvmNames() - objectMethodNames).containsExactly(
"getX", "getY", "setY"
)
assertThat(
sub.getAllNonPrivateInstanceMethods().jvmNames() - objectMethodNames
).containsExactly(
"getX", "getY", "setY"
)
}
}
}
@Test
fun companion() {
val src = Source.kotlin(
"Foo.kt",
"""
open class CompanionSubject {
companion object {
@JvmStatic
var mutableStatic: String = "a"
@JvmStatic
val immutableStatic: String = "bar"
val companionProp: Int = 3
@get:JvmStatic
var companionProp_getterJvmStatic:Int =3
@set:JvmStatic
var companionProp_setterJvmStatic:Int =3
fun companionMethod() {
}
@JvmStatic
fun companionMethodWithJvmStatic() {}
}
}
class SubClass : CompanionSubject()
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val objectMethodNames = invocation.processingEnv.requireTypeElement(
Any::class
).getAllMethods().jvmNames()
val subject = invocation.processingEnv.requireTypeElement("CompanionSubject")
assertThat(subject.getAllFieldNames() - "Companion").containsExactly(
"mutableStatic", "immutableStatic", "companionProp",
"companionProp_getterJvmStatic", "companionProp_setterJvmStatic"
)
val expectedMethodNames = listOf(
"getMutableStatic", "setMutableStatic", "getImmutableStatic",
"getCompanionProp_getterJvmStatic", "setCompanionProp_setterJvmStatic",
"companionMethodWithJvmStatic"
)
assertThat(
subject.getDeclaredMethods().jvmNames()
).containsExactlyElementsIn(
expectedMethodNames
)
assertThat(
subject.getAllMethods().jvmNames() - objectMethodNames
).containsExactlyElementsIn(
expectedMethodNames
)
assertThat(
subject.getAllNonPrivateInstanceMethods().jvmNames() - objectMethodNames
).isEmpty()
val subClass = invocation.processingEnv.requireTypeElement("SubClass")
assertThat(subClass.getDeclaredMethods()).isEmpty()
assertThat(
subClass.getAllMethods().jvmNames() - objectMethodNames
).containsExactlyElementsIn(
expectedMethodNames
)
// make sure everything coming from companion is marked as static
subject.getDeclaredFields().forEach {
assertWithMessage(it.name).that(it.isStatic()).isTrue()
}
subject.getDeclaredMethods().forEach {
assertWithMessage(it.jvmName).that(it.isStatic()).isTrue()
}
// make sure asMemberOf works fine for statics
val subClassType = subClass.type
subject.getDeclaredFields().forEach {
try {
it.asMemberOf(subClassType)
} catch (th: Throwable) {
throw AssertionError("Couldn't run asMemberOf for ${it.name}")
}
}
subject.getDeclaredMethods().forEach {
try {
it.asMemberOf(subClassType)
} catch (th: Throwable) {
throw AssertionError("Couldn't run asMemberOf for ${it.jvmName}")
}
}
}
}
@Test
fun gettersSetters_interface() {
val src = Source.kotlin(
"Foo.kt",
"""
interface JustGetter {
val x:Int
}
interface GetterSetter : JustGetter {
var y:Int
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
invocation.processingEnv.requireTypeElement("JustGetter").let { base ->
assertThat(base.getDeclaredMethods().jvmNames()).containsExactly(
"getX"
)
assertThat(base.getAllMethods().jvmNames()).containsExactly(
"getX"
)
assertThat(base.getAllNonPrivateInstanceMethods().jvmNames()).containsExactly(
"getX"
)
}
invocation.processingEnv.requireTypeElement("GetterSetter").let { sub ->
assertThat(sub.getDeclaredMethods().jvmNames()).containsExactly(
"getY", "setY"
)
assertThat(sub.getAllMethods().jvmNames()).containsExactly(
"getX", "getY", "setY"
)
assertThat(sub.getAllNonPrivateInstanceMethods().jvmNames()).containsExactly(
"getX", "getY", "setY"
)
}
}
}
@Test
fun constructors() {
val src = Source.kotlin(
"Foo.kt",
"""
interface MyInterface
class NoExplicitConstructor
open class Base(x:Int)
open class ExplicitConstructor {
constructor(x:Int)
}
open class BaseWithSecondary(x:Int) {
constructor(y:String):this(3)
}
class Sub(x:Int) : Base(x)
class SubWith3Constructors() : BaseWithSecondary("abc") {
constructor(list:List<String>): this()
constructor(list:List<String>, x:Int): this()
}
abstract class AbstractNoExplicit
abstract class AbstractExplicit(x:Int)
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val subjects = listOf(
"MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
"BaseWithSecondary", "Sub", "SubWith3Constructors",
"AbstractNoExplicit", "AbstractExplicit"
)
val constructorCounts = subjects.map {
it to invocation.processingEnv.requireTypeElement(it).getConstructors().size
}
assertThat(constructorCounts)
.containsExactly(
"MyInterface" to 0,
"NoExplicitConstructor" to 1,
"Base" to 1,
"ExplicitConstructor" to 1,
"BaseWithSecondary" to 2,
"Sub" to 1,
"SubWith3Constructors" to 3,
"AbstractNoExplicit" to 1,
"AbstractExplicit" to 1
)
val primaryConstructorParameterNames = subjects.map {
it to invocation.processingEnv.requireTypeElement(it)
.findPrimaryConstructor()
?.parameters?.map {
it.name
}
}
assertThat(primaryConstructorParameterNames)
.containsExactly(
"MyInterface" to null,
"NoExplicitConstructor" to emptyList<String>(),
"Base" to listOf("x"),
"ExplicitConstructor" to null,
"BaseWithSecondary" to listOf("x"),
"Sub" to listOf("x"),
"SubWith3Constructors" to emptyList<String>(),
"AbstractNoExplicit" to emptyList<String>(),
"AbstractExplicit" to listOf("x")
)
}
}
@Test
fun jvmDefault() {
val src = Source.kotlin(
"Foo.kt",
"""
interface MyInterface {
fun notJvmDefault()
@JvmDefault
fun jvmDefault() {}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("MyInterface")
assertThat(subject.getMethodByJvmName("notJvmDefault").isJavaDefault()).isFalse()
assertThat(subject.getMethodByJvmName("jvmDefault").isJavaDefault()).isTrue()
}
}
@Test
fun constructors_java() {
val src = Source.java(
"Source",
"""
import java.util.List;
interface MyInterface {}
class NoExplicitConstructor{}
class Base {
Base(int x){}
}
class ExplicitConstructor {
ExplicitConstructor(int x){}
}
class BaseWithSecondary {
BaseWithSecondary(int x){}
BaseWithSecondary(String y){}
}
class Sub extends Base {
Sub(int x) {
super(x);
}
}
class SubWith3Constructors extends BaseWithSecondary {
SubWith3Constructors() {
super(3);
}
SubWith3Constructors(List<String> list) {
super(3);
}
SubWith3Constructors(List<String> list, int x) {
super(3);
}
}
abstract class AbstractNoExplicit {}
abstract class AbstractExplicit {
AbstractExplicit(int x) {}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val subjects = listOf(
"MyInterface", "NoExplicitConstructor", "Base", "ExplicitConstructor",
"BaseWithSecondary", "Sub", "SubWith3Constructors",
"AbstractNoExplicit", "AbstractExplicit"
)
val constructorCounts = subjects.map {
it to invocation.processingEnv.requireTypeElement(it).getConstructors().size
}
assertThat(constructorCounts)
.containsExactly(
"MyInterface" to 0,
"NoExplicitConstructor" to 1,
"Base" to 1,
"ExplicitConstructor" to 1,
"BaseWithSecondary" to 2,
"Sub" to 1,
"SubWith3Constructors" to 3,
"AbstractNoExplicit" to 1,
"AbstractExplicit" to 1
)
subjects.forEach {
assertWithMessage(it)
.that(invocation.processingEnv.requireTypeElement(it).findPrimaryConstructor())
.isNull()
}
}
}
@Test
fun enumTypeElement() {
runTest(
sources = listOf(
Source.kotlin(
"test/KotlinEnum.kt",
"""
package test
enum class KotlinEnum(private val x:Int) {
VAL1(1),
VAL2(2);
fun enumMethod(): Unit {}
}
""".trimIndent()
),
Source.java(
"test.JavaEnum",
"""
package test;
public enum JavaEnum {
VAL1(1),
VAL2(2);
private int x;
JavaEnum(int x) {
this.x = x;
}
void enumMethod() {}
}
""".trimIndent()
)
),
) { invocation ->
listOf(
"test.KotlinEnum", "test.JavaEnum",
).forEach { qName ->
val typeElement = invocation.processingEnv.requireTypeElement(qName)
assertWithMessage("$qName is enum")
.that(typeElement.isEnum())
.isTrue()
assertWithMessage("$qName does not report enum constants in methods")
.that(typeElement.getDeclaredMethods().map { it.jvmName })
.run {
contains("enumMethod")
containsNoneOf("VAL1", "VAL2")
}
assertWithMessage("$qName can return enum constants")
.that((typeElement as XEnumTypeElement).entries.map { it.name })
.containsExactly("VAL1", "VAL2")
assertWithMessage("$qName does not report enum constants in fields")
.that(typeElement.getAllFieldNames())
.run {
contains("x")
containsNoneOf("VAL1", "VAL2")
}
assertWithMessage("$qName does not report enum constants in declared fields")
.that(typeElement.getDeclaredFields().map { it.name })
.containsExactly("x")
assertWithMessage("$qName enum entries are XEnumEntry")
.that(typeElement.getEnclosedElements().filter { it.isEnumEntry() })
.hasSize(2)
assertWithMessage("$qName enum entries are not type elements")
.that(typeElement.getEnclosedElements().filter { it.isTypeElement() })
.isEmpty()
}
}
}
@Test
fun enclosedTypes() {
val src = Source.kotlin(
"Foo.kt",
"""
class TopLevelClass {
class NestedClass
object NestedObject
interface NestedInterface
enum class NestedEnum {
A, B
}
companion object {
val foo = 1
}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val topLevelClass = invocation.processingEnv.requireTypeElement("TopLevelClass")
val enclosedTypeElements = topLevelClass.getEnclosedTypeElements()
assertThat(enclosedTypeElements)
.containsExactly(
invocation.processingEnv.requireTypeElement("TopLevelClass.NestedClass"),
invocation.processingEnv.requireTypeElement("TopLevelClass.NestedObject"),
invocation.processingEnv.requireTypeElement("TopLevelClass.NestedInterface"),
invocation.processingEnv.requireTypeElement("TopLevelClass.NestedEnum"),
invocation.processingEnv.requireTypeElement("TopLevelClass.Companion"),
)
}
}
@Test
fun enclosedTypes_java() {
val src = Source.java(
"Source",
"""
class TopLevelClass {
class InnerClass { }
static class NestedClass { }
interface NestedInterface { }
enum NestedEnum {
A, B
}
}
""".trimIndent()
)
runTest(sources = listOf(src)) { invocation ->
val topLevelClass = invocation.processingEnv.requireTypeElement("TopLevelClass")
val enclosedTypeElements = topLevelClass.getEnclosedTypeElements()
assertThat(enclosedTypeElements)
.containsExactly(
invocation.processingEnv.requireTypeElement("TopLevelClass.InnerClass"),
invocation.processingEnv.requireTypeElement("TopLevelClass.NestedClass"),
invocation.processingEnv.requireTypeElement("TopLevelClass.NestedInterface"),
invocation.processingEnv.requireTypeElement("TopLevelClass.NestedEnum"),
)
}
}
@Test
fun kotlinObjects() {
val kotlinSrc = Source.kotlin(
"Test.kt",
"""
package foo.bar
class KotlinClass {
companion object
object NestedObject
}
""".trimIndent()
)
runTest(listOf(kotlinSrc)) { invocation ->
val kotlinClass = invocation.processingEnv.requireTypeElement(
"foo.bar.KotlinClass")
val companionObjects = kotlinClass.getEnclosedTypeElements().filter {
it.isCompanionObject()
}
assertThat(companionObjects.size).isEqualTo(1)
val companionObj = companionObjects.first()
assertThat(companionObj.isKotlinObject()).isTrue()
}
}
@Test
fun inheritedGenericMethod() {
runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
"""
package test
class ConcreteClass: AbstractClass<Foo, Bar>() {}
abstract class AbstractClass<T1, T2> {
fun method(t1: T1, t2: T2): T2 {
return t2
}
}
class Foo
class Bar
""".trimIndent()
)
)
) { invocation ->
val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
val concreteClassMethod = concreteClass.getMethodByJvmName("method")
val abstractClassMethod = abstractClass.getMethodByJvmName("method")
val fooType = invocation.processingEnv.requireType("test.Foo")
val barType = invocation.processingEnv.requireType("test.Bar")
fun checkMethodElement(method: XMethodElement) {
checkMethodElement(
method = method,
name = "method",
enclosingElement = abstractClass,
returnType = createXTypeVariableName("T2"),
parameterTypes = arrayOf(
createXTypeVariableName("T1"),
createXTypeVariableName("T2")
)
)
checkMethodType(
methodType = method.executableType,
returnType = createXTypeVariableName("T2"),
parameterTypes = arrayOf(
createXTypeVariableName("T1"),
createXTypeVariableName("T2")
)
)
checkMethodType(
methodType = method.asMemberOf(abstractClass.type),
returnType = createXTypeVariableName("T2"),
parameterTypes = arrayOf(
createXTypeVariableName("T1"),
createXTypeVariableName("T2")
)
)
checkMethodType(
methodType = method.asMemberOf(concreteClass.type),
returnType = barType.asTypeName(),
parameterTypes = arrayOf(fooType.asTypeName(), barType.asTypeName())
)
}
assertThat(concreteClassMethod).isEqualTo(abstractClassMethod)
checkMethodElement(concreteClassMethod)
checkMethodElement(abstractClassMethod)
}
}
@Test
fun overriddenGenericMethod() {
runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
"""
package test
class ConcreteClass: AbstractClass<Foo, Bar>() {
override fun method(t1: Foo, t2: Bar): Bar {
return t2
}
}
abstract class AbstractClass<T1, T2> {
abstract fun method(t1: T1, t2: T2): T2
}
class Foo
class Bar
""".trimIndent()
)
)
) { invocation ->
val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
val concreteClassMethod = concreteClass.getMethodByJvmName("method")
val abstractClassMethod = abstractClass.getMethodByJvmName("method")
val fooType = invocation.processingEnv.requireType("test.Foo")
val barType = invocation.processingEnv.requireType("test.Bar")
assertThat(concreteClassMethod).isNotEqualTo(abstractClassMethod)
assertThat(concreteClassMethod.overrides(abstractClassMethod, concreteClass)).isTrue()
// Check the abstract method and method type
checkMethodElement(
method = abstractClassMethod,
name = "method",
enclosingElement = abstractClass,
returnType = createXTypeVariableName("T2"),
parameterTypes = arrayOf(
createXTypeVariableName("T1"),
createXTypeVariableName("T2")
)
)
checkMethodType(
methodType = abstractClassMethod.executableType,
returnType = createXTypeVariableName("T2"),
parameterTypes = arrayOf(
createXTypeVariableName("T1"),
createXTypeVariableName("T2")
)
)
checkMethodType(
methodType = abstractClassMethod.asMemberOf(abstractClass.type),
returnType = createXTypeVariableName("T2"),
parameterTypes = arrayOf(
createXTypeVariableName("T1"),
createXTypeVariableName("T2"),
)
)
checkMethodType(
methodType = abstractClassMethod.asMemberOf(concreteClass.type),
returnType = barType.asTypeName(),
parameterTypes = arrayOf(fooType.asTypeName(), barType.asTypeName())
)
// Check the concrete method and method type
checkMethodElement(
method = concreteClassMethod,
name = "method",
enclosingElement = concreteClass,
returnType = barType.asTypeName(),
parameterTypes = arrayOf(fooType.asTypeName(), barType.asTypeName())
)
checkMethodType(
methodType = concreteClassMethod.executableType,
returnType = barType.asTypeName(),
parameterTypes = arrayOf(fooType.asTypeName(), barType.asTypeName())
)
checkMethodType(
methodType = concreteClassMethod.asMemberOf(concreteClass.type),
returnType = barType.asTypeName(),
parameterTypes = arrayOf(fooType.asTypeName(), barType.asTypeName())
)
}
}
private fun checkMethodElement(
method: XMethodElement,
name: String,
enclosingElement: XTypeElement,
returnType: XTypeName,
parameterTypes: Array<XTypeName>
) {
assertThat(method.name).isEqualTo(name)
assertThat(method.enclosingElement).isEqualTo(enclosingElement)
assertThat(method.returnType.asTypeName()).isEqualTo(returnType)
assertThat(method.parameters.map { it.type.asTypeName() })
.containsExactly(*parameterTypes)
.inOrder()
}
private fun checkMethodType(
methodType: XMethodType,
returnType: XTypeName,
parameterTypes: Array<XTypeName>
) {
assertThat(methodType.returnType.asTypeName()).isEqualTo(returnType)
assertThat(methodType.parameterTypes.map { it.asTypeName() })
.containsExactly(*parameterTypes)
.inOrder()
}
@Test
fun overriddenGenericConstructor() {
runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
"""
package test
class ConcreteClass(foo: Foo): AbstractClass<Foo>(foo) {}
abstract class AbstractClass<T>(t: T)
class Foo
""".trimIndent()
)
)
) { invocation ->
val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
val fooType = invocation.processingEnv.requireType("test.Foo")
fun checkConstructorParameters(
typeElement: XTypeElement,
expectedParameters: Array<XTypeName>
) {
assertThat(typeElement.getConstructors()).hasSize(1)
val constructor = typeElement.getConstructors()[0]
assertThat(constructor.parameters.map { it.type.asTypeName() })
.containsExactly(*expectedParameters)
.inOrder()
}
checkConstructorParameters(
typeElement = abstractClass,
expectedParameters = arrayOf(
createXTypeVariableName("T")
)
)
checkConstructorParameters(
typeElement = concreteClass,
expectedParameters = arrayOf(fooType.asTypeName())
)
}
}
@Test
fun inheritedGenericField() {
runTest(
sources = listOf(
Source.kotlin(
"test.ConcreteClass.kt",
"""
package test
class ConcreteClass: AbstractClass<Foo>()
abstract class AbstractClass<T> {
val field: T = TODO()
}
class Foo
""".trimIndent()
)
)
) { invocation ->
val concreteClass = invocation.processingEnv.requireTypeElement("test.ConcreteClass")
val abstractClass = invocation.processingEnv.requireTypeElement("test.AbstractClass")
val concreteClassField = concreteClass.getField("field")
val abstractClassField = abstractClass.getField("field")
val fooType = invocation.processingEnv.requireType("test.Foo")
fun checkFieldElement(field: XFieldElement) {
assertThat(field.name).isEqualTo("field")
assertThat(field.type.asTypeName().java).isEqualTo(JTypeVariableName.get("T"))
assertThat(field.asMemberOf(abstractClass.type).asTypeName().java)
.isEqualTo(JTypeVariableName.get("T"))
assertThat(field.asMemberOf(concreteClass.type).asTypeName().java)
.isEqualTo(fooType.asTypeName().java)
if (invocation.isKsp) {
assertThat(field.type.asTypeName().kotlin).isEqualTo(KTypeVariableName("T"))
assertThat(field.asMemberOf(abstractClass.type).asTypeName().kotlin)
.isEqualTo(KTypeVariableName("T"))
assertThat(field.asMemberOf(concreteClass.type).asTypeName().kotlin)
.isEqualTo(fooType.asTypeName().kotlin)
}
}
assertThat(concreteClassField).isEqualTo(abstractClassField)
checkFieldElement(abstractClassField)
checkFieldElement(concreteClassField)
}
}
@Test
fun internalModifier() {
runTest(
sources = listOf(
Source.kotlin(
"test.Foo.kt",
"""
package test
internal class InternalClass internal constructor() {
internal val valField: String = TODO()
internal var varField: String = TODO()
internal fun method(): String = TODO()
internal lateinit var lateinitVarField: String
}
class PublicClass constructor() {
val valField: String = TODO()
var varField: String = TODO()
fun method(): String = TODO()
lateinit var lateinitVarField: String
}
""".trimIndent()
)
)
) { invocation ->
// Matches by name rather than jvmName to avoid dealing with name mangling.
fun XTypeElement.getDeclaredMethod(name: String): XMethodElement {
return getDeclaredMethods().filter { it.name == name }.single()
}
val internalClass = invocation.processingEnv.requireTypeElement("test.InternalClass")
assertThat(internalClass.isInternal()).isTrue()
assertThat(internalClass.getConstructors().single().isInternal()).isTrue()
assertThat(internalClass.getDeclaredField("valField").isInternal()).isTrue()
assertThat(internalClass.getDeclaredField("varField").isInternal()).isTrue()
assertThat(internalClass.getDeclaredField("lateinitVarField").isInternal()).isTrue()
assertThat(internalClass.getDeclaredMethod("method").isInternal()).isTrue()
assertThat(internalClass.getDeclaredMethod("getValField").isInternal()).isTrue()
assertThat(internalClass.getDeclaredMethod("getVarField").isInternal()).isTrue()
assertThat(internalClass.getDeclaredMethod("getLateinitVarField").isInternal()).isTrue()
assertThat(internalClass.getDeclaredMethod("setVarField").isInternal()).isTrue()
assertThat(internalClass.getDeclaredMethod("setLateinitVarField").isInternal()).isTrue()
val publicClass = invocation.processingEnv.requireTypeElement("test.PublicClass")
assertThat(publicClass.isInternal()).isFalse()
assertThat(publicClass.getConstructors().single().isInternal()).isFalse()
assertThat(publicClass.getDeclaredField("valField").isInternal()).isFalse()
assertThat(publicClass.getDeclaredField("varField").isInternal()).isFalse()
assertThat(publicClass.getDeclaredField("lateinitVarField").isInternal()).isFalse()
assertThat(publicClass.getDeclaredMethod("method").isInternal()).isFalse()
assertThat(publicClass.getDeclaredMethod("getValField").isInternal()).isFalse()
assertThat(publicClass.getDeclaredMethod("getVarField").isInternal()).isFalse()
assertThat(publicClass.getDeclaredMethod("getLateinitVarField").isInternal()).isFalse()
assertThat(publicClass.getDeclaredMethod("setVarField").isInternal()).isFalse()
assertThat(publicClass.getDeclaredMethod("setLateinitVarField").isInternal()).isFalse()
}
}
@Test
fun sameMethodNameOrder() {
runTest(
sources = listOf(
Source.kotlin(
"test.Foo.kt",
"""
package test
class Foo<T1: Bar, T2: Baz> {
fun method(): String = TODO()
fun method(param: String): String = TODO()
fun method(param: Any): String = TODO()
fun method(param: T1): T2 = TODO()
fun method(param: T2): T1 = TODO()
fun <U1: Baz, U2> method(param1: U1, param2: U2) {}
fun <U1: Baz, U2: U1> method(param1: U1, param2: U2): T1 = TODO()
}
interface Bar
interface Baz
""".trimIndent()
)
)
) { invocation ->
val foo = invocation.processingEnv.requireTypeElement("test.Foo")
assertThat(foo.getDeclaredMethods().map { it.jvmDescriptor }.toList())
.containsExactly(
"method()Ljava/lang/String;",
"method(Ljava/lang/String;)Ljava/lang/String;",
"method(Ljava/lang/Object;)Ljava/lang/String;",
"method(Ltest/Bar;)Ltest/Baz;",
"method(Ltest/Baz;)Ltest/Bar;",
"method(Ltest/Baz;Ljava/lang/Object;)V",
"method(Ltest/Baz;Ltest/Baz;)Ltest/Bar;"
).inOrder()
}
}
@Test
fun jvmDescriptors() {
runTest(
sources = listOf(
Source.kotlin(
"test.Foo.kt",
"""
package test
class Foo<T1: Bar, T2: Baz> {
val field1: String = TODO()
var field2: String? = TODO()
val field3: T1 = TODO()
fun method(): String = TODO()
fun method(param: String): String = TODO()
fun method(param: Any): String = TODO()
fun method(param: T1): T2 = TODO()
}
interface Bar
interface Baz
""".trimIndent()
)
)
) { invocation ->
val foo = invocation.processingEnv.requireTypeElement("test.Foo")
assertThat(foo.getEnclosedElements().map {
when {
it.isField() -> it.jvmDescriptor
it.isMethod() -> it.jvmDescriptor
it.isConstructor() -> it.jvmDescriptor
else -> error("Unsupported element to describe.")
}
}.toList())
.containsExactly(
"<init>()V",
"field1:Ljava/lang/String;",
"field2:Ljava/lang/String;",
"field3:Ltest/Bar;",
"getField1()Ljava/lang/String;",
"getField2()Ljava/lang/String;",
"setField2(Ljava/lang/String;)V",
"getField3()Ltest/Bar;",
"method()Ljava/lang/String;",
"method(Ljava/lang/String;)Ljava/lang/String;",
"method(Ljava/lang/Object;)Ljava/lang/String;",
"method(Ltest/Bar;)Ltest/Baz;",
)
}
}
/**
* it is good to exclude methods coming from Object when testing as they differ between KSP
* and KAPT but irrelevant for Room.
*/
private fun XTestInvocation.objectMethodNames() = processingEnv
.requireTypeElement("java.lang.Object")
.getAllMethods()
.jvmNames()
private fun Sequence<XMethodElement>.jvmNames() = map {
it.jvmName
}.toList()
private fun List<XMethodElement>.jvmNames() = map {
it.jvmName
}.toList()
private fun XMethodElement.signature(owner: XType): String {
val methodType = this.asMemberOf(owner)
val params = methodType.parameterTypes.joinToString(",") {
it.asTypeName().java.toString()
}
return "$jvmName($params):${returnType.asTypeName().java}"
}
private fun XTestInvocation.nonObjectMethodSignatures(typeElement: XTypeElement): List<String> =
typeElement.getAllMethods()
.filterNot { it.jvmName in objectMethodNames() }
.map { it.signature(typeElement.type) }.toList()
companion object {
@JvmStatic
@Parameterized.Parameters(name = "isPreCompiled_{0}")
fun params(): List<Array<Any>> {
return listOf(arrayOf(false), arrayOf(true))
}
}
}