Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
162: Kotlin Compiler Plugins Overview
By the time you've reached this part of the course, you're probably comfortable with how Kotlin works as a user. But today, we're going to peek under the hood. Compiler plugins are where the real "magic" happens—think of things like @Serializable in Kotlinx.Serialization or the way Compose transforms your functions into UI nodes. It's powerful, but I'll be honest: it's one of the steepest learning curves in the ecosystem because you're no longer writing Kotlin; you're writing code that writes Kotlin.
To keep this grounded, let's try to build a conceptual plugin. I want a feature where I can mark a property with a @TraceChange annotation, and the compiler should automatically inject a print statement whenever that property is updated. I'm tired of manually adding println("Value changed to $value") to every setter in my state machines.
Picking our entry point in the IR
Kotlin uses something called IR (Intermediate Representation). Think of it as a tree-like structure that represents your code after it's been parsed but before it becomes JVM bytecode. To change the actual logic of a program, we need to implement an IrGenerationExtension.
My plan is to traverse the IR tree, look for any IrProperty that has my @TraceChange annotation, and then modify its setter. Here is the basic logic I'm aiming for in the extension:
class TraceChangeIrGenerator : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
moduleFragment.accept(object : IrElementVisitorVoid() {
override fun visitProperty(declaration: IrProperty) {
if (declaration.hasAnnotation("TraceChange")) {
injectLoggingCall(declaration, pluginContext)
}
super.visitProperty(declaration)
}
}, null)
}
}
The "Infinite Loop" mistake
Now, here is where I messed up during my first attempt. I decided that since I wanted to log the new value, I would simply call the property's getter inside the setter to see what the value became. It sounded logical at the time.
I wrote a bit of IR code that effectively did this: set(value) { field = value; println(this.myProperty) }. The problem? this.myProperty calls the getter. In some of my more complex custom getters, this triggered a chain reaction that eventually called the setter again, or simply created an unnecessary overhead that slowed the app to a crawl. I realized I shouldn't be referencing the property itself; I should be referencing the field (the backing field) or the value parameter passed into the setter.
I had to go back and modify the IR builder to specifically use the IrValueParameter of the setter. It's a subtle difference in the code, but a huge difference in execution.
Manually stitching the IR nodes
Since we can't just write println() inside a compiler plugin, we have to manually find the function reference for println in the Kotlin Standard Library and "stitch" it into the IR tree. It looks something like this (simplified for clarity):
private fun injectLoggingCall(property: IrProperty, context: IrPluginContext) {
val setter = property.setter ?: return
val body = setter.body as IrBlockBody
// Find the println function from the stdlib
val printlnFunc = context.referenceFunctions(FqName("kotlin.io.println")).first()
// Create the call: println("Property ${property.name} changed!")
val call = IrCallImpl(
startOffset = setter.startOffset,
endOffset = setter.endOffset,
type = IrBuiltIn.Void,
symbol = printlnFunc.symbol,
typeArgumentsCount = 0,
valueArguments = listOf(createStringIr(context, "Property ${property.name} changed!"))
)
body.statements.add(call)
}
Wiring it all together via Gradle
The hardest part isn't actually the IR logic—it's getting the compiler to recognize your plugin. You can't just add a dependency; you have to register a ComponentRegistrar. This is a special class that the Kotlin compiler looks for using Java's ServiceLoader.
I usually set up a separate Gradle module for the plugin itself, then use the kotlin-compiler-plugin-gradle-plugin to apply it to my main app. It's a bit of a "bootstrap" process: you write a plugin, you write a Gradle plugin to apply that compiler plugin, and then you apply the Gradle plugin to your project. It feels redundant, but it's necessary to ensure the compiler has the plugin loaded before it starts analyzing your source code.
📋 Practical Task
Implement a "StrictlyReadOnly" IR Validation Logic
Instead of modifying code (which is destructive), you are tasked with designing the logic for a Validation Plugin. Your goal is to create a plugin that marks a compile-time error if a property annotated with @StrictlyReadOnly is assigned a value anywhere other than the class initializer (the init block or constructor).
Write a pseudo-code implementation of the IrElementVisitorVoid that would achieve this. Your solution should:
- Identify
IrSetExpression(where a value is being assigned to a property). - Check if the target property has the
@StrictlyReadOnlyannotation. - Verify if the current containing function is the constructor/initializer.
- Call a hypothetical
context.reportError("Property cannot be changed after initialization")if the conditions are met.
There are no comments for now.