All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.annotation.processing.test.support.Ksp.kt Maven / Gradle / Ivy

/*
 * Copyright 2017-2023 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.annotation.processing.test.support

import com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension
import com.google.devtools.ksp.KspOptions
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot
import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment
import org.jetbrains.kotlin.com.intellij.mock.MockProject
import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeAdapter
import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.languageVersionSettings
import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File

/**
 * The list of symbol processors for the kotlin compilation.
 * https://goo.gle/ksp
 */
var KotlinCompilation.symbolProcessorProviders: List
    get() = getKspRegistrar().providers
    set(value) {
        val registrar = getKspRegistrar()
        registrar.providers = value
    }

/**
 * The directory where generated KSP sources are written
 */
val KotlinCompilation.kspSourcesDir: File
    get() = kspWorkingDir.resolve("sources")

/**
 * Arbitrary arguments to be passed to ksp
 */
var KotlinCompilation.kspArgs: MutableMap
    get() = getKspRegistrar().options
    set(value) {
        val registrar = getKspRegistrar()
        registrar.options = value
    }

/**
 * Controls for enabling incremental processing in KSP.
 */
var KotlinCompilation.kspIncremental: Boolean
    get() = getKspRegistrar().incremental
    set(value) {
        val registrar = getKspRegistrar()
        registrar.incremental = value
    }

/**
 * Controls for enabling incremental processing logs in KSP.
 */
var KotlinCompilation.kspIncrementalLog: Boolean
    get() = getKspRegistrar().incrementalLog
    set(value) {
        val registrar = getKspRegistrar()
        registrar.incrementalLog = value
    }

/**
 * Controls for enabling all warnings as errors in KSP.
 */
var KotlinCompilation.kspAllWarningsAsErrors: Boolean
    get() = getKspRegistrar().allWarningsAsErrors
    set(value) {
        val registrar = getKspRegistrar()
        registrar.allWarningsAsErrors = value
    }

/**
 * Run processors and compilation in a single compiler invocation if true.
 * See [com.google.devtools.ksp.KspCliOption.WITH_COMPILATION_OPTION].
 */
var KotlinCompilation.kspWithCompilation: Boolean
    get() = getKspRegistrar().withCompilation
    set(value) {
        val registrar = getKspRegistrar()
        registrar.withCompilation = value
    }

private val KotlinCompilation.kspJavaSourceDir: File
    get() = kspSourcesDir.resolve("java")

private val KotlinCompilation.kspKotlinSourceDir: File
    get() = kspSourcesDir.resolve("kotlin")

private val KotlinCompilation.kspResources: File
    get() = kspSourcesDir.resolve("resources")

/**
 * The working directory for KSP
 */
private val KotlinCompilation.kspWorkingDir: File
    get() = workingDir.resolve("ksp")

/**
 * The directory where compiled KSP classes are written
 */
// TODO this seems to be ignored by KSP and it is putting classes into regular classes directory
//  but we still need to provide it in the KSP options builder as it is required
//  once it works, we should make the property public.
private val KotlinCompilation.kspClassesDir: File
    get() = kspWorkingDir.resolve("classes")

/**
 * The directory where compiled KSP caches are written
 */
private val KotlinCompilation.kspCachesDir: File
    get() = kspWorkingDir.resolve("caches")

/**
 * Custom subclass of [AbstractKotlinSymbolProcessingExtension] where processors are pre-defined instead of being
 * loaded via ServiceLocator.
 */
private class KspTestExtension(
    options: KspOptions,
    processorProviders: List,
    logger: KSPLogger
) : AbstractKotlinSymbolProcessingExtension(
    options = options,
    logger = logger,
    testMode = false
) {
    private val loadedProviders = processorProviders

    override fun loadProviders() = loadedProviders
}

/**
 * Registers the [KspTestExtension] to load the given list of processors.
 */
@OptIn(ExperimentalCompilerApi::class)
private class KspCompileTestingComponentRegistrar(
    private val compilation: KotlinCompilation
) : ComponentRegistrar {
    var providers = emptyList()

    var options: MutableMap = mutableMapOf()

    var incremental: Boolean = false
    var incrementalLog: Boolean = false
    var allWarningsAsErrors: Boolean = false
    var withCompilation: Boolean = false

    override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
        if (providers.isEmpty()) {
            return
        }
        val options = KspOptions.Builder().apply {
            this.projectBaseDir = compilation.kspWorkingDir

            this.processingOptions.putAll(compilation.kspArgs)

            this.languageVersionSettings = configuration.languageVersionSettings
            this.incremental = [email protected]
            this.incrementalLog = [email protected]
            this.allWarningsAsErrors = [email protected]
            this.withCompilation = [email protected]

            this.cachesDir = compilation.kspCachesDir.also {
                it.deleteRecursively()
                it.mkdirs()
            }
            this.kspOutputDir = compilation.kspSourcesDir.also {
                it.deleteRecursively()
                it.mkdirs()
            }
            this.classOutputDir = compilation.kspClassesDir.also {
                it.deleteRecursively()
                it.mkdirs()
            }
            this.javaOutputDir = compilation.kspJavaSourceDir.also {
                it.deleteRecursively()
                it.mkdirs()
            }
            this.kotlinOutputDir = compilation.kspKotlinSourceDir.also {
                it.deleteRecursively()
                it.mkdirs()
            }
            this.resourceOutputDir = compilation.kspResources.also {
                it.deleteRecursively()
                it.mkdirs()
            }
            configuration[CLIConfigurationKeys.CONTENT_ROOTS]
                ?.filterIsInstance()
                ?.forEach {
                    this.javaSourceRoots.add(it.file)
                }

        }.build()

        // Temporary until friend-paths is fully supported https://youtrack.jetbrains.com/issue/KT-34102
        @Suppress("invisible_member")
        val messageCollector = PrintingMessageCollector(
            compilation.internalMessageStreamAccess,
            MessageRenderer.GRADLE_STYLE,
            compilation.verbose
        )
        val messageCollectorBasedKSPLogger = MessageCollectorBasedKSPLogger(
            messageCollector = messageCollector,
            wrappedMessageCollector = messageCollector,
            allWarningsAsErrors = allWarningsAsErrors
        )
        val registrar = KspTestExtension(options, providers, messageCollectorBasedKSPLogger)
        AnalysisHandlerExtension.registerExtension(project, registrar)
        // Dummy extension point; Required by dropPsiCaches().
        CoreApplicationEnvironment.registerExtensionPoint(project.extensionArea, PsiTreeChangeListener.EP.name, PsiTreeChangeAdapter::class.java)
    }
}

/**
 * Gets the test registrar from the plugin list or adds if it does not exist.
 */
@OptIn(ExperimentalCompilerApi::class)
private fun KotlinCompilation.getKspRegistrar(): KspCompileTestingComponentRegistrar {
    compilerPlugins.firstIsInstanceOrNull()?.let {
        return it
    }
    val kspRegistrar = KspCompileTestingComponentRegistrar(this)
    compilerPlugins = compilerPlugins + kspRegistrar
    return kspRegistrar
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy