aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/kotlin/plugability
diff options
context:
space:
mode:
authorPaweł Marks <pmarks@virtuslab.com>2020-07-17 16:36:09 +0200
committerPaweł Marks <pmarks@virtuslab.com>2020-07-17 16:36:09 +0200
commit6996b1135f61c7d2cb60b0652c6a2691dda31990 (patch)
treed568096c25e31c28d14d518a63458b5a7526b896 /core/src/main/kotlin/plugability
parentde56cab76f556e5b4af0b8c8cb08d8b482b86d0a (diff)
parent1c3530dcbb50c347f80bef694829dbefe89eca77 (diff)
downloaddokka-6996b1135f61c7d2cb60b0652c6a2691dda31990.tar.gz
dokka-6996b1135f61c7d2cb60b0652c6a2691dda31990.tar.bz2
dokka-6996b1135f61c7d2cb60b0652c6a2691dda31990.zip
Merge branch 'dev-0.11.0'
Diffstat (limited to 'core/src/main/kotlin/plugability')
-rw-r--r--core/src/main/kotlin/plugability/DefaultExtensions.kt0
-rw-r--r--core/src/main/kotlin/plugability/DokkaContext.kt223
-rw-r--r--core/src/main/kotlin/plugability/DokkaPlugin.kt82
-rw-r--r--core/src/main/kotlin/plugability/LazyEvaluated.kt16
-rw-r--r--core/src/main/kotlin/plugability/extensions.kt87
5 files changed, 408 insertions, 0 deletions
diff --git a/core/src/main/kotlin/plugability/DefaultExtensions.kt b/core/src/main/kotlin/plugability/DefaultExtensions.kt
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/core/src/main/kotlin/plugability/DefaultExtensions.kt
diff --git a/core/src/main/kotlin/plugability/DokkaContext.kt b/core/src/main/kotlin/plugability/DokkaContext.kt
new file mode 100644
index 00000000..323039e9
--- /dev/null
+++ b/core/src/main/kotlin/plugability/DokkaContext.kt
@@ -0,0 +1,223 @@
+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 <T : DokkaPlugin> plugin(kclass: KClass<T>): T?
+
+ operator fun <T, E> get(point: E): List<T>
+ where T : Any, E : ExtensionPoint<T>
+
+ fun <T, E> single(point: E): T where T : Any, E : ExtensionPoint<T>
+
+ val logger: DokkaLogger
+ val configuration: DokkaConfiguration
+ val unusedPoints: Collection<ExtensionPoint<*>>
+
+
+ companion object {
+ fun create(
+ configuration: DokkaConfiguration,
+ logger: DokkaLogger,
+ pluginOverrides: List<DokkaPlugin>
+ ): 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 <reified T : DokkaPlugin> 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<KClass<*>, DokkaPlugin>()
+ private val pluginStubs = mutableMapOf<KClass<*>, DokkaPlugin>()
+ val extensions = mutableMapOf<ExtensionPoint<*>, MutableList<Extension<*, *, *>>>()
+ val pointsUsed: MutableSet<ExtensionPoint<*>> = mutableSetOf()
+ val pointsPopulated: MutableSet<ExtensionPoint<*>> = mutableSetOf()
+ override val unusedPoints: Set<ExtensionPoint<*>>
+ 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<Extension<*, *, *>>()
+ private val rawAdjacencyList = mutableMapOf<Extension<*, *, *>, MutableList<Extension<*, *, *>>>()
+ private val suppressedExtensions = mutableMapOf<Extension<*, *, *>, MutableList<Suppression>>()
+
+ 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<Extension<*, *, *>> = 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<Extension<*, *, *>, Set<Extension<*, *, *>>> {
+ val buckets = rawExtensions.associateWithTo(mutableMapOf()) { setOf(it) }
+ suppressedExtensions.forEach { (extension, suppressions) ->
+ val mergedBucket = suppressions.filterIsInstance<Suppression.ByExtension>()
+ .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<*, *, *>>): Extension<*, *, *> {
+ val filtered = bucket.filter { it !in suppressedExtensions }
+ return filtered.singleOrNull() ?: throw IllegalStateException("Conflicting overrides: $filtered")
+ }
+
+ private fun translateAdjacencyList(
+ overridesInfo: Map<Extension<*, *, *>, Set<Extension<*, *, *>>>
+ ): Map<Extension<*, *, *>, List<Extension<*, *, *>>> {
+ 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 <T, E> get(point: E) where T : Any, E : ExtensionPoint<T> =
+ actions(point).also { pointsUsed += point }.orEmpty() as List<T>
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T, E> single(point: E): T where T : Any, E : ExtensionPoint<T> {
+ 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<Extension<T, *, *>>
+ return when (extensions.size) {
+ 0 -> throwBadArity("none was")
+ 1 -> extensions.single().action.get(this)
+ else -> throwBadArity("many were")
+ }
+ }
+
+ private fun <E : ExtensionPoint<*>> actions(point: E) = extensions[point]?.map { it.action.get(this) }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : DokkaPlugin> plugin(kclass: KClass<T>) = (plugins[kclass] ?: pluginStubFor(kclass)) as T
+
+ private fun <T : DokkaPlugin> pluginStubFor(kclass: KClass<T>): 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) {
+ suppressedExtensions.listFor(extension.override.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 <K, V> MutableMap<K, MutableList<V>>.listFor(key: K) = getOrPut(key, ::mutableListOf)
diff --git a/core/src/main/kotlin/plugability/DokkaPlugin.kt b/core/src/main/kotlin/plugability/DokkaPlugin.kt
new file mode 100644
index 00000000..2c755a49
--- /dev/null
+++ b/core/src/main/kotlin/plugability/DokkaPlugin.kt
@@ -0,0 +1,82 @@
+package org.jetbrains.dokka.plugability
+
+import com.google.gson.Gson
+import org.jetbrains.dokka.DokkaConfiguration
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+import kotlin.reflect.KProperty1
+import kotlin.reflect.full.createInstance
+
+abstract class DokkaPlugin {
+ private val extensionDelegates = mutableListOf<KProperty<*>>()
+
+ @PublishedApi
+ internal var context: DokkaContext? = null
+
+ protected inline fun <reified T : DokkaPlugin> plugin(): T = context?.plugin(T::class) ?: throwIllegalQuery()
+
+ protected fun <T : Any> extensionPoint() =
+ object : ReadOnlyProperty<DokkaPlugin, ExtensionPoint<T>> {
+ override fun getValue(thisRef: DokkaPlugin, property: KProperty<*>) = ExtensionPoint<T>(
+ thisRef::class.qualifiedName ?: throw AssertionError("Plugin must be named class"),
+ property.name
+ )
+ }
+
+ protected fun <T : Any> extending(definition: ExtendingDSL.() -> Extension<T, *, *>) = ExtensionProvider(definition)
+
+ protected class ExtensionProvider<T : Any> internal constructor(
+ private val definition: ExtendingDSL.() -> Extension<T, *, *>
+ ) {
+ operator fun provideDelegate(thisRef: DokkaPlugin, property: KProperty<*>) = lazy {
+ ExtendingDSL(
+ thisRef::class.qualifiedName ?: throw AssertionError("Plugin must be named class"),
+ property.name
+ ).definition()
+ }.also { thisRef.extensionDelegates += property }
+ }
+
+ internal fun internalInstall(ctx: DokkaContextConfiguration, configuration: DokkaConfiguration) {
+ extensionDelegates.asSequence()
+ .filterIsInstance<KProperty1<DokkaPlugin, Extension<*, *, *>>>() // should be always true
+ .map { it.get(this) }
+ .forEach { if (configuration.(it.condition)()) ctx.installExtension(it) }
+ }
+}
+
+interface WithUnsafeExtensionSuppression {
+ val extensionsSuppressed: List<Extension<*, *, *>>
+}
+
+interface Configurable {
+ val pluginsConfiguration: Map<String, String>
+}
+
+interface ConfigurableBlock
+
+inline fun <reified P : DokkaPlugin, reified T : ConfigurableBlock> Configurable.pluginConfiguration(block: T.() -> Unit) {
+ val instance = T::class.createInstance().apply(block)
+
+ val mutablePluginsConfiguration = pluginsConfiguration as MutableMap<String, String>
+ mutablePluginsConfiguration[P::class.qualifiedName!!] = Gson().toJson(instance, T::class.java)
+}
+
+inline fun <reified P : DokkaPlugin, reified E : Any> P.query(extension: P.() -> ExtensionPoint<E>): List<E> =
+ context?.let { it[extension()] } ?: throwIllegalQuery()
+
+inline fun <reified P : DokkaPlugin, reified E : Any> P.querySingle(extension: P.() -> ExtensionPoint<E>): E =
+ context?.single(extension()) ?: throwIllegalQuery()
+
+fun throwIllegalQuery(): Nothing =
+ throw IllegalStateException("Querying about plugins is only possible with dokka context initialised")
+
+inline fun <reified T : DokkaPlugin, reified R : ConfigurableBlock> configuration(context: DokkaContext): ReadOnlyProperty<Any?, R> {
+ return object : ReadOnlyProperty<Any?, R> {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): R {
+ return context.configuration.pluginsConfiguration[T::class.qualifiedName
+ ?: throw AssertionError("Plugin must be named class")].let {
+ Gson().fromJson(it, R::class.java)
+ }
+ }
+ }
+}
diff --git a/core/src/main/kotlin/plugability/LazyEvaluated.kt b/core/src/main/kotlin/plugability/LazyEvaluated.kt
new file mode 100644
index 00000000..c0c271f4
--- /dev/null
+++ b/core/src/main/kotlin/plugability/LazyEvaluated.kt
@@ -0,0 +1,16 @@
+package org.jetbrains.dokka.plugability
+
+internal class LazyEvaluated<T : Any> private constructor(private val recipe: ((DokkaContext) -> T)? = null, private var value: T? = null) {
+
+ internal fun get(context: DokkaContext): T {
+ if(value == null) {
+ value = recipe?.invoke(context)
+ }
+ return value ?: throw AssertionError("Incorrect initialized LazyEvaluated instance")
+ }
+
+ companion object {
+ fun <T : Any> fromInstance(value: T) = LazyEvaluated(value = value)
+ fun <T : Any> fromRecipe(recipe: (DokkaContext) -> T) = LazyEvaluated(recipe = recipe)
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/plugability/extensions.kt b/core/src/main/kotlin/plugability/extensions.kt
new file mode 100644
index 00000000..c6dd0b85
--- /dev/null
+++ b/core/src/main/kotlin/plugability/extensions.kt
@@ -0,0 +1,87 @@
+package org.jetbrains.dokka.plugability
+
+import org.jetbrains.dokka.DokkaConfiguration
+
+data class ExtensionPoint<T : Any> internal constructor(
+ internal val pluginClass: String,
+ internal val pointName: String
+) {
+ override fun toString() = "ExtensionPoint: $pluginClass/$pointName"
+}
+
+sealed class OrderingKind {
+ object None : OrderingKind()
+ class ByDsl(val block: (OrderDsl.() -> Unit)) : OrderingKind()
+}
+
+sealed class OverrideKind {
+ object None : OverrideKind()
+ class Present(val overriden: Extension<*, *, *>) : OverrideKind()
+}
+
+class Extension<T : Any, Ordering : OrderingKind, Override : OverrideKind> internal constructor(
+ internal val extensionPoint: ExtensionPoint<T>,
+ internal val pluginClass: String,
+ internal val extensionName: String,
+ internal val action: LazyEvaluated<T>,
+ internal val ordering: Ordering,
+ internal val override: Override,
+ internal val conditions: List<DokkaConfiguration.() -> Boolean>
+) {
+ override fun toString() = "Extension: $pluginClass/$extensionName"
+
+ override fun equals(other: Any?) =
+ if (other is Extension<*, *, *>) this.pluginClass == other.pluginClass && this.extensionName == other.extensionName
+ else false
+
+ override fun hashCode() = listOf(pluginClass, extensionName).hashCode()
+
+ val condition: DokkaConfiguration.() -> Boolean
+ get() = { conditions.all { it(this) } }
+}
+
+private fun <T : Any> Extension(
+ extensionPoint: ExtensionPoint<T>,
+ pluginClass: String,
+ extensionName: String,
+ action: LazyEvaluated<T>
+) = Extension(extensionPoint, pluginClass, extensionName, action, OrderingKind.None, OverrideKind.None, emptyList())
+
+@DslMarker
+annotation class ExtensionsDsl
+
+@ExtensionsDsl
+class ExtendingDSL(private val pluginClass: String, private val extensionName: String) {
+
+ infix fun <T : Any> ExtensionPoint<T>.with(action: T) =
+ Extension(this, this@ExtendingDSL.pluginClass, extensionName, LazyEvaluated.fromInstance(action))
+
+ infix fun <T : Any> ExtensionPoint<T>.providing(action: (DokkaContext) -> T) =
+ Extension(this, this@ExtendingDSL.pluginClass, extensionName, LazyEvaluated.fromRecipe(action))
+
+ infix fun <T : Any, Override : OverrideKind> Extension<T, OrderingKind.None, Override>.order(
+ block: OrderDsl.() -> Unit
+ ) = Extension(extensionPoint, pluginClass, extensionName, action, OrderingKind.ByDsl(block), override, conditions)
+
+ infix fun <T : Any, Override : OverrideKind, Ordering: OrderingKind> Extension<T, Ordering, Override>.applyIf(
+ condition: DokkaConfiguration.() -> Boolean
+ ) = Extension(extensionPoint, pluginClass, extensionName, action, ordering, override, conditions + condition)
+
+ infix fun <T : Any, Override : OverrideKind, Ordering: OrderingKind> Extension<T, Ordering, Override>.override(
+ overriden: Extension<T, *, *>
+ ) = Extension(extensionPoint, pluginClass, extensionName, action, ordering, OverrideKind.Present(overriden), conditions)
+}
+
+@ExtensionsDsl
+class OrderDsl {
+ internal val previous = mutableSetOf<Extension<*, *, *>>()
+ internal val following = mutableSetOf<Extension<*, *, *>>()
+
+ fun after(vararg extensions: Extension<*, *, *>) {
+ previous += extensions
+ }
+
+ fun before(vararg extensions: Extension<*, *, *>) {
+ following += extensions
+ }
+} \ No newline at end of file