blob: ba831108039bd83732f2d66d5229e89017f674b4 [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.IrGetValue
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.junit.Test
class ComposerParamTransformTests : ComposeIrTransformTest() {
private fun composerParam(
@Language("kotlin")
source: String,
expectedTransformed: String,
validator: (element: IrElement) -> Unit = { },
dumpTree: Boolean = false
) = verifyComposeIrTransform(
"""
@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(),
expectedTransformed,
"""
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
}
""",
"""
val bar: Int
@Composable @JvmName(name = "getBar")
get() {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C:Test.kt#2487m")
val tmp0 = 123
%composer.endReplaceableGroup()
return tmp0
}
@NonRestartableComposable
@Composable
fun Example(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(Example)<bar>:Test.kt#2487m")
bar
%composer.endReplaceableGroup()
}
"""
)
@Test
fun testAbstractComposable(): Unit = composerParam(
"""
abstract class BaseFoo {
@NonRestartableComposable
@Composable
abstract fun bar()
}
class FooImpl : BaseFoo() {
@NonRestartableComposable
@Composable
override fun bar() {}
}
""",
"""
@StabilityInferred(parameters = 0)
abstract class BaseFoo {
@NonRestartableComposable
@Composable
abstract fun bar(%composer: Composer?, %changed: Int)
static val %stable: Int = 0
}
@StabilityInferred(parameters = 0)
class FooImpl : BaseFoo {
@NonRestartableComposable
@Composable
override fun bar(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(bar):Test.kt#2487m")
%composer.endReplaceableGroup()
}
static val %stable: Int = 0
}
"""
)
@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()
}
""",
"""
@NonRestartableComposable
@Composable
fun Wat(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(Wat):Test.kt#2487m")
%composer.endReplaceableGroup()
}
@NonRestartableComposable
@Composable
fun Foo(x: Int, %composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(Foo)<Wat()>,<goo()>,<baz()>:Test.kt#2487m")
Wat(%composer, 0)
@NonRestartableComposable
@Composable
fun goo(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(goo)<Wat()>:Test.kt#2487m")
Wat(%composer, 0)
%composer.endReplaceableGroup()
}
class Bar {
@NonRestartableComposable
@Composable
fun baz(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(baz)<Wat()>:Test.kt#2487m")
Wat(%composer, 0)
%composer.endReplaceableGroup()
}
}
goo(%composer, 0)
Bar().baz(%composer, 0)
%composer.endReplaceableGroup()
}
"""
)
@Test
fun testVarargWithNoArgs(): Unit = composerParam(
"""
@Composable
fun VarArgsFirst(vararg foo: Any?) {
println(foo)
}
@Composable
fun VarArgsCaller() {
VarArgsFirst()
}
""",
"""
@Composable
fun VarArgsFirst(foo: Array<out Any?>, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(VarArgsFirst):Test.kt#2487m")
println(foo)
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
VarArgsFirst(*foo, %composer, %changed or 0b0001)
}
}
@Composable
fun VarArgsCaller(%composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(VarArgsCaller)<VarArg...>:Test.kt#2487m")
if (%changed !== 0 || !%composer.skipping) {
VarArgsFirst(
%composer = %composer,
%changed = 8
)
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
VarArgsCaller(%composer, %changed or 0b0001)
}
}
"""
)
@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()
}
}
}
""",
"""
fun A() { }
val b: Int
get() {
return 123
}
fun C(x: Int) {
var x = 0
x++
class D {
fun E() {
A()
}
val F: Int
get() {
return 123
}
}
val g = object {
fun H() { }
}
}
fun I(block: Function0<Unit>) {
block()
}
fun J() {
I {
I {
A()
}
}
}
"""
)
@Test
fun testCircularCall(): Unit = composerParam(
"""
@NonRestartableComposable
@Composable fun Example() {
Example()
}
""",
"""
@NonRestartableComposable
@Composable
fun Example(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(Example)<Exampl...>:Test.kt#2487m")
Example(%composer, 0)
%composer.endReplaceableGroup()
}
"""
)
@Test
fun testInlineCall(): Unit = composerParam(
"""
@Composable inline fun Example(content: @Composable () -> Unit) {
content()
}
@NonRestartableComposable
@Composable fun Test() {
Example {}
}
""",
"""
@Composable
@ComposableInferredTarget(scheme = "[0[0]]")
fun Example(content: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(Example)<conten...>:Test.kt#2487m")
content(%composer, 0b1110 and %changed)
%composer.endReplaceableGroup()
}
@NonRestartableComposable
@Composable
fun Test(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(Test)<Exampl...>:Test.kt#2487m")
Example({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C:Test.kt#2487m")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
}
%composer.endReplaceableGroup()
}, %composer, 0)
%composer.endReplaceableGroup()
}
"""
)
@Test
fun testDexNaming(): Unit = composerParam(
"""
val myProperty: () -> Unit @Composable get() {
return { }
}
""",
"""
val myProperty: Function0<Unit>
@Composable @JvmName(name = "getMyProperty")
get() {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C:Test.kt#2487m")
val tmp0 = {
}
%composer.endReplaceableGroup()
return tmp0
}
"""
)
@Test
fun testInnerClass(): Unit = composerParam(
"""
interface A {
fun b() {}
}
class C {
val foo = 1
inner class D : A {
override fun b() {
print(foo)
}
}
}
""",
"""
interface A {
open fun b() { }
}
@StabilityInferred(parameters = 0)
class C {
val foo: Int = 1
inner class D : A {
override fun b() {
print(foo)
}
}
static val %stable: Int = 0
}
"""
)
@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")
}
}
}
""",
"""
@Composable
@ComposableInferredTarget(scheme = "[0[0]]")
fun Wrapper(block: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Wrapper)<block(...>:Test.kt#2487m")
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(block)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
block(%composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
Wrapper(block, %composer, %changed or 0b0001)
}
}
@Composable
fun Leaf(text: String, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Leaf):Test.kt#2487m")
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(text)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
used(text)
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
Leaf(text, %composer, %changed or 0b0001)
}
}
@Composable
fun Test(value: Int, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test):Test.kt#2487m")
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(value)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
%composer.startMovableGroup(<>, value)
sourceInformation(%composer, "<Wrappe...>")
Wrapper(composableLambda(%composer, <>, true) { %composer: Composer?, %changed: Int ->
sourceInformation(%composer, "C<Leaf("...>:Test.kt#2487m")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Leaf("Value %value", %composer, 0)
} else {
%composer.skipToGroupEnd()
}
}, %composer, 0b0110)
%composer.endMovableGroup()
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
Test(value, %composer, %changed or 0b0001)
}
}
""",
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()
}
""",
"""
@Composable
@ComposableInferredTarget(scheme = "[0[0]]")
fun composeVector(composable: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(composeVector)<emit>:Test.kt#2487m")
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(composable)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
emit({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<emit>:Test.kt#2487m")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
emit({ %composer: Composer?, %changed: Int ->
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C<compos...>:Test.kt#2487m")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
composable(%composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
}
%composer.endReplaceableGroup()
}, %composer, 0)
} else {
%composer.skipToGroupEnd()
}
%composer.endReplaceableGroup()
}, %composer, 0)
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
composeVector(composable, %composer, %changed or 0b0001)
}
}
@Composable
@ComposableInferredTarget(scheme = "[0[0]]")
fun emit(composable: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>)
sourceInformation(%composer, "C(emit)<compos...>:Test.kt#2487m")
composable(%composer, 0b1110 and %changed)
%composer.endReplaceableGroup()
}
""".trimIndent()
)
}
}