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

plugability.DokkaContext.kt Maven / Gradle / Ivy

Go to download

Dokka is an API documentation engine for Kotlin and Java, performing the same function as Javadoc for Java

There is a newer version: 2.0.0
Show newest version
package org.jetbrains.dokka.plugability

import org.jetbrains.dokka.DokkaConfiguration
import org.jetbrains.dokka.utilities.DokkaLogger
import java.io.File
import java.net.URLClassLoader
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance

interface DokkaContext {
    fun  plugin(kclass: KClass): T?

    operator fun  get(point: E): List
            where T : Any, E : ExtensionPoint

    fun  single(point: E): T where T : Any, E : ExtensionPoint

    val logger: DokkaLogger
    val configuration: DokkaConfiguration
    val unusedPoints: Collection>

    companion object {
        fun create(
            configuration: DokkaConfiguration,
            logger: DokkaLogger,
            pluginOverrides: List
        ): DokkaContext =
            DokkaContextConfigurationImpl(logger, configuration).apply {
                // File(it.path) is a workaround for an incorrect filesystem in a File instance returned by Gradle.
                configuration.pluginsClasspath.map { File(it.path).toURI().toURL() }
                    .toTypedArray()
                    .let { URLClassLoader(it, this.javaClass.classLoader) }
                    .also { checkClasspath(it) }
                    .let { ServiceLoader.load(DokkaPlugin::class.java, it) }
                    .let { it + pluginOverrides }
                    .forEach { install(it) }
                topologicallySortAndPrune()
            }.also { it.logInitialisationInfo() }
    }
}

inline fun  DokkaContext.plugin(): T = plugin(T::class)
    ?: throw java.lang.IllegalStateException("Plugin ${T::class.qualifiedName} is not present in context.")

interface DokkaContextConfiguration {
    fun installExtension(extension: Extension<*, *, *>)
}

private class DokkaContextConfigurationImpl(
    override val logger: DokkaLogger,
    override val configuration: DokkaConfiguration
) : DokkaContext, DokkaContextConfiguration {
    private val plugins = mutableMapOf, DokkaPlugin>()
    private val pluginStubs = mutableMapOf, DokkaPlugin>()
    val extensions = mutableMapOf, MutableList>>()
    val pointsUsed: MutableSet> = mutableSetOf()
    val pointsPopulated: MutableSet> = mutableSetOf()
    override val unusedPoints: Set>
        get() = pointsPopulated - pointsUsed

    private enum class State {
        UNVISITED,
        VISITING,
        VISITED;
    }

    private sealed class Suppression {
        data class ByExtension(val extension: Extension<*, *, *>) : Suppression() {
            override fun toString() = extension.toString()
        }

        data class ByPlugin(val plugin: DokkaPlugin) : Suppression() {
            override fun toString() = "Plugin ${plugin::class.qualifiedName}"
        }
    }

    private val rawExtensions = mutableListOf>()
    private val rawAdjacencyList = mutableMapOf, MutableList>>()
    private val suppressedExtensions = mutableMapOf, MutableList>()

    fun topologicallySortAndPrune() {
        pointsPopulated.clear()
        extensions.clear()

        val overridesInfo = processOverrides()
        val extensionsToSort = overridesInfo.keys
        val adjacencyList = translateAdjacencyList(overridesInfo)

        val verticesWithState = extensionsToSort.associateWithTo(mutableMapOf()) { State.UNVISITED }
        val result: MutableList> = mutableListOf()

        fun visit(n: Extension<*, *, *>) {
            val state = verticesWithState[n]
            if (state == State.VISITED)
                return
            if (state == State.VISITING)
                throw Error("Detected cycle in plugins graph")
            verticesWithState[n] = State.VISITING
            adjacencyList[n]?.forEach { visit(it) }
            verticesWithState[n] = State.VISITED
            result += n
        }

        extensionsToSort.forEach(::visit)

        val filteredResult = result.asReversed().filterNot { it in suppressedExtensions }

        filteredResult.mapTo(pointsPopulated) { it.extensionPoint }
        filteredResult.groupByTo(extensions) { it.extensionPoint }
    }

    private fun processOverrides(): Map, Set>> {
        val buckets = rawExtensions.associateWithTo(mutableMapOf()) { setOf(it) }
        suppressedExtensions.forEach { (extension, suppressions) ->
            val mergedBucket = suppressions.filterIsInstance()
                .map { it.extension }
                .plus(extension)
                .flatMap { buckets[it].orEmpty() }
                .toSet()
            mergedBucket.forEach { buckets[it] = mergedBucket }
        }
        return buckets.values.distinct().associateBy(::findNotOverridden)
    }

    private fun findNotOverridden(bucket: Set>): Extension<*, *, *> {
        val filtered = bucket.filter { it !in suppressedExtensions }
        return filtered.singleOrNull()
            ?: throw IllegalStateException("Conflicting overrides: $filtered")
    }

    private fun translateAdjacencyList(
        overridesInfo: Map, Set>>
    ): Map, List>> {
        val reverseOverrideInfo = overridesInfo.flatMap { (ext, set) -> set.map { it to ext } }.toMap()
        return rawAdjacencyList.mapNotNull { (ext, list) ->
            reverseOverrideInfo[ext]?.to(list.mapNotNull { reverseOverrideInfo[it] })
        }.toMap()
    }

    @Suppress("UNCHECKED_CAST")
    override operator fun  get(point: E) where T : Any, E : ExtensionPoint =
        actions(point).also { pointsUsed += point }.orEmpty() as List

    @Suppress("UNCHECKED_CAST")
    override fun  single(point: E): T where T : Any, E : ExtensionPoint {
        fun throwBadArity(substitution: String): Nothing = throw IllegalStateException(
            "$point was expected to have exactly one extension registered, but $substitution found."
        )
        pointsUsed += point

        val extensions = extensions[point].orEmpty() as List>
        return when (extensions.size) {
            0 -> throwBadArity("none was")
            1 -> extensions.single().action.get(this)
            else -> throwBadArity("many were")
        }
    }

    private fun > actions(point: E) = extensions[point]?.map { it.action.get(this) }

    @Suppress("UNCHECKED_CAST")
    override fun  plugin(kclass: KClass) = (plugins[kclass] ?: pluginStubFor(kclass)) as T

    private fun  pluginStubFor(kclass: KClass): DokkaPlugin =
        pluginStubs.getOrPut(kclass) { kclass.createInstance().also { it.context = this } }

    fun install(plugin: DokkaPlugin) {
        plugins[plugin::class] = plugin
        plugin.context = this
        plugin.internalInstall(this, this.configuration)

        if (plugin is WithUnsafeExtensionSuppression) {
            plugin.extensionsSuppressed.forEach {
                suppressedExtensions.listFor(it) += Suppression.ByPlugin(plugin)
            }
        }
    }

    override fun installExtension(extension: Extension<*, *, *>) {
        rawExtensions += extension

        if (extension.ordering is OrderingKind.ByDsl) {
            val orderDsl = OrderDsl()
            orderDsl.(extension.ordering.block)()

            rawAdjacencyList.listFor(extension) += orderDsl.following.toList()
            orderDsl.previous.forEach { rawAdjacencyList.listFor(it) += extension }
        }

        if (extension.override is OverrideKind.Present) {
            fun root(ext: Extension<*, *, *>): List> = if (ext.override is OverrideKind.Present) ext.override.overriden.flatMap(::root) else listOf(ext)
            if (extension.override.overriden.size > 1 && root(extension).distinct().size > 1)
                throw IllegalStateException("Extension $extension overrides extensions without common root")
            extension.override.overriden.forEach { overriden ->
                suppressedExtensions.listFor(overriden) += Suppression.ByExtension(extension)
            }
        }
    }

    fun logInitialisationInfo() {
        val pluginNames = plugins.values.map { it::class.qualifiedName.toString() }

        val loadedListForDebug = extensions.run { keys + values.flatten() }.toList()
            .joinToString(prefix = "[\n", separator = ",\n", postfix = "\n]") { "\t$it" }

        val suppressedList = suppressedExtensions.asSequence()
            .joinToString(prefix = "[\n", separator = ",\n", postfix = "\n]") {
                "\t${it.key} by " + (it.value.singleOrNull() ?: it.value)
            }

        logger.info("Loaded plugins: $pluginNames")
        logger.info("Loaded: $loadedListForDebug")
        logger.info("Suppressed: $suppressedList")
    }
}

private fun checkClasspath(classLoader: URLClassLoader) {
    classLoader.findResource(DokkaContext::class.java.name.replace('.', '/') + ".class")?.also {
        throw AssertionError(
            "Dokka API found on plugins classpath. This will lead to subtle bugs. " +
                    "Please fix your plugins dependencies or exclude dokka api artifact from plugin classpath"
        )
    }
}

private fun  MutableMap>.listFor(key: K) = getOrPut(key, ::mutableListOf)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy