I know that a plugin shouldn’t throw in general, b...
# compiler
m
I know that a plugin shouldn’t throw in general, but where do you draw the line?
Copy code
@Target(AnnotationTarget.CLASS)
public annotation class MyAnnotation(val arg: String)
Once it has been found that a
class
is annotated with
MyAnnotation
,
Copy code
.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?
h
The FIR validates the source code. If it “compiles”, you are fine to use !! in IR. At least that's the way I see it.
m
@hfhbd Would that make the
getAnnotationByClassId
call chain shown above safe to use in a
FirDeclarationGenerationExtension
?
h
No, FIR is unsafe.
p
No. In FIR you should be ready to gracefully proceed with red code. Typically by just not doing anything, if something is really required. Than later check would report missing argument. Otherwise, you would get errors that are really hard to understand for user, and break IDE during editing, if runned there. Note, that if it's possible to do something (like still generate class, but without bodies, or without some methods), it would lead to better IDE experience during editing, when/if you plan to add your plugin to IDE.
m
I’m so confused. How can there be a
MyAnnotation
without an
arg: String
?
h
@MyAnnotation fun foo()
m
@hfhbd
arg
doesn’t have a default value though. That’s illegal.
h
What's the definition of illegal?
p
Yes. But your code would still run on that (before missing arugment is reported later). And it need to do "something", just to proceed until error is reported.
m
@hfhbd What do you mean?
Copy code
annotation class MyAnnotation(val arg: String)

fun main() {
    @MyAnnotation fun f() {}
}
This code is rejected by the compiler.
h
Yes, but AFTER your plugin is called. So your FIR plugin would crash
➕ 1
And you can also run 3rd party compiler plugins in IDE, so your code will be called every time a user types, resulting into many crashes.
m
@Pavel Kunyavskiy [JB] I’ve been told that there’s a limit to what I can achieve with plugins because all code has to be valid before it‘s manipulated by plugins too. But is that not the full picture? What can I count on then? I feel paralysed by how complex and treacherous the DX is
@hfhbd So no throwing at all in the FIR, and I may throw in the IR if I’m in normally impossible territory?
👌 1
p
Well. It really depends on when exactly the plugin is called. E.g. there is no plugin endpoints, before code parsing, so the code must by syntactically valid. On the other point IR part of the plugin is called after all checks, so it can assume much more. Than depending on specific endpoint, amount of what you can assume can vary. But that also true for how complete is FIR data. For example, during "add a super class" extension, you can't read types or query scopes of the classes or know which exactly function was called (because overload resolution didn't happen yet). In custom checkers code all data is availiable, but you are now in the part which is responsible for rejecting incorrect code. So a lot of incorrect code is not rejected yet. That's one of really complicated parts of both compiler plugin and the compiler itself. But it's very important to user experience. You don't want everything to become red in IDE because of single typo., niether you want to report errors one-by-one. So it's important, to keep errors as local as possible.
m
@Pavel Kunyavskiy [JB] My actual
annotation class
has an
arg: KClass<*>
field.
Copy code
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
?
p
Not really sure. I think it can be strange on something like
Copy code
@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.
m
It feels like my whole plugin is a precarious house of cards
y
Let me try to simplify this a bit: if you want your plugin to run in IDE, then bad code (like missing an annotation argument) will still get to your plugin. Throwing an error in this case is bad practice since it can break the IDE experience. In other words, invalid code can get to your plugin, but all you really need to do is just return early and not bother dealing with it. So indeed, code like:
```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.
m
I think it was you who told me that all code had to be valid before being transformed by plugins too, so I’m a little confused.
Copy code
class 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:
Copy code
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)
.
y
Imagine you're in the IDE typing. You might write this "invalid" code that doesn't have a body. Normally, it'll just give you the normal error and that's it But oops, you have a compiler plugin running, and it assumes that all functions will have a body in FIR, so it throws... Very likely, this'll break code highlighting in IDE because it'll short-circuit all the IDE analysis of code. In other words, currently, the IDE does not expect FIR plugins to throw any exceptions, so it doesn't do anything to handle that gracefully. Even if the IDE eventually handles this well, it'll likely still consider a throwing plugin faulty in some way, and will e.g. display the thrown exceptions to the user. That's too much noise for a very normal situation when someone writes half-complete code in the IDE Thus, the recommendation is to not throw any exceptions in FIR plugins