https://kotlinlang.org logo
#compiler
Title
# compiler
m

MerlinTHS

12/02/2023, 10:42 PM
Hi, I'm a bit confused about testing diagnostics. I have written a compiler plugin that generates companion objects. If the type already has such a companion, my tests pass without problems, but when I use the plugin in a demo project to do the same (generate a companion for a type that already has one), I get the following exception: Plugin generated companion object for class org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl@5c23485, but it is already present in class It comes from the
FirCompanionGenerationProcessor
. It's called (along with all the other processors) in the
runResolution
method from the org.jetbrains.kotlin.fir.pipeline package. But it's never called in my tests. Is there a way to include it in the test pipeline, to reproduce the error in test cases?
j

Javier

12/03/2023, 8:42 AM
Looks like a bug. How are you generating it?
m

MerlinTHS

12/03/2023, 10:35 AM
Copy code
import org.jetbrains.kotlin.GeneratedDeclarationKey
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.declarations.FirDeclarationOrigin
import org.jetbrains.kotlin.fir.extensions.*
import org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate
import org.jetbrains.kotlin.fir.plugin.createCompanionObject
import org.jetbrains.kotlin.fir.plugin.createDefaultPrivateConstructor
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirConstructorSymbol
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.name.SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT
import org.jetbrains.kotlin.utils.addToStdlib.runIf

class CompanionGenerator(session: FirSession, annotations: List<String>) : FirDeclarationGenerationExtension(session) {
    object Key : GeneratedDeclarationKey() {
       override fun toString() = "CompanionGeneratorKey"
    }

    private val companionPredicate = LookupPredicate.create { 
       annotated(annotations.map(::FqName)) 
    }

    override fun FirDeclarationPredicateRegistrar.registerPredicates() {
       register(companionPredicate)
    }

    override fun generateNestedClassLikeDeclaration(owner: FirClassSymbol<*>, name: Name, context: NestedClassGenerationContext): FirClassLikeSymbol<*>? =
        runIf(name == DEFAULT_NAME_FOR_COMPANION_OBJECT) { createCompanionObject(owner, Key).symbol }

    override fun generateConstructors(context: MemberGenerationContext): List<FirConstructorSymbol> {
        val constructor = createDefaultPrivateConstructor(context.owner, Key)
        return listOf(constructor.symbol)
    }

    override fun getCallableNamesForClass(classSymbol: FirClassSymbol<*>, context: MemberGenerationContext): Set<Name> {
       if (!classSymbol.isCompanion) return emptySet()

        val origin = classSymbol.origin as? FirDeclarationOrigin.Plugin
        return runIf(origin?.key == Key) { setOf(SpecialNames.INIT) }.orEmpty()
    }

    override fun getNestedClassifiersNames(classSymbol: FirClassSymbol<*>, context: NestedClassGenerationContext): Set<Name> =
        runIf(classSymbol matches companionPredicate and classSymbol.needsCompanion) {
            setOf(DEFAULT_NAME_FOR_COMPANION_OBJECT)
        }.orEmpty()

    private infix fun FirClassSymbol<*>.matches(predicate: LookupPredicate): Boolean =
       session.predicateBasedProvider.matches(predicate, this)
}

private val FirClassSymbol<*>.isCompanion get() =
    isSingleton && with(classId) {
       isNestedClass && shortClassName == DEFAULT_NAME_FOR_COMPANION_OBJECT
    }

private val FirClassSymbol<*>.needsCompanion get() =
    !isSingleton && declarationSymbols.none { (it as? FirClassSymbol<*>)?.isCompanion ?: false }

private val FirClassSymbol<*>.isSingleton get() =
    classKind == ClassKind.OBJECT
This is generation extension. The error occurs (only) in a demo project when I omit the second check in
needsCompanion
(obviously). Of course, I can simply rewrite my code to "work", but every time I do that, I would like to have a test to check the issue I have fixed.
j

Javier

12/03/2023, 10:38 AM
I think the test is valid and it is a bug in the IDE. You can compare your code with the Kotlin Serialization one which generates companion objects too. If they are not doing anything special I would expect this to be working before the final release as they can’t release 2.0.0 with Kotlin Serialization being broken.
m

MerlinTHS

12/03/2023, 11:20 AM
Looks like I should create my own errors/warnings for that case.
j

Javier

12/03/2023, 11:27 AM
Why? FIR shouldn't fail in this case. If you try to generate something that already exists, FIR skips it by default (that is the reason your test passes, correctly).
But on the IDE this is not working... looks like a bug
m

MerlinTHS

12/03/2023, 11:33 AM
But where to find that "skipping"? All I found was the `FirCompanionGenerationProcessor`which is part of the normal compiler pipeline. And it's the source of the given error. So I expected the same error to occur in my test cases.
Here you can find the complete project: https://github.com/MerlinTHS/Companionate
j

Javier

12/03/2023, 2:32 PM
AFAIK if you tries to generate something that already exists, it is skipped. Not only companion.