blob: cd9027b0b4e0addecacec5aed0bca72dfbbef440 [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.compose.compiler.plugins.kotlin
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrGetValue
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.junit.Assert.assertEquals
import org.junit.Test
class ComposerParamTransformTests(useFir: Boolean) : AbstractIrTransformTest(useFir) {
private fun composerParam(
@Language("kotlin")
source: String,
validator: (element: IrElement) -> Unit = { },
dumpTree: Boolean = false
) = verifyGoldenComposeIrTransform(
"""
@file:OptIn(
InternalComposeApi::class,
)
package test
import androidx.compose.runtime.InternalComposeApi
import androidx.compose.runtime.ComposeCompilerApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
$source
""".trimIndent(),
"""
package test
fun used(x: Any?) {}
""",
validator,
dumpTree
)
@Test
fun testCallingProperties(): Unit = composerParam(
"""
val bar: Int @Composable get() { return 123 }
@NonRestartableComposable @Composable fun Example() {
bar
}
"""
)
@Test
fun testAbstractComposable(): Unit = composerParam(
"""
abstract class BaseFoo {
@NonRestartableComposable
@Composable
abstract fun bar()
}
class FooImpl : BaseFoo() {
@NonRestartableComposable
@Composable
override fun bar() {}
}
"""
)
@Test
fun testLocalClassAndObjectLiterals(): Unit = composerParam(
"""
@NonRestartableComposable
@Composable
fun Wat() {}
@NonRestartableComposable
@Composable
fun Foo(x: Int) {
Wat()
@NonRestartableComposable
@Composable fun goo() { Wat() }
class Bar {
@NonRestartableComposable
@Composable fun baz() { Wat() }
}
goo()
Bar().baz()
}
"""
)
@Test
fun testVarargWithNoArgs(): Unit = composerParam(
"""
@Composable
fun VarArgsFirst(vararg foo: Any?) {
println(foo)
}
@Composable
fun VarArgsCaller() {
VarArgsFirst()
}
"""
)
// Regression test for b/286132194
@Test
fun testStableVarargParams(): Unit = composerParam(
"""
@Composable
fun B(vararg values: Int) {
print(values)
}
@NonRestartableComposable
@Composable
fun Test() {
B(0, 1, 2, 3)
}
"""
)
@Test
fun testNonComposableCode(): Unit = composerParam(
"""
fun A() {}
val b: Int get() = 123
fun C(x: Int) {
var x = 0
x++
class D {
fun E() { A() }
val F: Int get() = 123
}
val g = object { fun H() {} }
}
fun I(block: () -> Unit) { block() }
fun J() {
I {
I {
A()
}
}
}
"""
)
@Test
fun testCircularCall(): Unit = composerParam(
"""
@NonRestartableComposable
@Composable fun Example() {
Example()
}
"""
)
@Test
fun testInlineCall(): Unit = composerParam(
"""
@Composable inline fun Example(content: @Composable () -> Unit) {
content()
}
@NonRestartableComposable
@Composable fun Test() {
Example {}
}
"""
)
@Test
fun testDexNaming(): Unit = composerParam(
"""
val myProperty: () -> Unit @Composable get() {
return { }
}
"""
)
@Test
fun testInnerClass(): Unit = composerParam(
"""
interface A {
fun b() {}
}
class C {
val foo = 1
inner class D : A {
override fun b() {
print(foo)
}
}
}
"""
)
@Test
fun testKeyCall() {
composerParam(
"""
import androidx.compose.runtime.key
@Composable
fun Wrapper(block: @Composable () -> Unit) {
block()
}
@Composable
fun Leaf(text: String) {
used(text)
}
@Composable
fun Test(value: Int) {
key(value) {
Wrapper {
Leaf("Value ${'$'}value")
}
}
}
""",
validator = { element ->
// Validate that no composers are captured by nested lambdas
var currentComposer: IrValueParameter? = null
element.accept(
object : IrElementVisitorVoid {
override fun visitSimpleFunction(declaration: IrSimpleFunction) {
val composer = declaration.valueParameters.firstOrNull {
it.name == KtxNameConventions.COMPOSER_PARAMETER
}
val oldComposer = currentComposer
if (composer != null) currentComposer = composer
super.visitSimpleFunction(declaration)
currentComposer = oldComposer
}
override fun visitElement(element: IrElement) {
element.acceptChildren(this, null)
}
override fun visitGetValue(expression: IrGetValue) {
super.visitGetValue(expression)
val value = expression.symbol.owner
if (
value is IrValueParameter && value.name ==
KtxNameConventions.COMPOSER_PARAMETER
) {
assertEquals(
"Composer unexpectedly captured",
currentComposer,
value
)
}
}
},
null
)
}
)
}
@Test
fun testComposableNestedCall() {
composerParam(
"""
@Composable
fun composeVector(
composable: @Composable () -> Unit
) {
emit {
emit {
composable()
}
}
}
@Composable
inline fun emit(composable: @Composable () -> Unit) {
composable()
}
"""
)
}
@Test
fun testDelegateCall() {
composerParam(
"""
import kotlin.reflect.KProperty
class Foo
@Composable
operator fun Foo.getValue(thisObj: Any?, property: KProperty<*>): Foo = this
class FooDelegate {
@Composable
operator fun getValue(thisObj: Any?, property: KProperty<*>): FooDelegate = this
}
class Bar {
@get:Composable
val foo by Foo()
}
@Composable
fun test() {
val foo by Foo()
val fooDelegate by FooDelegate()
val bar = Bar()
println(foo)
println(fooDelegate)
println(bar.foo)
}
""",
)
}
@Test
fun testUnstableDelegateCall() = composerParam(
"""
import kotlin.reflect.KProperty
class Foo {
var unstableField: Int = 0
}
@Composable
inline operator fun Foo.getValue(thisObj: Any?, property: KProperty<*>): Foo = this
@Composable
fun test() {
val foo by Foo()
println(foo)
}
"""
)
@Test
fun testStableDelegateCall() = composerParam(
"""
import kotlin.reflect.KProperty
class Foo
@Composable
inline operator fun Foo.getValue(thisObj: Any?, property: KProperty<*>): Foo = this
@Composable
fun test(foo: Foo) {
val delegated by foo
used(delegated)
}
"""
)
@Test
fun validateNoComposableFunctionSymbolCalls() = composerParam(
source = """
fun abc0(l: @Composable () -> Unit) {
val hc = l.hashCode()
}
fun abc1(l: @Composable (String) -> Unit) {
val hc = l.hashCode()
}
fun abc2(l: @Composable (String, Int) -> Unit) {
val hc = l.hashCode()
}
fun abc3(
l: @Composable (Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any, Any) -> Any
) {
val hc = l.hashCode()
}
""".trimIndent(),
validator = {
val expectedArity = listOf(2, 3, 4, 15)
var i = 0 // to iterate over `hashCode` calls
it.acceptChildrenVoid(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitCall(expression: IrCall) {
if (expression.symbol.owner.name.asString() == "hashCode") {
assertEquals(
"kotlin.Function${expectedArity[i]}.hashCode",
expression.symbol.owner.fqNameForIrSerialization.asString())
i++
}
}
})
}
)
@Test
fun validateNoComposableFunctionReferencesInOverriddenSymbols() =
verifyGoldenCrossModuleComposeIrTransform(
dependencySource = """
package dependency
import androidx.compose.runtime.Composable
interface Content {
fun setContent(c: @Composable () -> Unit)
}
""".trimIndent(),
source = """
package test
import androidx.compose.runtime.Composable
import dependency.Content
class ContentImpl : Content {
override fun setContent(c: @Composable () -> Unit) {}
}
""".trimIndent(),
validator = {
it.acceptChildrenVoid(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
private val targetFqName = "test.ContentImpl.setContent"
override fun visitSimpleFunction(declaration: IrSimpleFunction) {
if (declaration.fqNameForIrSerialization.asString() == targetFqName) {
assertEquals(1, declaration.overriddenSymbols.size)
val firstParameterOfOverridden =
declaration.overriddenSymbols.first().owner.valueParameters.first()
.takeIf { it.name.asString() == "c" }!!
assertEquals(
"kotlin.Function2",
firstParameterOfOverridden.type.classFqName?.asString()
)
}
}
})
}
)
@Test
fun validateNoComposableFunctionReferencesInCalleeOverriddenSymbols() =
verifyGoldenCrossModuleComposeIrTransform(
dependencySource = """
package dependency
import androidx.compose.runtime.Composable
interface Content {
fun setContent(c: @Composable () -> Unit = {})
}
class ContentImpl : Content {
override fun setContent(c: @Composable () -> Unit) {}
}
""".trimIndent(),
source = """
package test
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import dependency.ContentImpl
@Composable
@NonRestartableComposable
fun Foo() {
ContentImpl().setContent()
}
""".trimIndent(),
validator = {
it.acceptChildrenVoid(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
private val targetFqName = "dependency.ContentImpl.setContent"
override fun visitCall(expression: IrCall) {
val callee = expression.symbol.owner
if (callee.fqNameForIrSerialization.asString() == targetFqName) {
val firstParameterOfOverridden =
callee.overriddenSymbols.first().owner.valueParameters.first()
.takeIf { it.name.asString() == "c" }!!
assertEquals(
"kotlin.Function2",
firstParameterOfOverridden.type.classFqName?.asString()
)
}
super.visitCall(expression)
}
})
}
)
}