Skip to content

Commit 6bc4986

Browse files
authored
Fix compatibility Kotlin with 2.2.0 (Sentry Kotlin Compiler Plugin) (#944)
* Duplicate source set to fix Kotlin 2.2.0 incompatibility * Fix dispatch receiver / extension receiver API changes * Update comment * Update Changelog
1 parent 00f769b commit 6bc4986

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- Enable caching for BundleSourcesTask ([#894](https://github.com/getsentry/sentry-android-gradle-plugin/pull/894)
8+
- Add support for Kotlin 2.2.0 for Sentry Kotlin Compiler Plugin ([#944](https://github.com/getsentry/sentry-android-gradle-plugin/pull/944))
89

910
### Breaking Changes
1011

sentry-kotlin-compiler-plugin/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ plugins {
1212

1313
val kotlin1920: SourceSet by sourceSets.creating
1414
val kotlin2120: SourceSet by sourceSets.creating
15+
val kotlin2200: SourceSet by sourceSets.creating
1516

1617
spotless {
1718
kotlin {
@@ -52,12 +53,15 @@ dependencies {
5253
testImplementation(libs.composeDesktop)
5354
testImplementation(kotlin1920.output)
5455
testImplementation(kotlin2120.output)
56+
testImplementation(kotlin2200.output)
5557

5658
kotlin1920.compileOnlyConfigurationName("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.24")
5759
kotlin2120.compileOnlyConfigurationName("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.20")
60+
kotlin2200.compileOnlyConfigurationName("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0")
5861

5962
compileOnly(kotlin1920.output)
6063
compileOnly(kotlin2120.output)
64+
compileOnly(kotlin2200.output)
6165
}
6266

6367
kapt { correctErrorTypes = true }
@@ -76,6 +80,7 @@ plugins.withId("com.vanniktech.maven.publish.base") {
7680
tasks.withType<Jar>().configureEach {
7781
from(kotlin1920.output)
7882
from(kotlin2120.output)
83+
from(kotlin2200.output)
7984
}
8085

8186
// see
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package io.sentry.compose
2+
3+
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
4+
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
5+
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
6+
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
7+
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
8+
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
9+
import org.jetbrains.kotlin.ir.IrStatement
10+
import org.jetbrains.kotlin.ir.builders.irCall
11+
import org.jetbrains.kotlin.ir.builders.irGetObjectValue
12+
import org.jetbrains.kotlin.ir.builders.irString
13+
import org.jetbrains.kotlin.ir.declarations.IrFunction
14+
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
15+
import org.jetbrains.kotlin.ir.expressions.IrCall
16+
import org.jetbrains.kotlin.ir.expressions.IrComposite
17+
import org.jetbrains.kotlin.ir.expressions.IrExpression
18+
import org.jetbrains.kotlin.ir.types.classFqName
19+
import org.jetbrains.kotlin.ir.types.createType
20+
import org.jetbrains.kotlin.ir.util.companionObject
21+
import org.jetbrains.kotlin.ir.util.defaultType
22+
import org.jetbrains.kotlin.ir.util.hasAnnotation
23+
import org.jetbrains.kotlin.ir.util.kotlinFqName
24+
import org.jetbrains.kotlin.name.CallableId
25+
import org.jetbrains.kotlin.name.ClassId
26+
import org.jetbrains.kotlin.name.FqName
27+
import org.jetbrains.kotlin.name.Name
28+
import org.jetbrains.kotlin.name.SpecialNames
29+
30+
// Modified duplicate of JetpackComposeTracingIrExtension21, compiled against 2.2.0
31+
class JetpackComposeTracingIrExtension22(private val messageCollector: MessageCollector) :
32+
IrGenerationExtension {
33+
34+
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
35+
val composableAnnotation = FqName("androidx.compose.runtime.Composable")
36+
val kotlinNothing = FqName("kotlin.Nothing")
37+
38+
val modifierClassFqName = FqName("androidx.compose.ui.Modifier")
39+
40+
val modifierClassId = FqName("androidx.compose.ui").classId("Modifier")
41+
val modifierClassSymbol = pluginContext.referenceClass(modifierClassId)
42+
if (modifierClassSymbol == null) {
43+
messageCollector.report(
44+
CompilerMessageSeverity.WARNING,
45+
"No class definition of androidx.compose.ui.Modifier found, " +
46+
"Sentry Kotlin Compiler plugin won't run. " +
47+
"Please ensure you're applying the plugin to a compose-enabled project.",
48+
)
49+
return
50+
}
51+
52+
val modifierType = modifierClassSymbol.owner.defaultType
53+
val modifierCompanionClass =
54+
pluginContext.referenceClass(modifierClassId)?.owner?.companionObject()
55+
val modifierCompanionClassRef = modifierCompanionClass?.symbol
56+
57+
if (modifierCompanionClass == null || modifierCompanionClassRef == null) {
58+
messageCollector.report(
59+
CompilerMessageSeverity.WARNING,
60+
"No type definition of androidx.compose.ui.Modifier found, " +
61+
"Sentry Kotlin Compiler plugin won't run. " +
62+
"Please ensure you're applying to plugin to a compose-enabled project.",
63+
)
64+
return
65+
}
66+
67+
val modifierThenRefs = pluginContext.referenceFunctions(modifierClassId.callableId("then"))
68+
if (modifierThenRefs.isEmpty()) {
69+
messageCollector.report(
70+
CompilerMessageSeverity.WARNING,
71+
"No definition of androidx.compose.ui.Modifier.then() found, " +
72+
"Sentry Kotlin Compiler plugin won't run. " +
73+
"Please ensure you're applying to plugin to a compose-enabled project.",
74+
)
75+
return
76+
} else if (modifierThenRefs.size != 1) {
77+
messageCollector.report(
78+
CompilerMessageSeverity.WARNING,
79+
"Multiple definitions androidx.compose.ui.Modifier.then() found, " +
80+
"which is not supported by Sentry Kotlin Compiler plugin won't run. " +
81+
"Please file an issue under " +
82+
"https://github.com/getsentry/sentry-android-gradle-plugin",
83+
)
84+
return
85+
}
86+
val modifierThen = modifierThenRefs.single()
87+
88+
val sentryModifierClassId = FqName("io.sentry.compose").classId("SentryModifier")
89+
90+
val sentryModifierCompanionClass =
91+
pluginContext.referenceClass(sentryModifierClassId)?.owner?.companionObject()
92+
93+
val sentryModifierTagFunction = sentryModifierClassId.callableId("sentryTag")
94+
95+
val sentryModifierTagFunctionRefs = pluginContext.referenceFunctions(sentryModifierTagFunction)
96+
97+
if (sentryModifierCompanionClass == null || sentryModifierTagFunctionRefs.isEmpty()) {
98+
messageCollector.report(
99+
CompilerMessageSeverity.WARNING,
100+
"io.sentry.compose.Modifier.sentryTag() not found, " +
101+
"Sentry Kotlin Compiler plugin won't run. " +
102+
"Please ensure you're using " +
103+
"'io.sentry:sentry-compose-android' as a dependency.",
104+
)
105+
return
106+
} else if (sentryModifierTagFunctionRefs.size != 1) {
107+
messageCollector.report(
108+
CompilerMessageSeverity.WARNING,
109+
"Multiple definitions io.sentry.compose.Modifier.sentryTag() found, " +
110+
"Sentry Kotlin Compiler plugin won't run. " +
111+
"Please ensure your versions of 'io.sentry:sentry-compose-android' " +
112+
"and the sentry Android Gradle plugin match.",
113+
)
114+
return
115+
}
116+
val sentryModifierTagFunctionRef = sentryModifierTagFunctionRefs.single()
117+
val sentryModifierCompanionClassRef = sentryModifierCompanionClass.symbol
118+
119+
val transformer =
120+
object : IrElementTransformerVoidWithContext() {
121+
122+
// a stack of the function names
123+
private var visitingFunctionNames = ArrayDeque<String?>()
124+
private var visitingDeclarationIrBuilder = ArrayDeque<DeclarationIrBuilder?>()
125+
126+
override fun visitFunctionNew(declaration: IrFunction): IrStatement {
127+
val anonymous = declaration.name == SpecialNames.ANONYMOUS
128+
129+
// in case of an anonymous, let's try to fallback to it's enclosing function name
130+
val name =
131+
if (!anonymous) declaration.name.toString()
132+
else {
133+
visitingFunctionNames.lastOrNull() ?: declaration.name.toString()
134+
}
135+
136+
val isComposable = declaration.symbol.owner.hasAnnotation(composableAnnotation)
137+
138+
val packageName = declaration.symbol.owner.parent.kotlinFqName.asString()
139+
140+
val isAndroidXPackage = packageName.startsWith("androidx")
141+
val isSentryPackage = packageName.startsWith("io.sentry.compose")
142+
143+
if (isComposable && !isAndroidXPackage && !isSentryPackage) {
144+
visitingFunctionNames.add(name)
145+
visitingDeclarationIrBuilder.add(
146+
DeclarationIrBuilder(pluginContext, declaration.symbol)
147+
)
148+
} else {
149+
visitingFunctionNames.add(null)
150+
visitingDeclarationIrBuilder.add(null)
151+
}
152+
val irStatement = super.visitFunctionNew(declaration)
153+
154+
visitingFunctionNames.removeLast()
155+
visitingDeclarationIrBuilder.removeLast()
156+
return irStatement
157+
}
158+
159+
override fun visitCall(expression: IrCall): IrExpression {
160+
val composableName =
161+
visitingFunctionNames.lastOrNull() ?: return super.visitCall(expression)
162+
val builder =
163+
visitingDeclarationIrBuilder.lastOrNull() ?: return super.visitCall(expression)
164+
165+
// avoid infinite recursion by instrumenting ourselves
166+
val dispatchReceiver = expression.dispatchReceiver
167+
if (
168+
(dispatchReceiver is IrCall &&
169+
dispatchReceiver.symbol == sentryModifierTagFunctionRef) ||
170+
expression.symbol == sentryModifierTagFunctionRef
171+
) {
172+
return super.visitCall(expression)
173+
}
174+
175+
for (idx in 0 until expression.symbol.owner.parameters.size) {
176+
val valueParameter = expression.symbol.owner.parameters[idx]
177+
if (valueParameter.type.classFqName == modifierClassFqName) {
178+
val argument = expression.arguments[idx]
179+
expression.arguments[idx] = wrapExpression(argument, composableName, builder)
180+
}
181+
}
182+
return super.visitCall(expression)
183+
}
184+
185+
private fun wrapExpression(
186+
expression: IrExpression?,
187+
composableName: String,
188+
builder: DeclarationIrBuilder,
189+
): IrExpression {
190+
val overwriteModifier =
191+
expression == null ||
192+
(expression is IrComposite && expression.type.classFqName == kotlinNothing)
193+
194+
if (overwriteModifier) {
195+
// Case A: modifier is not supplied
196+
// -> simply set our modifier as param
197+
// e.g. BasicText(text = "abc")
198+
// into BasicText(text = "abc", modifier = Modifier.sentryTag("<composable>"))
199+
200+
// we can safely set the sentryModifier if there's no value parameter provided
201+
// but in case the Jetpack Compose Compiler plugin runs before us,
202+
// it will inject all default value parameters as actual parameters using IrComposite
203+
// hence we need to cover this case and overwrite the composite default/null value with
204+
// sentryModifier
205+
// see
206+
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt;l=287-298;drc=f0b820e062ac34044b43144a87617e90d74657f3
207+
208+
// Modifier.sentryTag()
209+
return generateSentryTagCall(builder, composableName)
210+
} else {
211+
// Case B: modifier is already supplied
212+
// -> chain the modifiers
213+
// e.g. BasicText(text = "abc", modifier = Modifier.fillMaxSize())
214+
// into BasicText(text = "abc", modifier =
215+
// Modifier.sentryTag("<>").then(Modifier.fillMaxSize())
216+
217+
// wrap the call with the sentryTag modifier
218+
219+
// Modifier.sentryTag()
220+
val sentryTagCall = generateSentryTagCall(builder, composableName)
221+
222+
// sentryTag.then(<original expression>)
223+
val thenCall = builder.irCall(modifierThen, type = modifierType)
224+
// argument 0: dispatch receiver
225+
thenCall.arguments[0] = sentryTagCall
226+
// argument 1: modifier expression
227+
thenCall.arguments[1] = expression
228+
return thenCall
229+
}
230+
}
231+
232+
private fun generateSentryTagCall(
233+
builder: DeclarationIrBuilder,
234+
composableName: String,
235+
): IrCall {
236+
val sentryTagCall =
237+
builder.irCall(sentryModifierTagFunctionRef, type = modifierType).also {
238+
// dispatch receiver: SentryModifier
239+
// it.arguments[0]
240+
it.arguments[0] =
241+
builder.irGetObjectValue(
242+
type = sentryModifierCompanionClassRef.createType(false, emptyList()),
243+
classSymbol = sentryModifierCompanionClassRef,
244+
)
245+
246+
// extension receiver, Modifier
247+
it.arguments[1] =
248+
builder.irGetObjectValue(
249+
type = modifierCompanionClassRef.createType(false, emptyList()),
250+
classSymbol = modifierCompanionClassRef,
251+
)
252+
253+
it.arguments[2] = builder.irString(composableName)
254+
}
255+
return sentryTagCall
256+
}
257+
}
258+
259+
moduleFragment.transform(transformer, null)
260+
}
261+
}
262+
263+
fun FqName.classId(name: String): ClassId {
264+
return ClassId(this, Name.identifier(name))
265+
}
266+
267+
fun ClassId.callableId(name: String): CallableId {
268+
return CallableId(this, Name.identifier(name))
269+
}

sentry-kotlin-compiler-plugin/src/main/kotlin/io/sentry/SentryKotlinCompilerPlugin.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.sentry
33
import com.google.auto.service.AutoService
44
import io.sentry.compose.JetpackComposeTracingIrExtension19
55
import io.sentry.compose.JetpackComposeTracingIrExtension21
6+
import io.sentry.compose.JetpackComposeTracingIrExtension22
67
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
78
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
89
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
@@ -31,7 +32,9 @@ class SentryKotlinCompilerPlugin : CompilerPluginRegistrar() {
3132
}
3233

3334
val extension: IrGenerationExtension =
34-
if (version >= SimpleSemanticVersion(2, 1, 20)) {
35+
if (version >= SimpleSemanticVersion(2, 2, 0)) {
36+
JetpackComposeTracingIrExtension22(messageCollector)
37+
} else if (version >= SimpleSemanticVersion(2, 1, 20)) {
3538
// 2.1.20 removed some optional parameters, causing API incompatibility
3639
// e.g. java.lang.NoSuchMethodError
3740
// see https://github.com/JetBrains/kotlin/commit/dd508452c414a0ee8082aa6f76d664271cb38f2f

0 commit comments

Comments
 (0)