diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a418e2e120..0cfe67b575 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,9 +50,11 @@ jmh-generator = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.r jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } junit = { module = "junit:junit", version.ref = "junit" } kaml = { module = "com.charleskorn.kaml:kaml", version = "0.72.0" } +kotlin-compile-testing = { module = "dev.zacsweers.kctfork:core", version = "0.7.0" } kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlin-gradleApi = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } +kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-jsr223 = { module = "org.jetbrains.kotlin:kotlin-scripting-jsr223", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version = "1.8.0" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1740430e13..0724143698 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -83,3 +83,6 @@ include(":samples:wire-codegen-sample") include(":samples:wire-grpc-sample:client") include(":samples:wire-grpc-sample:protos") include(":samples:wire-grpc-sample:server") +include(":wire-binary-compatibility-gradle-plugin") +include(":wire-binary-compatibility-kotlin-plugin") +include(":wire-binary-compatibility-kotlin-plugin-tests") diff --git a/wire-binary-compatibility-gradle-plugin/README.md b/wire-binary-compatibility-gradle-plugin/README.md new file mode 100644 index 0000000000..310ae64f8e --- /dev/null +++ b/wire-binary-compatibility-gradle-plugin/README.md @@ -0,0 +1,43 @@ +# Wire Binary Compatibility Plugin + +The Wire Binary Compatibility Kotlin compiler plugin adapts Wire-generated callsites to be more resilient +to schema changes. + +## Current Support +Generated constructor callsites are rewritten at compile-time to instead use the Builder. +Generated copy() callsites are not yet supported. + +## Example +Given a generated class, Dinosaur, from a proto defintion: +```protobuf +package com.squareup.dinosaurs; + +message Dinosaur { + optional string name = 1; + optional double avg_length_meters = 2; +} +``` + +With existing usage: +```kotlin +val newDinosaur = Dinosaur( + name = "triceratops", + avg_length_meters = 9.0, +) +``` + +When a new field is added to the Dinosaur schema, if there are competing versions of the compiled class and the new +version is resolved at runtime, the usage above may encounter an error: +``` +java.lang.NoSuchMethodError: 'void com.squareup.dinosaurs.Dinosaur(java.lang.String, java.lang.Double, +{new_field})' +``` + +Using the Wire Binary Compatibility Plugin will adapt the compiled binary to use the equivalent Builder: +```kotlin +val newDinosaur = Dinosaur.Builder() + .name("triceratops") + .avg_length_meters(9.0) + .build() +``` +The rewritten, compiled code is backwards compatible with the new field. diff --git a/wire-binary-compatibility-gradle-plugin/build.gradle.kts b/wire-binary-compatibility-gradle-plugin/build.gradle.kts new file mode 100644 index 0000000000..9349fe0fc6 --- /dev/null +++ b/wire-binary-compatibility-gradle-plugin/build.gradle.kts @@ -0,0 +1,36 @@ +import com.vanniktech.maven.publish.GradlePlugin +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.MavenPublishBaseExtension + +plugins { + id("java-gradle-plugin") + `kotlin-dsl` + kotlin("jvm") + id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish.base") +} + +dependencies { + compileOnly(kotlin("gradle-plugin-api")) + compileOnly(project(":wire-binary-compatibility-kotlin-plugin")) + implementation(libs.kotlin.gradlePlugin) +} + +gradlePlugin { + plugins { + create("wireBinaryCompatibility") { + id = "com.squareup.wire.binarycompatibility" + displayName = "Wire Binary Compatibility" + description = "Rewrites Wire callsites to be resilient to API changes" + implementationClass = "com.squareup.wire.binarycompatibility.gradle.WireBinaryCompatibility" + } + } +} + +configure { + configure( + GradlePlugin( + javadocJar = JavadocJar.Empty() + ) + ) +} diff --git a/wire-binary-compatibility-gradle-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/gradle/BuildConfig.kt b/wire-binary-compatibility-gradle-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/gradle/BuildConfig.kt new file mode 100644 index 0000000000..4800173244 --- /dev/null +++ b/wire-binary-compatibility-gradle-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/gradle/BuildConfig.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2025 Square, Inc. + * + * 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 + * + * https://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 com.squareup.wire.binarycompatibility.gradle + +object BuildConfig { + const val KOTLIN_PLUGIN_GROUP: String = "com.squareup.wire.binarycompatibility" + + const val KOTLIN_PLUGIN_NAME: String = "wire-binary-compatibility-kotlin-plugin" + + const val KOTLIN_PLUGIN_VERSION: String = "1.0.0-SNAPSHOT" +} diff --git a/wire-binary-compatibility-gradle-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/gradle/WireBinaryCompatibilityPlugin.kt b/wire-binary-compatibility-gradle-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/gradle/WireBinaryCompatibilityPlugin.kt new file mode 100644 index 0000000000..297ee2a837 --- /dev/null +++ b/wire-binary-compatibility-gradle-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/gradle/WireBinaryCompatibilityPlugin.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 Square, Inc. + * + * 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 + * + * https://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 com.squareup.wire.binarycompatibility.gradle + +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation +import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin +import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact +import org.jetbrains.kotlin.gradle.plugin.SubpluginOption + +@Suppress("unused") // Created reflectively by Gradle. +class WireBinaryCompatibilityPlugin : KotlinCompilerPluginSupportPlugin { + override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true + + override fun getCompilerPluginId(): String = "com.squareup.wire.binarycompatibility.kotlin" + + override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact( + groupId = BuildConfig.KOTLIN_PLUGIN_GROUP, + artifactId = BuildConfig.KOTLIN_PLUGIN_NAME, + version = BuildConfig.KOTLIN_PLUGIN_VERSION, + ) + + override fun applyToCompilation( + kotlinCompilation: KotlinCompilation<*>, + ): Provider> { + return kotlinCompilation.target.project.provider { + listOf() // No options. + } + } +} diff --git a/wire-binary-compatibility-kotlin-plugin-tests/build.gradle.kts b/wire-binary-compatibility-kotlin-plugin-tests/build.gradle.kts new file mode 100644 index 0000000000..944584394e --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin-tests/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + kotlin("jvm") + id("org.jetbrains.dokka") +} + +dependencies { + testImplementation(project(":wire-binary-compatibility-kotlin-plugin")) + testImplementation(kotlin("compiler-embeddable")) + testImplementation(kotlin("test-junit")) + testImplementation(libs.assertk) + testImplementation(libs.kotlin.compile.testing) +} diff --git a/wire-binary-compatibility-kotlin-plugin-tests/src/test/kotlin/com/squareup/wire/binarycompatibility/WireBinaryCompatibilityKotlinPluginTest.kt b/wire-binary-compatibility-kotlin-plugin-tests/src/test/kotlin/com/squareup/wire/binarycompatibility/WireBinaryCompatibilityKotlinPluginTest.kt new file mode 100644 index 0000000000..a15bcc1038 --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin-tests/src/test/kotlin/com/squareup/wire/binarycompatibility/WireBinaryCompatibilityKotlinPluginTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2025 Square, Inc. + * + * 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 + * + * https://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 com.squareup.wire.binarycompatibility.kotlin + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.containsExactly +import com.tschuchort.compiletesting.JvmCompilationResult +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import kotlin.test.Test +import kotlin.test.assertEquals +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +@OptIn(ExperimentalCompilerApi::class) +class WireBinaryCompatibilityKotlinPluginTest { + @Test + fun rewriteConstructorCallToBuilderCall() { + val result = compile( + sourceFile = SourceFile.kotlin( + "Sample.kt", + """ + package com.squareup.wire + + val log = mutableListOf() + + fun callConstructor() { + log += "${'$'}{Money(5, "USD")}" + } + + fun callConstructorWithDefaultParameters() { + log += "${'$'}{Money(amount = 5)}" + log += "${'$'}{Money(currencyCode = "USD")}" + } + + data class Money( + val amount: Long? = null, + val currencyCode: String? = null, + ) : Message { + + class Builder : Message.Builder { + var amount: Long? = null + var currencyCode: String? = null + + fun amount(amount: Long) : Builder { + log += "calling amount()!" + this.amount = amount + return this + } + + fun currencyCode(currencyCode: String) : Builder { + log += "calling currencyCode()!" + this.currencyCode = currencyCode + return this + } + fun build() : Money = Money(amount, currencyCode) + } + } + + interface Message { + interface Builder + } + """, + ), + ) + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode, result.messages) + + val testClass = result.classLoader.loadClass("com.squareup.wire.SampleKt") + val log = testClass.getMethod("getLog") + .invoke(null) as MutableList + + testClass.getMethod("callConstructor").invoke(null) + assertThat(log).containsExactly( + "calling amount()!", + "calling currencyCode()!", + "Money(amount=5, currencyCode=USD)", + ) + log.clear() + + testClass.getMethod("callConstructorWithDefaultParameters").invoke(null) + assertThat(log).containsExactly( + "calling amount()!", + "Money(amount=5, currencyCode=null)", + "calling currencyCode()!", + "Money(amount=null, currencyCode=USD)", + ) + log.clear() + } +} + +@ExperimentalCompilerApi +fun compile( + sourceFiles: List, + plugin: CompilerPluginRegistrar = WireBinaryCompatibilityCompilerPluginRegistrar(), +): JvmCompilationResult { + return KotlinCompilation().apply { + sources = sourceFiles + compilerPluginRegistrars = listOf(plugin) + inheritClassPath = true + kotlincArguments += "-Xverify-ir=error" + kotlincArguments += "-Xverify-ir-visibility" + }.compile() +} + +@ExperimentalCompilerApi +fun compile( + sourceFile: SourceFile, + plugin: CompilerPluginRegistrar = WireBinaryCompatibilityCompilerPluginRegistrar(), +): JvmCompilationResult { + return compile(listOf(sourceFile), plugin) +} diff --git a/wire-binary-compatibility-kotlin-plugin/build.gradle.kts b/wire-binary-compatibility-kotlin-plugin/build.gradle.kts new file mode 100644 index 0000000000..d546e9602f --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin/build.gradle.kts @@ -0,0 +1,21 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinJvm +import com.vanniktech.maven.publish.MavenPublishBaseExtension + +plugins { + kotlin("jvm") + id("com.vanniktech.maven.publish.base") +} + +dependencies { + compileOnly(kotlin("compiler-embeddable")) + compileOnly(kotlin("stdlib")) +} + +configure { + configure( + KotlinJvm( + javadocJar = JavadocJar.Empty() + ) + ) +} diff --git a/wire-binary-compatibility-kotlin-plugin/gradle.properties b/wire-binary-compatibility-kotlin-plugin/gradle.properties new file mode 100644 index 0000000000..cacc3b8b72 --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin/gradle.properties @@ -0,0 +1,2 @@ +# We want the stdlib as a compileOnly dependency. +kotlin.stdlib.default.dependency=false diff --git a/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityCommandLineProcessor.kt b/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityCommandLineProcessor.kt new file mode 100644 index 0000000000..27bdc5e86b --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityCommandLineProcessor.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 Square, Inc. + * + * 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 + * + * https://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 com.squareup.wire.binarycompatibility.kotlin + +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi + +@OptIn(ExperimentalCompilerApi::class) +class WireBinaryCompatibilityCommandLineProcessor : CommandLineProcessor { + override val pluginId = "com.squareup.wire.binarycompatibility.kotlin" + + override val pluginOptions: List = listOf() +} diff --git a/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityCompilerPluginRegistrar.kt b/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityCompilerPluginRegistrar.kt new file mode 100644 index 0000000000..5445c6ae2f --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityCompilerPluginRegistrar.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 Square, Inc. + * + * 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 + * + * https://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 com.squareup.wire.binarycompatibility.kotlin + +import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +@OptIn( + ExperimentalCompilerApi::class, + UnsafeDuringIrConstructionAPI::class, +) +class WireBinaryCompatibilityCompilerPluginRegistrar : CompilerPluginRegistrar() { + override val supportsK2: Boolean + get() = true + + override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { + val messageCollector = configuration.get( + CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, + MessageCollector.NONE, + ) + IrGenerationExtension.registerExtension( + extension = WireBinaryCompatibilityIrGenerationExtension(messageCollector), + ) + } +} diff --git a/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityIrGenerationExtension.kt b/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityIrGenerationExtension.kt new file mode 100644 index 0000000000..ef3600dee3 --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin/src/main/kotlin/com/squareup/wire/binarycompatibility/kotlin/WireBinaryCompatibilityIrGenerationExtension.kt @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2025 Square, Inc. + * + * 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 + * + * https://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. + */ +@file:OptIn(UnsafeDuringIrConstructionAPI::class) + +package com.squareup.wire.binarycompatibility.kotlin + +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +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.MessageCollector +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.backend.js.utils.valueArguments +import org.jetbrains.kotlin.ir.builders.IrBlockBuilder +import org.jetbrains.kotlin.ir.builders.Scope +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irCallConstructor +import org.jetbrains.kotlin.ir.builders.irGet +import org.jetbrains.kotlin.ir.builders.irTemporary +import org.jetbrains.kotlin.ir.declarations.IrClass +import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.getClass +import org.jetbrains.kotlin.ir.util.classId +import org.jetbrains.kotlin.ir.util.constructors +import org.jetbrains.kotlin.ir.util.functions +import org.jetbrains.kotlin.ir.util.patchDeclarationParents +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +/** + * Rewrites calls to Wire-generated constructors like this: + + * ```kotlin + * val money = Money( + * amount = 5, + * currency = "USD", + * ) + * ``` + * + * into calls to the corresponding builders: + * + * ```kotlin + * val money = Money.Builder() + * .amount(5) + * .currency("USD") + * .build() + * ``` + */ +class WireConstructorCallRewriter( + private val pluginContext: IrPluginContext, + val constructorCall: IrConstructorCall, +) { + /** Returns the rewrite, or null if we don't want to rewrite this one. */ + @OptIn(FirIncompatiblePluginAPI::class) + fun rewrite(): IrExpression? { + // Validate that constructorCall has the shape we want: it's a Wire class + // Validate that the target class has a nested Builder class + // Validate that the target class is not within a Builder class + + val messageClassId = constructorCall.type.getClass()?.classId ?: return null + val builderClassId = messageClassId.createNestedClassId(Name.identifier("Builder")) + val builderSymbol: IrClassSymbol = pluginContext.referenceClass(builderClassId) ?: return null + + val buildFunction = + builderSymbol.functions.find { it.owner.valueParameters.isEmpty() && it.owner.name.identifier == "build" } + ?: return null + + // Create a block + val bodyBuilder = IrBlockBuilder( + startOffset = constructorCall.startOffset, + endOffset = constructorCall.endOffset, + context = pluginContext, + scope = Scope(constructorCall.symbol), + ) + + return bodyBuilder.block { + // First statement: + // val builder = Money.Builder() + val builder = irTemporary( + value = this.irCallConstructor( + callee = builderSymbol.constructors.find { it.owner.valueParameters.isEmpty() }!!, + listOf(), + ), + nameHint = "builder", + isMutable = false, + ).apply { + origin = IrDeclarationOrigin.DEFINED + } + + // builder.amount(5) + for (i in 0 until constructorCall.valueArguments.size) { + val valueArgument = constructorCall.valueArguments[i] ?: continue // Skip default parameters + val valueParameter = constructorCall.symbol.owner.valueParameters[i] + val parameterFunction = builderSymbol.functions.find { it.owner.valueParameters.size == 1 && it.owner.name == valueParameter.name } ?: return null + + +irCall( + callee = parameterFunction, + ).apply { + this.dispatchReceiver = irGet(builder) + this.putValueArgument(0, valueArgument) + } + } + + // return builder.build() + +irCall( + callee = buildFunction, + ).apply { + this.dispatchReceiver = irGet(builder) + } + } + } +} + +private val wirePackage = FqName("com.squareup.wire") +private val wireMessage = ClassId(wirePackage, Name.identifier("Message")) +private val wireMessageBuilder = wireMessage.createNestedClassId(Name.identifier("Builder")) +fun IrFunction.isDeclaredByWireMessageOrBuilder(): Boolean { + return (this.parent as? IrClass)?.superTypes?.any { it.isWireMessageOrBuilder() } ?: false +} + +fun IrType.isWireMessageOrBuilder(): Boolean { + val classId = this.getClass()?.classId ?: return false + return classId == wireMessage || classId == wireMessageBuilder +} + +@UnsafeDuringIrConstructionAPI // To use IrDeclarationContainer.declarations. +class WireBinaryCompatibilityIrGenerationExtension( + private val messageCollector: MessageCollector, +) : IrGenerationExtension { + + override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { + pluginContext.referenceClass(wireMessage) + ?: return // Don't do anything if Wire isn't on the classpath. There's no code to rewrite here. + + val transformer = object : IrElementTransformerVoidWithContext() { + override fun visitFunctionNew(declaration: IrFunction): IrStatement { + // When we visit any code generated by Wire (subtypes of Message and Message.Builder), do not rewrite this + // for binary compatibility! Otherwise, we'll recurse forever and stack overflow. We don't need to worry about + // this case in practice because Wire-generated code is by definition binary-compatible with itself. + if (declaration.isDeclaredByWireMessageOrBuilder()) return declaration + + return super.visitFunctionNew(declaration) + } + + override fun visitConstructorCall(expression: IrConstructorCall): IrExpression { + val constructorCall = super.visitConstructorCall(expression) + if (constructorCall !is IrConstructorCall) return constructorCall + val rewrite = WireConstructorCallRewriter(pluginContext, constructorCall).rewrite() + return rewrite ?: constructorCall + } + } + + moduleFragment.transform(transformer, null) + + moduleFragment.patchDeclarationParents() + } +} diff --git a/wire-binary-compatibility-kotlin-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/wire-binary-compatibility-kotlin-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor new file mode 100644 index 0000000000..813418a260 --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor @@ -0,0 +1 @@ +com.squareup.wire.binarycompatibility.kotlin.WireBinaryCompatibilityCommandLineProcessor diff --git a/wire-binary-compatibility-kotlin-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/wire-binary-compatibility-kotlin-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar new file mode 100644 index 0000000000..5c1898fb5e --- /dev/null +++ b/wire-binary-compatibility-kotlin-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar @@ -0,0 +1 @@ +com.squareup.wire.binarycompatibility.kotlin.WireBinaryCompatibilityCompilerPluginRegistrar