blob: 40eadd32f4aa74a103435dc25a9a263ab514fa0b [file] [log] [blame]
/*
* Copyright 2019 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.compose.compiler.plugins.kotlin
import android.widget.TextView
import androidx.compose.runtime.Composer
import com.intellij.openapi.util.io.FileUtil
import org.jetbrains.kotlin.backend.common.output.OutputFile
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.io.File
import java.net.URLClassLoader
@RunWith(RobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
minSdk = 23,
maxSdk = 23
)
class KtxCrossModuleTests : AbstractCodegenTest() {
@Test
fun testInlineFunctionDefaultArgument(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/library.kt" to """
package x
import androidx.compose.runtime.Composable
@Composable
inline fun f(x: () -> Unit = { println("default") }) {
x()
}
""".trimIndent()
),
"Main" to mapOf(
"y/User.kt" to """
package y
import x.f
import androidx.compose.runtime.Composable
@Composable
fun g() {
f {
println("non-default")
}
}
""".trimIndent()
)
)
)
}
@Test
fun testInlineFunctionDefaultArgument2(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/library.kt" to """
package x
import androidx.compose.runtime.Composable
@Composable
inline fun f(x: () -> Unit = { println("default") }) {
x()
}
""".trimIndent()
),
"Main" to mapOf(
"y/User.kt" to """
package y
import x.f
import androidx.compose.runtime.Composable
@Composable
fun g() {
f()
}
""".trimIndent()
)
)
)
}
@Test
fun testAccessibilityBridgeGeneration(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/I.kt" to """
package x
import androidx.compose.runtime.Composable
@Composable fun bar(arg: @Composable () -> Unit) {
arg()
}
""".trimIndent()
),
"Main" to mapOf(
"y/User.kt" to """
package y
import x.bar
import androidx.compose.runtime.Composable
@Composable fun baz() {
bar {
foo()
}
}
@Composable private fun foo() { }
""".trimIndent()
)
)
) {
// Check that there is only one method declaration for access$foo.
// We used to introduce more symbols for the same function leading
// to multiple identical methods in the output.
// In the dump, $ is mapped to %.
val declaration = "synthetic access%foo"
val occurrences = it.windowed(declaration.length) { candidate ->
if (candidate.equals(declaration))
1
else
0
}.sum()
assert(occurrences == 1)
}
}
@Test
fun testInlineClassCrossModule(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/I.kt" to """
package x
inline class I(val i: Int) {
val prop
get() = i + 1
}
""".trimIndent()
),
"Main" to mapOf(
"y/User.kt" to """
package y
import x.I
inline class J(val j: Int)
fun foo(): Int = I(42).i + J(23).j + I(1).prop
""".trimIndent()
)
)
) {
// If the output contains getI-impl, the cross-module inline class
// was incorrectly compiled and the getter was not removed. This
// happens if the relationship between the getter and the corresponding
// property is broken by the compiler.
assert(!it.contains("getI-impl"))
// Check that inline classes where optimized to integers.
assert(it.contains("INVOKESTATIC x/I.constructor-impl (I)I"))
assert(it.contains("INVOKESTATIC y/J.constructor-impl (I)I"))
// Check that the inline class prop getter is correctly mangled.
assert(it.contains("INVOKESTATIC x/I.getProp-impl (I)I"))
}
}
@Test
fun testInlineClassOverloading(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/A.kt" to """
package x
import androidx.compose.runtime.Composable
inline class I(val i: Int)
inline class J(val j: Int)
@Composable fun foo(i: I) { }
@Composable fun foo(j: J) { }
""".trimIndent()
),
"Main" to mapOf(
"y/B.kt" to """
package y
import androidx.compose.runtime.Composable
import x.*
@Composable fun bar(k: Int) {
foo(I(k))
foo(J(k))
}
"""
)
)
) {
// Check that the composable functions were properly mangled
assert(
it.contains(
"public final static foo-4e73Vzs(ILandroidx/compose/runtime/Composer;I)V"
)
)
assert(
it.contains(
"public final static foo-YK1ovzU(ILandroidx/compose/runtime/Composer;I)V"
)
)
// Check that we didn't leave any references to the original name, which probably
// leads to a compile error.
assert(!it.contains("foo("))
}
}
@Test
fun testFunInterfaceWithInlineClass(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/A.kt" to """
package x
inline class A(val value: Int)
fun interface B {
fun method(a: A)
}
""".trimIndent()
),
"Main" to mapOf(
"y/B.kt" to """
package y
import x.*
val b = B { }
"""
)
)
) {
assert(it.contains("public abstract method-C8LvVsQ(I)V"))
assert(it.contains("public final method-C8LvVsQ(I)V"))
assert(!it.contains("public final method(I)V"))
}
}
@Test
fun testParentNotInitializedBug(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/Base.kt" to """
package x
import androidx.compose.runtime.Composable
class Foo
abstract class Base {
@Composable abstract fun content(a: Foo)
}
"""
),
"Main" to mapOf(
"b/Extends.kt" to """
package b
import androidx.compose.runtime.Composable
import x.Base
import x.Foo
abstract class Extends : Base() {
@Composable
override fun content(a: Foo) {
}
}
"""
)
)
)
}
@Test
fun testConstCrossModule(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/A.kt" to """
package x
const val MyConstant: String = ""
"""
),
"Main" to mapOf(
"b/B.kt" to """
package b
import x.MyConstant
fun Test(foo: String = MyConstant) {
print(foo)
}
"""
)
)
) {
assert(it.contains("LDC \"\""))
assert(!it.contains("INVOKESTATIC x/AKt.getMyConstant"))
}
}
@Test
fun testNonCrossinlineComposable(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/A.kt" to """
package x
import androidx.compose.runtime.Composable
@Composable
inline fun <T> key(
block: @Composable () -> T
): T = block()
"""
),
"Main" to mapOf(
"b/B.kt" to """
package b
import androidx.compose.runtime.Composable
import x.key
@Composable fun Test() {
key { }
}
"""
)
)
)
}
@Test
fun testNonCrossinlineComposableNoGenerics(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/A.kt" to """
package x
import androidx.compose.runtime.Composable
@Composable
inline fun key(
@Suppress("UNUSED_PARAMETER")
v1: Int,
block: @Composable () -> Int
): Int = block()
"""
),
"Main" to mapOf(
"b/B.kt" to """
package b
import androidx.compose.runtime.Composable
import x.key
@Composable fun Test() {
key(123) { 456 }
}
"""
)
)
)
}
@Test
fun testRemappedTypes(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/A.kt" to """
package x
class A {
fun makeA(): A { return A() }
fun makeB(): B { return B() }
class B() {
}
}
"""
),
"Main" to mapOf(
"b/B.kt" to """
package b
import x.A
class C {
fun useAB() {
val a = A()
a.makeA()
a.makeB()
val b = A.B()
}
}
"""
)
)
)
}
@Test
fun testInlineIssue(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/C.kt" to """
fun ghi() {
abc {}
}
""",
"x/A.kt" to """
inline fun abc(fn: () -> Unit) {
fn()
}
""",
"x/B.kt" to """
fun def() {
abc {}
}
"""
),
"Main" to mapOf(
"b/B.kt" to """
"""
)
)
)
}
@Test
fun testInlineComposableProperty(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/A.kt" to """
package x
import androidx.compose.runtime.Composable
class Foo {
val value: Int @Composable get() = 123
}
"""
),
"Main" to mapOf(
"b/B.kt" to """
package b
import androidx.compose.runtime.Composable
import x.Foo
val foo = Foo()
@Composable fun Test() {
print(foo.value)
}
"""
)
)
)
}
@Test
fun testNestedInlineIssue(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/C.kt" to """
fun ghi() {
abc {
abc {
}
}
}
""",
"x/A.kt" to """
inline fun abc(fn: () -> Unit) {
fn()
}
""",
"x/B.kt" to """
fun def() {
abc {
abc {
}
}
}
"""
),
"Main" to mapOf(
"b/B.kt" to """
"""
)
)
)
}
@Test
fun testComposerIntrinsicInline(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/C.kt" to """
import androidx.compose.runtime.Composable
@Composable
fun ghi() {
val x = abc()
print(x)
}
""",
"x/A.kt" to """
import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentComposer
@Composable
inline fun abc(): Any {
return currentComposer
}
""",
"x/B.kt" to """
import androidx.compose.runtime.Composable
@Composable
fun def() {
val x = abc()
print(x)
}
"""
),
"Main" to mapOf(
"b/B.kt" to """
"""
)
)
)
}
@Test
fun testComposableOrderIssue(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"C.kt" to """
import androidx.compose.runtime.*
@Composable
fun b() {
a()
}
""",
"A.kt" to """
import androidx.compose.runtime.*
@Composable
fun a() {
}
""",
"B.kt" to """
import androidx.compose.runtime.*
@Composable
fun c() {
a()
}
"""
),
"Main" to mapOf(
"b/B.kt" to """
"""
)
)
)
}
@Test
fun testSimpleXModuleCall(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"a/A.kt" to """
package a
import androidx.compose.runtime.*
@Composable
fun FromA() {}
"""
),
"Main" to mapOf(
"b/B.kt" to """
package b
import a.FromA
import androidx.compose.runtime.*
@Composable
fun FromB() {
FromA()
}
"""
)
)
)
}
@Test
fun testJvmFieldIssue(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/C.kt" to """
fun Test2() {
bar = 10
print(bar)
}
""",
"x/A.kt" to """
@JvmField var bar: Int = 0
""",
"x/B.kt" to """
fun Test() {
bar = 10
print(bar)
}
"""
),
"Main" to mapOf(
"b/B.kt" to """
"""
)
)
)
}
@Test
fun testInstanceXModuleCall(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"a/Foo.kt" to """
package a
import androidx.compose.runtime.*
class Foo {
@Composable
fun FromA() {}
}
"""
),
"Main" to mapOf(
"B.kt" to """
import a.Foo
import androidx.compose.runtime.*
@Composable
fun FromB() {
Foo().FromA()
}
"""
)
)
)
}
@Test
fun testXModuleProperty(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"a/Foo.kt" to """
package a
import androidx.compose.runtime.*
val foo: Int @Composable get() { return 123 }
"""
),
"Main" to mapOf(
"B.kt" to """
import a.foo
import androidx.compose.runtime.*
@Composable
fun FromB() {
foo
}
"""
)
)
)
}
@Test
fun testXModuleInterface(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"a/Foo.kt" to """
package a
import androidx.compose.runtime.*
interface Foo {
@Composable fun foo()
}
"""
),
"Main" to mapOf(
"B.kt" to """
import a.Foo
import androidx.compose.runtime.*
class B : Foo {
@Composable override fun foo() {}
}
@Composable fun Example(inst: Foo) {
B().foo()
inst.foo()
}
"""
)
)
)
}
@Test
fun testXModuleComposableProperty(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"a/Foo.kt" to """
package a
import androidx.compose.runtime.*
val foo: () -> Unit
@Composable get() = {}
"""
),
"Main" to mapOf(
"B.kt" to """
import a.foo
import androidx.compose.runtime.*
@Composable fun Example() {
val bar = foo
bar()
}
"""
)
)
)
}
@Test
fun testXModuleCtorComposableParam(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"a/Foo.kt" to """
package a
import androidx.compose.runtime.*
class Foo(val bar: @Composable () -> Unit)
"""
),
"Main" to mapOf(
"B.kt" to """
import a.Foo
import androidx.compose.runtime.*
@Composable fun Example(bar: @Composable () -> Unit) {
val foo = Foo(bar)
}
"""
)
)
)
}
@Ignore("b/171801506")
@Test
fun testCrossModule_SimpleComposition(): Unit = ensureSetup {
val tvId = 29
compose(
"TestF",
mapOf(
"library module" to mapOf(
"my/test/lib/InternalComp.kt" to """
package my.test.lib
import androidx.compose.runtime.*
@Composable fun InternalComp(block: @Composable () -> Unit) {
block()
}
"""
),
"Main" to mapOf(
"my/test/app/Main.kt" to """
package my.test.app
import android.widget.*
import androidx.compose.runtime.*
import androidx.compose.ui.viewinterop.emitView
import my.test.lib.*
var bar = 0
var scope: RecomposeScope? = null
class TestF {
@Composable
fun compose() {
scope = currentRecomposeScope
Foo(bar)
}
fun advance() {
bar++
scope?.invalidate()
}
}
@Composable
fun Foo(bar: Int) {
InternalComp {
emitView(::TextView) {
it.text="${'$'}bar"
it.id=$tvId
}
}
}
"""
)
)
).then { activity ->
val tv = activity.findViewById(tvId) as TextView
assertEquals("0", tv.text)
}.then { activity ->
val tv = activity.findViewById(tvId) as TextView
assertEquals("1", tv.text)
}
}
/**
* Test for b/169071070
*/
@Test
fun testCrossModule_ComposableInterfaceFunctionWithInlineClasses(): Unit = ensureSetup {
compile(
mapOf(
"library module" to mapOf(
"x/Library.kt" to """
package x
import androidx.compose.runtime.Composable
inline class InlineClass(val value: Float)
interface Foo {
@Composable
fun apply(value: InlineClass)
}
""".trimIndent()
),
"Main" to mapOf(
"y/Impl.kt" to """
package y
import androidx.compose.runtime.Composable
import x.Foo
import x.InlineClass
object Bar : Foo {
@Composable
override fun apply(value: InlineClass) {}
}
""".trimIndent()
)
)
)
}
@Test
fun testAnnotationInferenceAcrossModules() = ensureSetup {
compile(
mapOf(
"Base" to mapOf(
"base/Library.kt" to """
package base
import androidx.compose.runtime.*
@Composable
@ComposableTarget("UI")
fun Text(text: String) { }
@Composable
@ComposableTarget("UI")
fun Row(content: @Composable @ComposableTarget("UI") () -> Unit) {
content()
}
"""
),
"Client" to mapOf(
"client/Library.kt" to """
package client
import androidx.compose.runtime.*
import base.*
@Composable
fun Labeled(text: String, content: @Composable () -> Unit) {
Text(text)
Row { content() }
}
"""
),
"Main" to mapOf(
"Main.kt" to """
package main
import androidx.compose.runtime.*
import client.*
@Composable
fun Main() {
Labeled("test") { }
}
"""
)
)
) {
assert(it.contains("[UI[UI]]", false)) {
"Layered composable didn't store the inferred composable target"
}
}
}
/**
* Test for b/221280935
*/
@Test
fun testOverriddenSymbolParentsInDefaultParameters() = ensureSetup {
compile(
mapOf(
"Base" to mapOf(
"base/Base.kt" to """
package base
import androidx.compose.runtime.Composable
open class Base {
fun f(block: (@Composable () -> Unit)? = null) {}
}
"""
),
"Main" to mapOf(
"Main.kt" to """
package main
import androidx.compose.runtime.Composable
import base.Base
class Child : Base() {
init { f {} }
}
"""
)
)
)
}
fun compile(
modules: Map<String, Map<String, String>>,
dumpClasses: Boolean = false,
validate: ((String) -> Unit)? = null
): List<OutputFile> {
val libraryClasses = (
modules.filter { it.key != "Main" }.map {
// Setup for compile
this.classFileFactory = null
this.myEnvironment = null
setUp(it.key.contains("--ktx=false"))
classLoader(it.value, dumpClasses).allGeneratedFiles.also {
// Write the files to the class directory so they can be used by the next module
// and the application
it.writeToDir(classesDirectory)
}
} + emptyList()
).reduce { acc, mutableList -> acc + mutableList }
// Setup for compile
this.classFileFactory = null
this.myEnvironment = null
setUp()
// compile the next one
val appClasses = classLoader(
modules["Main"]
?: error("No Main module specified"),
dumpClasses
).allGeneratedFiles
// Load the files looking for mainClassName
val outputFiles = (libraryClasses + appClasses).filter {
it.relativePath.endsWith(".class")
}
if (validate != null) {
validate(outputFiles.joinToString("\n") { it.asText().replace('$', '%') })
}
return outputFiles
}
fun compose(
mainClassName: String,
modules: Map<String, Map<String, String>>,
dumpClasses: Boolean = false
): RobolectricComposeTester {
val allClasses = compile(modules, dumpClasses)
val loader = URLClassLoader(emptyArray(), this.javaClass.classLoader)
val instanceClass = run {
var instanceClass: Class<*>? = null
var loadedOne = false
for (outFile in allClasses) {
val bytes = outFile.asByteArray()
val loadedClass = loadClass(loader, null, bytes)
if (loadedClass.name.endsWith(mainClassName)) instanceClass = loadedClass
loadedOne = true
}
if (!loadedOne) error("No classes loaded")
instanceClass ?: error("Could not find class $mainClassName in loaded classes")
}
val instanceOfClass = instanceClass.newInstance()
val advanceMethod = instanceClass.getMethod("advance")
val composeMethod = instanceClass.getMethod(
"compose",
Composer::class.java,
Int::class.java
)
return composeMulti({ composer, _ ->
composeMethod.invoke(instanceOfClass, composer, 1)
}) {
advanceMethod.invoke(instanceOfClass)
}
}
fun setUp(disable: Boolean = false) {
if (disable) {
this.disableIrAndKtx = true
try {
setUp()
} finally {
this.disableIrAndKtx = false
}
} else {
setUp()
}
}
override fun setUp() {
if (disableIrAndKtx) {
super.setUp()
} else {
super.setUp()
}
}
override fun setupEnvironment(environment: KotlinCoreEnvironment) {
if (!disableIrAndKtx) {
super.setupEnvironment(environment)
}
}
private var disableIrAndKtx = false
override fun updateConfiguration(configuration: CompilerConfiguration) {
super.updateConfiguration(configuration)
if (disableIrAndKtx) {
configuration.put(JVMConfigurationKeys.IR, false)
}
}
private var testLocalUnique = 0
private var classesDirectory = tmpDir(
"kotlin-${testLocalUnique++}-classes"
)
override val additionalPaths: List<File> = listOf(classesDirectory)
}
fun OutputFile.writeToDir(directory: File) =
FileUtil.writeToFile(File(directory, relativePath), asByteArray())
fun Collection<OutputFile>.writeToDir(directory: File) = forEach { it.writeToDir(directory) }