```when (declaration.primaryConstructorIfAny(conte...
# compiler
m
Copy code
when (declaration.primaryConstructorIfAny(context.session)) {
    null -> reporter.reportOn(
        declaration.source,
        KtDiagnosticFactory0(
            name = "",
            severity = Severity.ERROR,
            defaultPositioningStrategy = TODO(),
            psiType = TODO(),
            rendererFactory = TODO(),
        ),
        context,
    )
    // ...
}
What kind of arguments does
KtDiagnosticFactory0
expect? I can’t find its docs. I want my plugin to cause a compiler error if an annotated class doesn’t have a primary constructor.
d
You need to register your diagnostic factories using the special DSL provided by
KtDiagnosticsContainer
. After declaration you also need to register messages for your diagnostics. Check how it's done in `kotlinx.serialization`: • diagnostic factories themselves: FirSerializationErrors.kt • diagnostic messages: KtDefaultErrorMessagesSerialization.kt
m
Copy code
object MyDiagnosticsContainer: KtDiagnosticsContainer() {
    val MY_ERROR by error2<KtElement, String, String>()

    override fun getRendererFactory(): BaseDiagnosticRendererFactory {
        return object: BaseDiagnosticRendererFactory() {
            override val MAP by KtDiagnosticFactoryToRendererMap("MyPlugin") { map ->

            }
        }
    }
}
What’s the difference between
error{0, 1, 2, ...}
? If it is documented anywhere, could you please point me to it (sorry) as I’m unable to find it myself? I vaguely understand that
MyDiagnosticsContainer#getRendererFactory
should return an object that holds a populated map, but I’m not 100% sure what to do.
d
error0/1/2/3
define how many additional arguments you could provide to diagnostics. For example, there is a
warning0
named
REDUNDANT_ELSE_IN_WHEN
, which marks the
else
branch as redundant. It has the message
'when' is exhaustive so 'else' is redundant here.
which is always the same. On the other hand there is an
UNINITIALIZED_VARIABLE
, which is
error1
with the message
Variable ''{0}'' must be initialized.
. When you create a specific diagnostic, you pass the symbol of the variable, which is uninitialized, so the actual message which user will see will look like
Variable ''x'' must be initialized.
(or whatever the variable is named).
So when you create a diagnostic, you need to pass all the parameters its factory is expecting:
Copy code
reporter.reportOn(branch.source, FirErrors.REDUNDANT_ELSE_IN_WHEN)
reporter.reportOn(expression.source, FirErrors.UNINITIALIZED_VARIABLE, symbol) // symbol is an argument
When you define the diagnostic message in the
DiagnosticRendererFactory
, you need to provide renderers for each argument this diagnostic factory has. The renderer is basically
T -> String
conversion.
Also please make your
BaseDiagnosticRendererFactory
a proper named object, not the anonymous one. It would be good for performance (there is no need to create this object on each call). And IIRC there is some code in the compiler which relies on identity of these factories, so creating new objects each time could break it.
m
Copy code
object MyDiagnosticsContainer: KtDiagnosticsContainer() {
    val MY_ERROR by error0<KtElement>()

    override fun getRendererFactory(): BaseDiagnosticRendererFactory {
        // inlining breaks the compiler
        return MyDiagnosticsContainer.GetRendererFactoryOutput
    }
    object GetRendererFactoryOutput: BaseDiagnosticRendererFactory() {
        override val MAP: KtDiagnosticFactoryToRendererMap
            by KtDiagnosticFactoryToRendererMap("Fence") { map ->
                map.put(MyDiagnosticsContainer.MY_ERROR, message = "asdf")
            }
    }
}
Thank you for the explanation. I couldn’t navigate this alone because nothing seems to be documented, everything is a DSL, and only some of equivalent solutions work.
Copy code
class MyFirAdditionalCheckersExtension(
    session: FirSession,
    mppKind: MppCheckerKind,
): FirAdditionalCheckersExtension(session) {
    override val declarationCheckers: DeclarationCheckers = object: DeclarationCheckers() {
        override val regularClassCheckers = setOf(object: FirRegularClassChecker(mppKind) {
            context(context: CheckerContext, reporter: DiagnosticReporter)
            override fun check(declaration: FirRegularClass) {
                when (val myAnnotation = MyAnnotation.on(
                    maybeAnnotatedClass = declaration.symbol,
                    session = context.session,
                )) {
                    // Either<FenceAnnotation, FactoryError>
                    is Either.Foo -> return
                    is Either.Bar -> myAnnotation.bar.report(reporter, context)
                }
            }
        })
    }
}
I’ve also added an implementation of
FirAdditionalCheckersExtension
like the serialisation example you’ve showed me. The
FactoryError#report
method currently has the following scaffold implementation.
Copy code
fun report(reporter: DiagnosticReporter, context: CheckerContext) {
    when (this) {
        is FactoryError.ClassNotAnnotated -> reporter.reportOn(
            this.decl.source,
            MyDiagnosticsContainer.MY_ERROR,
            context,
        )
        is FactoryError.ArgumentMissing -> reporter.reportOn(
            this.annotatedClass.source,
            MyDiagnosticsContainer.MY_ERROR,
            context,
        )
        is FactoryError.ArgumentInvalid -> reporter.reportOn(
            this.annotatedClass.source,
            MyDiagnosticsContainer.MY_ERROR,
            context,
        )
        is FactoryError.PrimaryConstructorMissing -> reporter.reportOn(
            this.annotatedClass.source,
            MyDiagnosticsContainer.MY_ERROR,
            context,
        )
        is FactoryError.PrimaryConstructorExposed -> reporter.reportOn(
            this.primaryConstructor.source,
            MyDiagnosticsContainer.MY_ERROR,
            context,
        )
    }
}
I don’t understand why
<http://MyDiagnosticsContainer.MY|MyDiagnosticsContainer.MY>_ERROR
can’t be inlined (replaced with
error0<KtElement>()
)
d
Small styling suggestion:
Copy code
fun report(reporter: DiagnosticReporter, context: CheckerContext) {
    val source = when (this) {
        is FactoryError.ClassNotAnnotated -> this.decl.source
        is FactoryError.ArgumentMissing -> this.annotatedClass.source
        is FactoryError.ArgumentInvalid -> this.annotatedClass.source
        is FactoryError.PrimaryConstructorMissing -> this.annotatedClass.source
        is FactoryError.PrimaryConstructorExposed -> this.primaryConstructor.source
    }
    reporter.reportOn(source, MyDiagnosticsContainer.MY_ERROR, context)
}
I don’t understand why
<http://MyDiagnosticsContainer.MY|MyDiagnosticsContainer.MY>_ERROR
can’t be inlined (replaced with
error0<KtElement>()
)
You need to access each diagnostic at least in two places: • declaring a message for it • in the place where you report it (which could be more than one) It is possible to use the name of the factory as a unique identifier, but it's much more error prone and less handy than having each factory to be represented as a proper resolvable property in the code.
m
<http://MyDiagnosticsContainer.MY|MyDiagnosticsContainer.MY>_ERROR
is just a standin, each
FactoryError
type will get its own kind of diagnostic to report later, so I can’t have a single
reporter.reportOn(source, <http://MyDiagnosticsContainer.MY|MyDiagnosticsContainer.MY>_ERROR, context)
statement, since the second argument to this call also depends on the factory error.
👌 1
Is
KtDiagnosticsContainer#getRendererFactory
the only way to associate a diagnostic with an error message?
d
Yes, it is. Previously there was another mechanism which included JVM service loading, but it was deleted in favor of more simple and explicit approach.
m
Why is
val MY_ERROR by _error0_<KtElement>()
permitted as an
object
member but not as a local variable in a function?
d
If you check definition of
errorN/warningN
functions, there is a context parameter of type
KtDiagnosticsContainer
. So you can use it only if this context is provided:
Copy code
context(_: KtDiagnosticsContainer)
fun foo() {
    val MY_ERROR by error0<KtExpression>()
}

fun bar(container: KtDiagnosticsContainer) {
    with(container) {
        val MY_ERROR by error0<KtExpression>()
    }
}
The thing is that these utility functions not only create an instance of the factory, but also register this factory in the container.
m
Copy code
sealed class FactoryError {
    class ClassNotAnnotated(
        val decl: FirClassSymbol<*>,
    ): FactoryError() {
        companion object { val ERROR by error0<KtElement>() }
    }
    class ArgumentMissing(
        val fencedClass: FirClassSymbol<*>,
    ): FactoryError() {
        companion object { val ERROR by error0<KtElement>() }
    }
    // ...
}
So should I make the `companion object`s extend
KtDiagnosticsContainer()
?
MAP
shouldn’t be per
FactoryError
type though. It should be a unified map, shouldn't it?
d
It could work. Different diagnostic containers could reuse the same renderer factory. But please remember that each diagnostic container should be registered in the
FirExtensionRegistrar
, so it could be not very scalable.
I could recommend you something like this:
Copy code
sealed class FactoryError {
    abstract val diagnostic: KtDiagnosticFactory0

    class ClassNotAnnotated(
        val decl: FirClassSymbol<*>,
    ): FactoryError() {
        override val diagnostic: KtDiagnosticFactory0 get() = MyErrors.MY_NOT_ANNOTATED
    }
    class ArgumentMissing(
        val fencedClass: FirClassSymbol<*>,
    ): FactoryError() {
        override val diagnostic: KtDiagnosticFactory0 get() = MyErrors.MY_ARGUMENT_MISSING
    }
    // ...
}

object MyErrors : KtDiagnosticsContainer() {
    val MY_NOT_ANNOTATED by error0<KtElement>()
    val MY_ARGUMENT_MISSING by error0<KtElement>()

    override fun getRendererFactory(): BaseDiagnosticRendererFactory = TODO()
}
So it would be one diagnostic container, but each of `FactoryError`s would know its diagnostic factory.
m
Why is
diagnostic
an instance member?
d
Because it's nice API. Note that there is
get() =
in implementations, which means that no backing field is created. So it's basically the same as
override fun getDiagnosticFactory() = <http://MyErrors.MY|MyErrors.MY>_NOT_ANNOTATED
.
m
I don’t think I’ll do that because it doesn’t make sense to me. Also I was trying not to spread diagnostic information between several classes but it seems there’s no way to avoid it.
🙌 1