From 010a0b7971ae14ac531e28d190aac1a739559ac1 Mon Sep 17 00:00:00 2001 From: luyi Date: Thu, 27 Feb 2025 15:32:19 +0100 Subject: [PATCH] RUM-8583: Add ComposeTagTransformer to inject datadog modifier call --- dd-sdk-android-gradle-plugin/build.gradle.kts | 1 + .../plugin/kcp/ComposeTagTransformer.kt | 146 +++++++++++++++++- .../gradle/plugin/kcp/DatadogIrExtension.kt | 11 +- .../plugin/kcp/DefaultPluginContextUtils.kt | 89 +++++++++++ .../gradle/plugin/kcp/PluginContextUtils.kt | 3 + .../kcp/DefaultPluginContextUtilsTest.kt | 134 ++++++++++++++++ instrumented/build.gradle.kts | 1 + .../android/instrumented/SemanticsTest.kt | 27 +++- .../android/instrumented/TestScreen.kt | 35 ++++- samples/lib-module/build.gradle | 8 + .../datadog/kcp/compose/DatadogModifier.kt | 26 ++++ 11 files changed, 470 insertions(+), 11 deletions(-) create mode 100644 dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtils.kt create mode 100644 dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/PluginContextUtils.kt create mode 100644 dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtilsTest.kt create mode 100644 samples/lib-module/src/main/kotlin/com/datadog/kcp/compose/DatadogModifier.kt diff --git a/dd-sdk-android-gradle-plugin/build.gradle.kts b/dd-sdk-android-gradle-plugin/build.gradle.kts index 397d1f75..b0161401 100644 --- a/dd-sdk-android-gradle-plugin/build.gradle.kts +++ b/dd-sdk-android-gradle-plugin/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.okHttpMock) testImplementation(libs.androidToolsPluginGradle) testImplementation(libs.kotlinPluginGradle) + testImplementation(libs.kotlinCompilerEmbeddable) compileOnly(libs.kotlinCompilerEmbeddable) compileOnly(libs.autoServiceAnnotation) diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/ComposeTagTransformer.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/ComposeTagTransformer.kt index 104c5a8f..9c8a74d3 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/ComposeTagTransformer.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/ComposeTagTransformer.kt @@ -2,11 +2,149 @@ package com.datadog.gradle.plugin.kcp import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irGetObjectValue +import org.jetbrains.kotlin.ir.builders.irString +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.expressions.IrCall +import org.jetbrains.kotlin.ir.expressions.IrComposite +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.types.classFqName +import org.jetbrains.kotlin.ir.types.createType +import org.jetbrains.kotlin.ir.types.defaultType +import org.jetbrains.kotlin.ir.util.defaultType +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.name.SpecialNames -// TODO RUM-8583: Implement Compose semantics tag instrumentation. -@Suppress("UnusedPrivateProperty") internal class ComposeTagTransformer( private val messageCollector: MessageCollector, - private val pluginContext: IrPluginContext -) : IrElementTransformerVoidWithContext() + private val pluginContext: IrPluginContext, + private val pluginContextUtils: DefaultPluginContextUtils = DefaultPluginContextUtils( + pluginContext, + messageCollector + ) +) : IrElementTransformerVoidWithContext() { + + private val visitedFunctions = ArrayDeque() + private val visitedBuilders = ArrayDeque() + private lateinit var datadogTagFunctionSymbol: IrSimpleFunctionSymbol + private lateinit var modifierClass: IrClassSymbol + private lateinit var modifierThenSymbol: IrSimpleFunctionSymbol + private lateinit var modifierCompanionClassSymbol: IrClassSymbol + + @Suppress("ReturnCount") + fun initReferences(): Boolean { + datadogTagFunctionSymbol = pluginContextUtils.getDatadogModifierSymbol() ?: return false + modifierClass = pluginContextUtils.getModifierClassSymbol() ?: return false + modifierThenSymbol = pluginContextUtils.getModifierThen() ?: return false + modifierCompanionClassSymbol = pluginContextUtils.getModifierCompanionClass() ?: return false + return true + } + + override fun visitFunctionNew(declaration: IrFunction): IrStatement { + val declarationName = declaration.name + val functionName = if (!isAnonymousFunction(declarationName)) { + declarationName.toString() + } else { + visitedFunctions.lastOrNull() ?: declarationName.toString() + } + + if (pluginContextUtils.isComposableFunction(declaration)) { + visitedFunctions.add(functionName) + visitedBuilders.add(DeclarationIrBuilder(pluginContext, declaration.symbol)) + } else { + visitedFunctions.add(null) + visitedBuilders.add(null) + } + + val irStatement = super.visitFunctionNew(declaration) + visitedFunctions.removeLast() + visitedBuilders.removeLast() + return irStatement + } + + @Suppress("ReturnCount") + override fun visitCall(expression: IrCall): IrExpression { + val builder = visitedBuilders.lastOrNull() ?: return super.visitCall( + expression + ) + val dispatchReceiver = expression.dispatchReceiver + // Chained function call should be skipped + if (dispatchReceiver is IrCall) { + return super.visitCall(expression) + } + expression.symbol.owner.valueParameters.forEachIndexed { index, irValueParameter -> + // Locate where Modifier is accepted in the parameter list and replace it with the new expression. + if (irValueParameter.type.classFqName == modifierClassFqName) { + val argument = expression.getValueArgument(index) + val irExpression = buildIrExpression(argument, builder) + expression.putValueArgument(index, irExpression) + } + } + return super.visitCall(expression) + } + + private fun buildIrExpression( + expression: IrExpression?, + builder: DeclarationIrBuilder + ): IrExpression { + // TODO RUM-8813:Use Compose function name as the semantics tag + val datadogTagModifier = buildDatadogTagModifierCall(builder) + // A Column(){} will be transformed into following code during FIR: + // Column(modifier = // COMPOSITE { + // null + // }, verticalArrangement = // COMPOSITE { + // null + // }, horizontalAlignment = // COMPOSITE { + // null + // }, content = @Composable + // checking if the argument is the type of `COMPOSITE` + // allows us to know if the modifier is absent in source code. + val overwriteModifier = expression == null || + (expression is IrComposite && expression.type.classFqName == kotlinNothingFqName) + if (overwriteModifier) { + return datadogTagModifier + } else { + // Modifier.then() + val thenCall = builder.irCall( + modifierThenSymbol, + modifierClass.owner.defaultType + ) + thenCall.putValueArgument(0, expression) + thenCall.dispatchReceiver = datadogTagModifier + return thenCall + } + } + + private fun buildDatadogTagModifierCall( + builder: DeclarationIrBuilder, + composableName: String = DEFAULT_DD_TAG + ): IrCall { + val datadogTagIrCall = builder.irCall( + datadogTagFunctionSymbol, + modifierClass.defaultType + ).also { + // Modifier + it.extensionReceiver = builder.irGetObjectValue( + type = modifierCompanionClassSymbol.createType(false, emptyList()), + classSymbol = modifierCompanionClassSymbol + ) + it.putValueArgument(0, builder.irString(composableName)) + } + return datadogTagIrCall + } + + private fun isAnonymousFunction(name: Name): Boolean = name == SpecialNames.ANONYMOUS + + private companion object { + private val modifierClassFqName = FqName("androidx.compose.ui.Modifier") + private val kotlinNothingFqName = FqName("kotlin.Nothing") + private const val DEFAULT_DD_TAG = "DD_DEFAULT_TAG" + } +} diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DatadogIrExtension.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DatadogIrExtension.kt index 524bb49d..3158deb2 100644 --- a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DatadogIrExtension.kt +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DatadogIrExtension.kt @@ -3,6 +3,7 @@ package com.datadog.gradle.plugin.kcp import org.jetbrains.kotlin.backend.common.extensions.FirIncompatiblePluginAPI import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.ir.declarations.IrModuleFragment @@ -14,7 +15,13 @@ class DatadogIrExtension(private val messageCollector: MessageCollector) : IrGen override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { val composeTagTransformer = ComposeTagTransformer(messageCollector, pluginContext) - - moduleFragment.accept(composeTagTransformer, null) + if (composeTagTransformer.initReferences()) { + moduleFragment.accept(composeTagTransformer, null) + } else { + messageCollector.report( + CompilerMessageSeverity.ERROR, + "Datadog kotlin compiler plugin missing reference, abort." + ) + } } } diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtils.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtils.kt new file mode 100644 index 00000000..4aff6334 --- /dev/null +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtils.kt @@ -0,0 +1,89 @@ +package com.datadog.gradle.plugin.kcp + +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.ir.util.companionObject +import org.jetbrains.kotlin.ir.util.hasAnnotation +import org.jetbrains.kotlin.ir.util.kotlinFqName +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +internal class DefaultPluginContextUtils( + private val pluginContext: IrPluginContext, + private val messageCollector: MessageCollector +) : PluginContextUtils { + + private val modifierClassId = ClassId(composeUiPackageName, modifierClassRelativeName) + private val modifierThenCallableId = CallableId(modifierClassId, modifierThenIdentifier) + private val datadogModifierCallableId = CallableId( + packageName = datadogPackageName, + callableName = datadogModifierIdentifier + ) + + fun getModifierCompanionClass(): IrClassSymbol? { + return getModifierClassSymbol()?.owner?.companionObject()?.symbol ?: logNotFoundError( + MODIFIER_COMPANION_NAME + ) + } + + fun getModifierClassSymbol(): IrClassSymbol? { + return pluginContext + .referenceClass(modifierClassId) ?: logNotFoundError( + modifierClassId.asString() + ) + } + + fun getDatadogModifierSymbol(): IrSimpleFunctionSymbol? { + return referenceSingleFunction(datadogModifierCallableId) + } + + fun getModifierThen(): IrSimpleFunctionSymbol? { + return referenceSingleFunction(modifierThenCallableId) + } + + fun isComposableFunction(owner: IrFunction): Boolean { + return !isAndroidX(owner) && hasComposableAnnotation(owner) + } + + fun referenceSingleFunction(callableId: CallableId): IrSimpleFunctionSymbol? { + return pluginContext + .referenceFunctions(callableId) + .singleOrNull() ?: logSingleMatchError(callableId.callableName.asString()) + } + + private fun logSingleMatchError(target: String): T? { + messageCollector.report(CompilerMessageSeverity.ERROR, ERROR_SINGLE_MATCH.format(target)) + return null + } + + private fun logNotFoundError(target: String): T? { + messageCollector.report(CompilerMessageSeverity.ERROR, ERROR_NOT_FOUND.format(target)) + return null + } + + private fun isAndroidX(owner: IrFunction): Boolean { + val packageName = owner.parent.kotlinFqName.asString() + return packageName.startsWith("androidx") + } + + private fun hasComposableAnnotation(owner: IrFunction): Boolean = + owner.hasAnnotation(composableFqName) + + companion object { + private const val ERROR_SINGLE_MATCH = "%s has none or several references." + private const val ERROR_NOT_FOUND = "%s is not found." + private const val MODIFIER_COMPANION_NAME = "Modifier.Companion" + private val composableFqName = FqName("androidx.compose.runtime.Composable") + private val datadogPackageName = FqName("com.datadog.kcp.compose") + private val datadogModifierIdentifier = Name.identifier("datadog") + private val composeUiPackageName = FqName("androidx.compose.ui") + private val modifierClassRelativeName = Name.identifier("Modifier") + private val modifierThenIdentifier = Name.identifier("then") + } +} diff --git a/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/PluginContextUtils.kt b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/PluginContextUtils.kt new file mode 100644 index 00000000..327b1dde --- /dev/null +++ b/dd-sdk-android-gradle-plugin/src/main/kotlin/com/datadog/gradle/plugin/kcp/PluginContextUtils.kt @@ -0,0 +1,3 @@ +package com.datadog.gradle.plugin.kcp + +internal interface PluginContextUtils diff --git a/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtilsTest.kt b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtilsTest.kt new file mode 100644 index 00000000..90a25801 --- /dev/null +++ b/dd-sdk-android-gradle-plugin/src/test/kotlin/com/datadog/gradle/plugin/kcp/DefaultPluginContextUtilsTest.kt @@ -0,0 +1,134 @@ +package com.datadog.gradle.plugin.kcp + +import com.datadog.gradle.plugin.utils.forge.Configurator +import fr.xgouchet.elmyr.Forge +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol +import org.jetbrains.kotlin.name.CallableId +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +class DefaultPluginContextUtilsTest { + + @Mock + private lateinit var mockPluginContext: IrPluginContext + + @Mock + private lateinit var mockMessageCollector: MessageCollector + + @Mock + private lateinit var mockModifierClassSymbol: IrClassSymbol + + @Mock + private lateinit var mockModifierClassOwnerSymbol: IrClass + + @Mock + private lateinit var mockModifierCompanionClass: IrClass + + @Mock + private lateinit var mockModifierCompanionClassSymbol: IrClassSymbol + + @Mock + private lateinit var mockDatadogModifierFunctionSymbol: IrSimpleFunctionSymbol + + @Mock + private lateinit var mockSimpleFunctionSymbol: IrSimpleFunctionSymbol + + private lateinit var fakeCallableId: CallableId + + private lateinit var pluginContextUtils: DefaultPluginContextUtils + + private val modifierClassId = ClassId(composeUiPackageName, modifierClassRelativeName) + + @BeforeEach + fun `set up`(forge: Forge) { + pluginContextUtils = DefaultPluginContextUtils(mockPluginContext, mockMessageCollector) + fakeCallableId = CallableId( + packageName = FqName(forge.anAsciiString()), + className = FqName(forge.anAsciiString()), + callableName = Name.identifier(forge.anAsciiString()), + pathToLocal = FqName(forge.anAsciiString()) + ) + } + + @Test + fun `M return Modifier Companion class symbol W have the dependency`() { + // Given + whenever(mockPluginContext.referenceClass(modifierClassId)) doReturn mockModifierClassSymbol + whenever(mockModifierClassSymbol.owner) doReturn mockModifierClassOwnerSymbol + whenever(mockModifierCompanionClass.isCompanion) doReturn true + whenever(mockModifierClassOwnerSymbol.declarations) doReturn listOf(mockModifierCompanionClass).toMutableList() + whenever(mockModifierCompanionClass.symbol) doReturn mockModifierCompanionClassSymbol + + // When + val companionClassSymbol = pluginContextUtils.getModifierCompanionClass() + + // Then + assertThat(companionClassSymbol).isEqualTo(mockModifierCompanionClassSymbol) + } + + @Test + fun `M return Modifier class symbol W have the dependency`() { + // Given + whenever(mockPluginContext.referenceClass(modifierClassId)) doReturn mockModifierClassSymbol + + // When + val modifierClassSymbol = pluginContextUtils.getModifierClassSymbol() + + // Then + assertThat(modifierClassSymbol).isEqualTo(mockModifierClassSymbol) + } + + @Test + fun `M return datadog modifier symbol W have the dependency`() { + // Given + whenever(mockPluginContext.referenceFunctions(fakeCallableId)) doReturn + listOf(mockDatadogModifierFunctionSymbol) + + // When + val irSimpleFunctionSymbol = pluginContextUtils.referenceSingleFunction(fakeCallableId) + + // Then + assertThat(irSimpleFunctionSymbol).isEqualTo(mockDatadogModifierFunctionSymbol) + } + + @Test + fun `M return function symbol W call referenceSingleFunction`() { + // Given + whenever(mockPluginContext.referenceFunctions(fakeCallableId)) doReturn listOf(mockSimpleFunctionSymbol) + + // When + val irSimpleFunctionSymbol = pluginContextUtils.referenceSingleFunction(fakeCallableId) + + // Then + assertThat(irSimpleFunctionSymbol).isEqualTo(mockSimpleFunctionSymbol) + } + + companion object { + private val composeUiPackageName = FqName("androidx.compose.ui") + private val modifierClassRelativeName = Name.identifier("Modifier") + } +} diff --git a/instrumented/build.gradle.kts b/instrumented/build.gradle.kts index 9bca11b9..b0b08c62 100644 --- a/instrumented/build.gradle.kts +++ b/instrumented/build.gradle.kts @@ -53,6 +53,7 @@ android { dependencies { kotlinCompilerPluginClasspath(project(":dd-sdk-android-gradle-plugin")) + implementation(project(":samples:lib-module")) implementation(libs.androidx.core) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/instrumented/src/androidTest/java/com/datadog/android/instrumented/SemanticsTest.kt b/instrumented/src/androidTest/java/com/datadog/android/instrumented/SemanticsTest.kt index 9744bb5e..59d15ad5 100644 --- a/instrumented/src/androidTest/java/com/datadog/android/instrumented/SemanticsTest.kt +++ b/instrumented/src/androidTest/java/com/datadog/android/instrumented/SemanticsTest.kt @@ -1,8 +1,10 @@ package com.datadog.android.instrumented import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.junit4.createComposeRule import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.jupiter.api.extension.ExtendWith @@ -21,13 +23,32 @@ class SemanticsTest { @get:Rule val composeTestRule = createComposeRule() + @Ignore("RUM-8813: Fix the compose tag injection when Modifier is absent") @Test - fun `M have no datadog semantics tag W compose kotlin compiler plugin is not registered`() { + fun `M have datadog semantics tag W modifier is absent`() { composeTestRule.setContent { - TestScreen() + ScreenWithoutModifier() } val semanticsMatcher = hasSemanticsValue(DD_SEMANTICS_KEY_NAME, DD_SEMANTICS_VALUE_DEFAULT) - composeTestRule.onNode(semanticsMatcher).assertDoesNotExist() + composeTestRule.onAllNodes(semanticsMatcher).assertCountEquals(2) + } + + @Test + fun `M have datadog semantics tag W modifier is default`() { + composeTestRule.setContent { + ScreenWithDefaultModifier() + } + val semanticsMatcher = hasSemanticsValue(DD_SEMANTICS_KEY_NAME, DD_SEMANTICS_VALUE_DEFAULT) + composeTestRule.onAllNodes(semanticsMatcher).assertCountEquals(2) + } + + @Test + fun `M have datadog semantics tag W modifier is custom`() { + composeTestRule.setContent { + ScreenWithCustomModifier() + } + val semanticsMatcher = hasSemanticsValue(DD_SEMANTICS_KEY_NAME, DD_SEMANTICS_VALUE_DEFAULT) + composeTestRule.onAllNodes(semanticsMatcher).assertCountEquals(2) } private fun hasSemanticsValue( diff --git a/instrumented/src/main/java/com/datadog/android/instrumented/TestScreen.kt b/instrumented/src/main/java/com/datadog/android/instrumented/TestScreen.kt index daf9f399..3cf742cf 100644 --- a/instrumented/src/main/java/com/datadog/android/instrumented/TestScreen.kt +++ b/instrumented/src/main/java/com/datadog/android/instrumented/TestScreen.kt @@ -1,12 +1,43 @@ package com.datadog.android.instrumented +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color @Composable -internal fun TestScreen() { +internal fun ScreenWithoutModifier() { Column { - Text("") + Text( + text = "ScreenWithoutModifier" + ) + } +} + +@Composable +internal fun ScreenWithDefaultModifier() { + Column(modifier = Modifier) { + Text( + modifier = Modifier, + text = "ScreenWithDefaultModifier" + ) + } +} + +@Composable +internal fun ScreenWithCustomModifier() { + Column( + modifier = Modifier + .fillMaxSize() + .background(color = Color.Red) + ) { + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = "ScreenWithCustomModifier" + ) } } diff --git a/samples/lib-module/build.gradle b/samples/lib-module/build.gradle index d7e869dc..e919fb56 100644 --- a/samples/lib-module/build.gradle +++ b/samples/lib-module/build.gradle @@ -26,3 +26,11 @@ android { jvmTarget = '17' } } + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) +} \ No newline at end of file diff --git a/samples/lib-module/src/main/kotlin/com/datadog/kcp/compose/DatadogModifier.kt b/samples/lib-module/src/main/kotlin/com/datadog/kcp/compose/DatadogModifier.kt new file mode 100644 index 00000000..effda5a9 --- /dev/null +++ b/samples/lib-module/src/main/kotlin/com/datadog/kcp/compose/DatadogModifier.kt @@ -0,0 +1,26 @@ +package com.datadog.kcp.compose + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.semantics + +/** + * Function to simulate production function in SDK code base for instrumented testing purpose. + * This function should have exactly the same package name, function signature and return type + * with the production one. + */ +fun Modifier.datadog(name: String): Modifier { + return this.semantics { + this.datadog = name + } +} + +internal val DatadogSemanticsPropertyKey: SemanticsPropertyKey = SemanticsPropertyKey( + name = "_dd_semantics", + mergePolicy = { parentValue, _ -> + parentValue + } +) + +private var SemanticsPropertyReceiver.datadog by DatadogSemanticsPropertyKey