package org.jetbrains.dokka.plugability import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.EnvironmentAndFacade import org.jetbrains.dokka.pages.PlatformData 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 platforms: Map val unusedPoints: Collection> companion object { fun create( configuration: DokkaConfiguration, logger: DokkaLogger, platforms: Map, pluginOverrides: List ): DokkaContext = DokkaContextConfigurationImpl(logger, configuration, platforms).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) } applyExtensions() }.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 addExtensionDependencies(extension: Extension<*>) } private class DokkaContextConfigurationImpl( override val logger: DokkaLogger, override val configuration: DokkaConfiguration, override val platforms: Map ) : DokkaContext, DokkaContextConfiguration { private val plugins = mutableMapOf, DokkaPlugin>() private val pluginStubs = mutableMapOf, DokkaPlugin>() internal 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; } internal val verticesWithState = mutableMapOf, State>() internal val adjacencyList: MutableMap, MutableList>> = mutableMapOf() private fun topologicalSort() { 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 } for ((vertex, state) in verticesWithState) { if (state == State.UNVISITED) visit(vertex) } result.asReversed().forEach { pointsPopulated += it.extensionPoint extensions.getOrPut(it.extensionPoint, ::mutableListOf) += it } } @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 -> { val notFallbacks = extensions.filterNot { it.isFallback } if (notFallbacks.size == 1) notFallbacks.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) } override fun addExtensionDependencies(extension: Extension<*>) { val orderDsl = OrderDsl() extension.ordering?.invoke(orderDsl) verticesWithState += extension to State.UNVISITED adjacencyList.getOrPut(extension, ::mutableListOf) += orderDsl.following.toList() orderDsl.previous.forEach { adjacencyList.getOrPut(it, ::mutableListOf) += 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" } logger.progress("Loaded plugins: $pluginNames") logger.progress("Loaded: $loadedListForDebug") } fun applyExtensions() { topologicalSort() } } 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" ) } }