Skip to content

Commit

Permalink
RUM-8583: Add ComposeTagTransformer to inject datadog modifier call
Browse files Browse the repository at this point in the history
  • Loading branch information
ambushwork committed Feb 27, 2025
1 parent 33b3236 commit 010a0b7
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 11 deletions.
1 change: 1 addition & 0 deletions dd-sdk-android-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
testImplementation(libs.okHttpMock)
testImplementation(libs.androidToolsPluginGradle)
testImplementation(libs.kotlinPluginGradle)
testImplementation(libs.kotlinCompilerEmbeddable)

compileOnly(libs.kotlinCompilerEmbeddable)
compileOnly(libs.autoServiceAnnotation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String?>()
private val visitedBuilders = ArrayDeque<DeclarationIrBuilder?>()
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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <T> logSingleMatchError(target: String): T? {
messageCollector.report(CompilerMessageSeverity.ERROR, ERROR_SINGLE_MATCH.format(target))
return null
}

private fun <T> 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")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.datadog.gradle.plugin.kcp

internal interface PluginContextUtils
Loading

0 comments on commit 010a0b7

Please sign in to comment.