diff options
author | Paweł Marks <pmarks@virtuslab.com> | 2020-07-17 16:36:09 +0200 |
---|---|---|
committer | Paweł Marks <pmarks@virtuslab.com> | 2020-07-17 16:36:09 +0200 |
commit | 6996b1135f61c7d2cb60b0652c6a2691dda31990 (patch) | |
tree | d568096c25e31c28d14d518a63458b5a7526b896 /core/src/main/kotlin/plugability | |
parent | de56cab76f556e5b4af0b8c8cb08d8b482b86d0a (diff) | |
parent | 1c3530dcbb50c347f80bef694829dbefe89eca77 (diff) | |
download | dokka-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.kt | 0 | ||||
-rw-r--r-- | core/src/main/kotlin/plugability/DokkaContext.kt | 223 | ||||
-rw-r--r-- | core/src/main/kotlin/plugability/DokkaPlugin.kt | 82 | ||||
-rw-r--r-- | core/src/main/kotlin/plugability/LazyEvaluated.kt | 16 | ||||
-rw-r--r-- | core/src/main/kotlin/plugability/extensions.kt | 87 |
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 |