min
09/12/2026, 8:26 AM@Target(AnnotationTarget.CLASS)
public annotation class MyAnnotation(val arg: String)
Once it has been found that a class is annotated with MyAnnotation,
.getAnnotationByClassId(
classId = ANNOTATION_CLASS, // `MyAnnotation`
session = session,
)
.let { nullable -> nullable ?: return null }
.getStringArgument(ANNOTATION_ARG)!! // `MyAnnotation::arg`
This is obviously fine in general application development, since a MyAnnotation without an arg: String is undefined behaviour territory. But am I expected to handle it differently in a plugin?hfhbd
09/12/2026, 8:30 AMmin
09/12/2026, 8:31 AMgetAnnotationByClassId call chain shown above safe to use in a FirDeclarationGenerationExtension?hfhbd
09/12/2026, 8:33 AMPavel Kunyavskiy [JB]
09/12/2026, 8:35 AMmin
09/12/2026, 8:35 AMMyAnnotation without an arg: String?hfhbd
09/12/2026, 8:36 AM@MyAnnotation fun foo()min
09/12/2026, 8:37 AMarg doesn’t have a default value though. That’s illegal.hfhbd
09/12/2026, 8:37 AMPavel Kunyavskiy [JB]
09/12/2026, 8:38 AMmin
09/12/2026, 8:39 AMannotation class MyAnnotation(val arg: String)
fun main() {
@MyAnnotation fun f() {}
}
This code is rejected by the compiler.hfhbd
09/12/2026, 8:40 AMhfhbd
09/12/2026, 8:41 AMmin
09/12/2026, 8:42 AMmin
09/12/2026, 8:44 AMPavel Kunyavskiy [JB]
09/12/2026, 8:50 AMmin
09/12/2026, 8:58 AMannotation class has an arg: KClass<*> field.
sealed class Either<out Y, out N> {
class Happy<out Y>(val happy: Y): Either<Y, Nothing>()
class Error<out N>(val error: N): Either<Nothing, N>()
}
private class Operand private constructor(
val annotatedClass: FirClassSymbol<*>,
val annotationArgument: ClassId,
) {
sealed class FactoryError {
object NotAnnotatedClass: FactoryError()
object MissingArgument: FactoryError()
}
companion object {
fun from(
maybeAnnotatedClass: FirClassSymbol<*>,
session: FirSession,
): Either<Operand, FactoryError> {
val annot = maybeAnnotatedClass.getAnnotationByClassId(
classId = ANNOTATION_CLASS,
session = session,
)
if (annot == null) {
return Either.Error(FactoryError.NotAnnotatedClass)
}
val annotationArgument = annot.getKClassArgument(ANNOTATION_ARG)
if (annotationArgument == null) {
return Either.Error(FactoryError.MissingArgument)
}
val classId = annotationArgument.classId
return TODO()
}
}
}
Is it still unsafe to do annotationArgument.classId!! for the val classId?Pavel Kunyavskiy [JB]
09/12/2026, 9:01 AM@Ann(toplevelProperty::class)
which is not a valid call, but would probably reach this stage. Also, it can be just not computed yet, if you are in too early endpoint.
Also, I'm not sure, what would happen on unresolved class there.min
09/12/2026, 9:09 AMYoussef Shoaib [MOD]
09/12/2026, 10:34 AM```annotation class MyAnnotation(val arg: String)
fun main() {
@MyAnnotation fun f() {}
}```will get to your plugin. This isn't a bad thing. Sometimes, you want to still do plugin stuff even on invalid code to still maintain the user experience. Think about how, in IDE, if you type invalid code, the surrounding code still stays valid, highlighted properly, etc. That's good UX: You can, of course, decide to just early return and do nothing, which is valid too I like using Arrow for this purpose. It lets you early return from very deep in your call stack, but without actually throwing an error ultimately.
min
09/21/2026, 7:56 AMclass MyClass: SomeClass() {
@Norm fun Int.f()
}
Would it be possible to author a plugin that lets people write code like this then? Normally a concrete function cannot leave out the body. But I’d like a plugin that operates on and transforms signatures such as this into:
class MyClass: SomeClass() {
// Assuming `SomeClass` declares `fun Int.f()`.
fun f(self: Int) {
self.f()
}
}
So that calls such as 42.f() within MyClass can be replaced with this.f(42).Youssef Shoaib [MOD]
09/21/2026, 8:13 AM