Skip to content

Consider using sealed hierarchies in the KSP API to allow exhaustive whens #1351

@lukellmann

Description

@lukellmann

Here is a real-world example that could benefit from sealed hierarchies in the KSP API:

/**
 * The binary name of this class-like declaration on the JVM, as specified by
 * [The Java® Language Specification](https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.1).
 */
val KSClassDeclaration.jvmBinaryName: String
    get() = when (val parent = parentDeclaration) {
        // this is a top-level class-like declaration -> canonical name / fully qualified name (same for top-level)
        null -> this.qualifiedName!!.asString()

        // this is a member class-like declaration -> binary name of immediately enclosing declaration + $ + simple name
        is KSClassDeclaration -> parent.jvmBinaryName + '$' + this.simpleName.asString()

        is KSFunctionDeclaration, is KSPropertyDeclaration ->
            error("jvmBinaryName isn't implemented for local class-like declarations but $this seems to be one")
        is KSTypeAlias, is KSTypeParameter -> error("$parent shouldn't be the parentDeclaration of $this")
        else -> error("$this has an unknown parentDeclaration: $parent")
    }

If KSDeclaration was a sealed interface, there would be no need to provide an else. In the hypothetical scenario that a new subtype of KSDeclaration was introduced, a compile error would arise because the when wouldn't cover all possible cases anymore (which is desirable if one wants to be exhaustive and handle all cases correctly).

jvmBinaryName could also be implemented using a visitor and can even also result in compiler errors (because of new non-overridden methods from KSVisitor) when new KSNodes are introduced, however the implementation is more complex and requires more code:

val KSClassDeclaration.jvmBinaryName get() = accept(JvmBinaryNameVisitor, data = this)

private object JvmBinaryNameVisitor : KSVisitor<KSClassDeclaration, String> {

    override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: KSClassDeclaration): String {
        val parent = classDeclaration.parentDeclaration
        return if (parent == null) {
            classDeclaration.qualifiedName!!.asString()
        } else {
            parent.accept(this, classDeclaration) + '$' + classDeclaration.simpleName.asString()
        }
    }


    private fun localError(clazz: KSClassDeclaration): Nothing =
        error("jvmBinaryName isn't implemented for local class-like declarations but $clazz seems to be one")

    override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: KSClassDeclaration) =
        localError(clazz = data)

    override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: KSClassDeclaration) =
        localError(clazz = data)


    private fun wrongParentError(parent: KSDeclaration, child: KSClassDeclaration): Nothing =
        error("$parent shouldn't be the parentDeclaration of $child")

    override fun visitTypeAlias(typeAlias: KSTypeAlias, data: KSClassDeclaration) =
        wrongParentError(parent = typeAlias, child = data)

    override fun visitTypeParameter(typeParameter: KSTypeParameter, data: KSClassDeclaration) =
        wrongParentError(parent = typeParameter, child = data)


    private fun error(symbol: KSNode): Nothing =
        error("Unexpected symbol $symbol, JvmBinaryNameVisitor should only visit subtypes of KSDeclaration")

    // repeat for all other visit<Node> methods, if a new one is added, there will be a compile error until the method is overridden
    override fun visitNode(node: KSNode, data: KSClassDeclaration) = error(node)
    override fun visitAnnotated(annotated: KSAnnotated, data: KSClassDeclaration) = error(annotated)
    // ...
}

I think the following types would benefit from being sealed: KSNode, KSAnnotated, KSDeclarationContainer, KSReferenceElement, KSModifierListOwner, KSPropertyAccessor, KSDeclaration (the one that would benefit the example above) and also com.google.devtools.ksp.processing.PlatformInfo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions