plugability.DokkaContext.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dokka-core Show documentation
Show all versions of dokka-core Show documentation
Dokka is an API documentation engine for Kotlin and Java, performing the same function as Javadoc for Java
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.")
fun 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<*, *, *> {
// Let's filter out all suppressedExtensions that are not only overrides.
// suppressedExtensions can be polluted by suppressions that completely disables the extension, and would break dokka behaviour
// if not filtered out
val suppressedExtensionsByOverrides = suppressedExtensions.filterNot { it.value.any { it !is Suppression.ByExtension } }
val filtered = bucket.filterNot { it in suppressedExtensionsByOverrides }
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