https://kotlinlang.org logo
m

myotive

03/01/2022, 11:52 PM
Hello! 👋 (cross posting this from #ksp) Is this a good place to discuss Kotlin Compiler Plugin’s? I’m trying to write a plugin that will add Gson’s
@SerializedName
annotation and Moshi’s
@Json
annotation to all the properties of a data class. Everything seems to be working in my transformer (
IrElementTransformerVoidWithContext
) , but the resulting class doesn’t seem to have the annotations when I try to run a unit test (which loads the class from a temp working directory). If anyone has any documentation or resources that could help, I would be most grateful. Thanks!
1
r

rnett

03/02/2022, 5:13 PM
When you say your transformer is working, does that mean it doesn't throw errors, or that you have dumped the IR and the annotations are there? Decompiling the class files is also worth checking
m

myotive

03/02/2022, 6:50 PM
@rnett - Thanks for the reply! When I say the transformer is working, I mean it’s at least compiling my source and attempt to add the annotations to my class. Here’s my source class:
Copy code
package com.test.models
                        
import com.example.annotations.DDJson
                
@DDJson
data class SampleClassModel(val sampleProperty: String)
From this class, I’m trying to produce:
Copy code
@JsonClass(generateAdapter = true)
data class SampleClassModel(
@Json(name = "sample_property")
@SeralizedName(value = "sample_property")
val sampleProperty: String
)
☝️ Here’s the output from the Plugin when I do a
declaration.dump()
r

rnett

03/02/2022, 6:52 PM
Well that looks fine, try opening the class file (that you were loading in the unit test) in IntelliJ or another decompiler
m

myotive

03/02/2022, 6:54 PM
Copy code
public final data class SampleClassModel public constructor(sampleProperty: kotlin.String) {
    public final val sampleProperty: kotlin.String /* compiled code */

    public final operator fun component1(): kotlin.String { /* compiled code */ }
}
To make it easier for you to see what I’m doing, here’s a quick gist of my project (unfortunately, I can’t post or upload the project to github 😬 😞 https://gist.github.com/myotive-dd/88922f34001803b754d8ba5ab29dd0b5
Note: This was my first stab at doing a Kotlin Compiler Plugin. I wanted to brute force all the things and then work my way backwards to make it better. My code quality is not…. great…. 🤦
r

rnett

03/02/2022, 7:55 PM
The only thing I noticed is that when testing, I added my
ComponentRegistrar
to
KotlinCompilation.compilerPlugins
and didn't do anything else. Is the IR dump from the test run?
m

myotive

03/02/2022, 8:06 PM
Yeah - I think I’ve got my ComponentRegistar added there:
compilerPlugins = listOf(DDJsonComponentRegistrar())
Copy code
private fun prepareCompilation(
        vararg sourceFiles: SourceFile
    ): KotlinCompilation {
        return KotlinCompilation().apply {
            workingDir = temporaryFolder.root
            compilerPlugins = listOf(DDJsonComponentRegistrar())
            val processor = DDJsonCommandLineProcessor()
            commandLineProcessors = listOf(processor)
            pluginOptions =
                listOf(
                    processor.option(KEY_ENABLED, "true"),
                )
            inheritClassPath = true
            sources = sourceFiles.asList()
            verbose = false
            jvmTarget = JvmTarget.fromString(System.getenv()["ci_java_version"] ?: "1.8")!!.description
        }
    }
Also - yeah, the IR dump is from the test run:
Copy code
override fun visitClassNew(declaration: IrClass): IrStatement {

        // todo = fix this hardcoded reference
        declaration.annotations = listOf(JsonClassNameAnnotation(pluginContext, declaration, true))

        val propertyTransformer = PropertyTransformer()
        declaration.properties.forEach {
            it.transform(propertyTransformer, null)
        }
....

--->    println(declaration.dump())

        return super.visitClassNew(declaration)
    }
r

rnett

03/02/2022, 8:26 PM
Hmm, maybe try using the plugin in a different project, rather than via compile-testing, and see if you still have the issue?
Also, how are you calling your transformer?
m

myotive

03/02/2022, 8:38 PM
I register the extension here:
Copy code
@AutoService(ComponentRegistrar::class)
class DDJsonComponentRegistrar : ComponentRegistrar {

    override fun registerProjectComponents(
        project: MockProject,
        configuration: CompilerConfiguration
    ) {

        if (configuration[KEY_ENABLED] == false) return

        val messageCollector =
            configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)

        IrGenerationExtension.registerExtension(
            project,
            DDJsonIrGenerationExtension(messageCollector)
        )
    }
}
And then inside
DDJsonIrGenerationExtension
, I call the transform (
DDJsonIrVisitor
):
Copy code
class DDJsonIrGenerationExtension(
    private val messageCollector: MessageCollector,
) : IrGenerationExtension {
    override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
        val fqName = FqName(DDJson::class.java.canonicalName)
        val ddjsonAnnotatedClass = pluginContext.referenceClass(fqName)!!
        val redactedTransformer = DDJsonIrVisitor(pluginContext, ddjsonAnnotatedClass, messageCollector)
        moduleFragment.transform(redactedTransformer, null)
    }
}
lol - `redactedTransformer`… Can you tell I had some influence from @Zac Sweers’s Redacted Compiler Plugin?
r

rnett

03/02/2022, 8:47 PM
I don't have the code in front of me to check, so I'm not sure, but I think
moduleFragment.transform
returns the transformed module, instead of applying it in-place. I've used
redactedTransformer.lower(moduleFragment)
in the past, maybe try that
m

myotive

03/02/2022, 9:17 PM
hmm… I’m not seeing a
lower
method on the
IrElementTransformerVoidWithContext
class (
redactedTransformer
is extending that). Am I using the wrong transformer?
r

rnett

03/02/2022, 9:21 PM
Ah right, have your transformer implement
FileLoweringPass
Yeah, see here, that's what actually applies the transformations
m

myotive

03/02/2022, 9:46 PM
Hmm… still not seeing the annotations in the class file (and my resulting test fails trying to load and send through to Moshi). Here’s my updates:
Copy code
class DDJsonIrVisitor(
    private val pluginContext: IrPluginContext,
    private val ddjsonAnnotatedClass: IrClassSymbol,
    private val messageCollector: MessageCollector
) : IrElementTransformerVoidWithContext(), FileLoweringPass {
...
    override fun lower(irFile: IrFile) {
        irFile.transformChildrenVoid()
    }
}
Copy code
class DDJsonIrGenerationExtension(
    private val messageCollector: MessageCollector,
) : IrGenerationExtension {
    override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
        val fqName = FqName(DDJson::class.java.canonicalName)
        val ddjsonAnnotatedClass = pluginContext.referenceClass(fqName)!!
        val ddjsonVisitor = DDJsonIrVisitor(pluginContext, ddjsonAnnotatedClass, messageCollector)
        ddjsonVisitor.lower(moduleFragment)
    }
}
It seems like the call to `DDJsonIrVisitor`’s lower method is happening before any of the
visitClassNew
methods get called. I found this project that was doing something similar…
r

rnett

03/03/2022, 5:21 AM
m

myotive

03/03/2022, 10:15 PM
I gave up…. 😢 Management says I shouldn’t spend anymore time on this (wish I could take the time I used to create an annotation processor that worked, but wasn’t a good fit for this task because I honestly think the Compiler Plugin was the way to go). I just can’t seem to get this working. When I get more time, perhaps I’ll loop back around and try this again and, if I get it working, put it up on github because I didn’t see a whole ton of projects adding annotations on the fly like I was trying to do (lots of code generation like statements and classes, but I couldn’t find much with annotations). I appreciate your help and support on this! If we ever meet IRL, I owe you many drinks of your choice.