diff options
3 files changed, 194 insertions, 111 deletions
diff --git a/integration/src/main/kotlin/org/jetbrains/dokka/ReflectDsl.kt b/integration/src/main/kotlin/org/jetbrains/dokka/ReflectDsl.kt new file mode 100644 index 00000000..3c9bf156 --- /dev/null +++ b/integration/src/main/kotlin/org/jetbrains/dokka/ReflectDsl.kt @@ -0,0 +1,70 @@ +package org.jetbrains.dokka + +import kotlin.reflect.* +import kotlin.reflect.jvm.isAccessible + +object ReflectDsl { + + class CallOrPropAccess(private val receiver: Any?, + private val clz: KClass<*>, + private val selector: String) { + + @Suppress("UNCHECKED_CAST") + operator fun <T : Any?> invoke(vararg a: Any?): T { + return func!!.call(receiver, *a) as T + } + + operator fun get(s: String): CallOrPropAccess { + return v<Any?>()!![s] + } + + val func: KFunction<*>? by lazy { clz.memberFunctions.find { it.name == selector } } + val prop: KProperty<*>? by lazy { clz.memberProperties.find { it.name == selector } } + + fun takeIfIsFunc(): CallOrPropAccess? = if (func != null) this else null + + fun takeIfIsProp(): CallOrPropAccess? = if (prop != null) this else null + + @Suppress("UNCHECKED_CAST") + fun <T : Any?> v(): T { + val prop = prop!! + return try { + prop.getter.apply { isAccessible = true }.call(receiver) as T + } catch (e: KotlinNullPointerException) { + // Hack around kotlin-reflect bug KT-18480 + val jclass = clz.java + val customGetterName = prop.getter.name + val getterName = if (customGetterName.startsWith("<")) "get" + prop.name.capitalize() else customGetterName + val getter = jclass.getDeclaredMethod(getterName) + getter.isAccessible = true + + getter.invoke(receiver) as T + + } + } + + @Suppress("UNCHECKED_CAST") + fun v(x: Any?) { + (prop as KMutableProperty).setter.apply { isAccessible = true }.call(receiver, x) + } + + + } + + operator fun Any.get(s: String): CallOrPropAccess { + val clz = this.javaClass.kotlin + return CallOrPropAccess(this, clz, s) + } + + operator fun Any.get(s: String, clz: Class<*>): CallOrPropAccess { + val kclz = clz.kotlin + return CallOrPropAccess(this, kclz, s) + } + + operator fun Any.get(s: String, clz: KClass<*>): CallOrPropAccess { + return CallOrPropAccess(this, clz, s) + } + + inline infix fun Any.isInstance(clz: Class<*>?): Boolean = clz != null && clz.isAssignableFrom(this.javaClass) + inline infix fun Any.isNotInstance(clz: Class<*>?): Boolean = !(this isInstance clz) +}
\ No newline at end of file diff --git a/runners/android-gradle-plugin/src/main/kotlin/mainAndroid.kt b/runners/android-gradle-plugin/src/main/kotlin/mainAndroid.kt index 5db2ad2f..8dd0a4c6 100644 --- a/runners/android-gradle-plugin/src/main/kotlin/mainAndroid.kt +++ b/runners/android-gradle-plugin/src/main/kotlin/mainAndroid.kt @@ -1,13 +1,8 @@ package org.jetbrains.dokka.gradle -import com.android.build.gradle.* -import com.android.build.gradle.internal.VariantManager import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.tasks.SourceSet import java.io.File -import kotlin.reflect.jvm.isAccessible -import kotlin.reflect.memberProperties open class DokkaAndroidPlugin : Plugin<Project> { override fun apply(project: Project) { @@ -19,50 +14,4 @@ open class DokkaAndroidPlugin : Plugin<Project> { } } -open class DokkaAndroidTask : DokkaTask() { - override val sdkProvider: SdkProvider? = AndroidSdkProvider(project) -} - -private class AndroidSdkProvider(private val project: Project) : SdkProvider { - private val ext: BaseExtension? by lazy { - project.extensions.findByType(LibraryExtension::class.java) - ?: project.extensions.findByType(AppExtension::class.java) - ?: project.extensions.findByType(TestExtension::class.java) - } - - private val isAndroidProject: Boolean get() = ext != null - - private val variantManager: VariantManager? by lazy { - val plugin = (project.plugins.findPlugin("android") - ?: project.plugins.findPlugin("android-library") - ?: project.plugins.findPlugin("com.android.test") - ?: throw Exception("Android plugin not found, please use dokka-android with android or android-library plugin.")) as BasePlugin - plugin.javaClass.kotlin.memberProperties - .find { it.name == "variantManager" } - ?.apply { isAccessible = true } - ?.let { it.get(plugin) as VariantManager } - ?: plugin.variantManager - } - - private val allVariantsClassPath by lazy { - try { - variantManager?.variantDataList?.flatMap { it.variantConfiguration.compileClasspath }!! - } catch(e: Exception) { - throw Exception("Unsupported version of android build tools, could not access variant manager.", e) - } - } - - override val name: String = "android" - - override val isValid: Boolean - get() = isAndroidProject - - override val classpath: List<File> - get() = ext?.bootClasspath.orEmpty() + allVariantsClassPath - - override val sourceDirs: Set<File>? - get() { - val sourceSet = ext?.sourceSets?.findByName(SourceSet.MAIN_SOURCE_SET_NAME) - return sourceSet?.java?.srcDirs - } -} +open class DokkaAndroidTask : DokkaTask() diff --git a/runners/gradle-plugin/src/main/kotlin/main.kt b/runners/gradle-plugin/src/main/kotlin/main.kt index 61c83de4..bd4b3ab3 100644 --- a/runners/gradle-plugin/src/main/kotlin/main.kt +++ b/runners/gradle-plugin/src/main/kotlin/main.kt @@ -4,15 +4,14 @@ import groovy.lang.Closure import org.gradle.api.DefaultTask import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.Task import org.gradle.api.file.FileCollection import org.gradle.api.plugins.JavaBasePlugin import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.* import org.gradle.api.tasks.Optional -import org.jetbrains.dokka.DokkaBootstrap -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.SerializeOnlyDokkaConfiguration -import org.jetbrains.dokka.automagicTypedProxy +import org.jetbrains.dokka.* +import org.jetbrains.dokka.ReflectDsl.isNotInstance import org.jetbrains.dokka.gradle.ClassloaderContainer.fatJarClassLoader import org.jetbrains.dokka.gradle.DokkaVersion.version import ru.yole.jkid.JsonExclude @@ -52,9 +51,24 @@ object ClassloaderContainer { } open class DokkaTask : DefaultTask() { + + fun defaultKotlinTasks() = with(ReflectDsl) { + + val abstractKotlinCompileClz = try { + project.buildscript.classLoader.loadClass(ABSTRACT_KOTLIN_COMPILE) + } catch (cnfe: ClassNotFoundException) { + return@with emptyList<Task>() + } + + return@with project.tasks.filter { it isInstance abstractKotlinCompileClz }.filter { "Test" !in it.name } + } + init { group = JavaBasePlugin.DOCUMENTATION_GROUP description = "Generates dokka documentation for Kotlin" + project.afterEvaluate { + this.dependsOn(kotlinTasks.flatMap { it.dependsOn }) + } } @Input @@ -62,8 +76,13 @@ open class DokkaTask : DefaultTask() { @Input var outputFormat: String = "html" var outputDirectory: String = "" - @Input - var processConfigurations: List<Any?> = arrayListOf("compile") + + + @Deprecated("Going to be removed in 0.9.16, use classpath + sourceDirs instead if kotlinTasks is not suitable for you") + @Input var processConfigurations: List<Any?> = emptyList() + + @Input var classpath: List<File> = arrayListOf() + @Input var includes: List<Any?> = arrayListOf() @Input @@ -92,7 +111,16 @@ open class DokkaTask : DefaultTask() { @Optional @Input var cacheRoot: String? = null - protected open val sdkProvider: SdkProvider? = null + @get:Input + internal val kotlinCompileBasedClasspathAndSourceRoots: ClasspathAndSourceRoots by lazy { extractClasspathAndSourceRootsFromKotlinTasks() } + + + private var kotlinTasksConfigurator: () -> List<Any?>? = { defaultKotlinTasks() } + private val kotlinTasks: List<Task> by lazy { extractKotlinCompileTasks() } + + fun kotlinTasks(closure: Closure<Any?>) { + kotlinTasksConfigurator = { closure.call() as? List<Any?> } + } fun linkMapping(closure: Closure<Any?>) { val mapping = LinkMapping() @@ -151,6 +179,56 @@ open class DokkaTask : DefaultTask() { } } + internal data class ClasspathAndSourceRoots(val classpath: List<File>, val sourceRoots: List<File>) : Serializable + + private fun extractKotlinCompileTasks(): List<Task> { + val inputList = (kotlinTasksConfigurator.invoke() ?: emptyList()).filterNotNull() + val (paths, other) = inputList.partition { it is String } + + val taskContainer = project.tasks + + val tasksByPath = paths.map { taskContainer.findByPath(it as String) ?: throw IllegalArgumentException("Task with path '$it' not found") } + + other + .filter { it !is Task || it isNotInstance getAbstractKotlinCompileFor(it) } + .forEach { throw IllegalArgumentException("Illegal entry in kotlinTasks, must be subtype of $ABSTRACT_KOTLIN_COMPILE or String, but was $it") } + + tasksByPath + .filter { it == null || it isNotInstance getAbstractKotlinCompileFor(it) } + .forEach { throw IllegalArgumentException("Illegal task path in kotlinTasks, must be subtype of $ABSTRACT_KOTLIN_COMPILE, but was $it") } + + + return (tasksByPath + other) as List<Task> + } + + private fun extractClasspathAndSourceRootsFromKotlinTasks(): ClasspathAndSourceRoots { + + val allTasks = kotlinTasks + + val allClasspath = mutableSetOf<File>() + val allSourceRoots = mutableSetOf<File>() + + allTasks.forEach { + + logger.debug("Dokka found AbstractKotlinCompile task: $it") + with(ReflectDsl) { + val taskSourceRoots: List<File> = it["sourceRootsContainer"]["sourceRoots"].v() + + val abstractKotlinCompileClz = getAbstractKotlinCompileFor(it)!! + + val taskClasspath: Iterable<File> = + (it["compileClasspath", abstractKotlinCompileClz].takeIfIsProp()?.v() ?: + it["getClasspath", abstractKotlinCompileClz]()) + + allClasspath += taskClasspath.filter { it.exists() } + allSourceRoots += taskSourceRoots.filter { it.exists() } + } + } + + return ClasspathAndSourceRoots(allClasspath.toList(), allSourceRoots.toList()) + } + + private fun Iterable<File>.toSourceRoots(): List<SourceRoot> = this.filter { it.exists() }.map { SourceRoot().apply { path = it.path } } @TaskAction fun generate() { @@ -159,22 +237,18 @@ open class DokkaTask : DefaultTask() { try { loadFatJar() - val project = project - val sdkProvider = sdkProvider - val sourceRoots = collectSourceRoots() - val allConfigurations = project.configurations + val (tasksClasspath, tasksSourceRoots) = kotlinCompileBasedClasspathAndSourceRoots - val classpath = - if (sdkProvider != null && sdkProvider.isValid) sdkProvider.classpath else emptyList<File>() + - processConfigurations - .map { allConfigurations?.getByName(it.toString()) ?: throw IllegalArgumentException("No configuration $it found") } - .flatMap { it } + val project = project + val sourceRoots = collectSourceRoots() + tasksSourceRoots.toSourceRoots() if (sourceRoots.isEmpty()) { logger.warn("No source directories found: skipping dokka generation") return } + val fullClasspath = collectClasspathFromOldSources() + tasksClasspath + classpath + val bootstrapClass = fatJarClassLoader!!.loadClass("org.jetbrains.dokka.DokkaBootstrapImpl") val bootstrapInstance = bootstrapClass.constructors.first().newInstance() @@ -183,7 +257,7 @@ open class DokkaTask : DefaultTask() { val configuration = SerializeOnlyDokkaConfiguration( moduleName, - classpath.map { it.absolutePath }, + fullClasspath.map { it.absolutePath }, sourceRoots, samples.filterNotNull().map { project.file(it).absolutePath }, includes.filterNotNull().map { project.file(it).absolutePath }, @@ -222,28 +296,40 @@ open class DokkaTask : DefaultTask() { } } - fun collectSourceRoots(): List<SourceRoot> { - val provider = sdkProvider + private fun collectClasspathFromOldSources(): List<File> { + + val allConfigurations = project.configurations + + val fromConfigurations = + processConfigurations.map { + allConfigurations?.getByName(it.toString()) ?: throw IllegalArgumentException("No configuration $it found") + }.flatten() + + return fromConfigurations + } + + private fun collectSourceRoots(): List<SourceRoot> { val sourceDirs = if (sourceDirs.any()) { logger.info("Dokka: Taking source directories provided by the user") sourceDirs.toSet() - } else if (provider != null && provider.isValid) { - logger.info("Dokka: Taking source directories from ${provider.name} sdk provider") - provider.sourceDirs - } else { + } else if (kotlinTasks.isEmpty()) { logger.info("Dokka: Taking source directories from default java plugin") val javaPluginConvention = project.convention.getPlugin(JavaPluginConvention::class.java) val sourceSets = javaPluginConvention.sourceSets?.findByName(SourceSet.MAIN_SOURCE_SET_NAME) sourceSets?.allSource?.srcDirs + } else { + emptySet() } - return sourceRoots + (sourceDirs?.filter { it.exists() }?.map { SourceRoot().apply { path = it.path } } ?: emptyList()) + return sourceRoots + (sourceDirs?.toSourceRoots() ?: emptyList()) } - @InputFiles + @SkipWhenEmpty + @InputFiles fun getInputFiles(): FileCollection = - project.files(collectSourceRoots().map { project.fileTree(File(it.path)) }) + + project.files(kotlinCompileBasedClasspathAndSourceRoots.sourceRoots.map { project.fileTree(File(it.path)) }) + + project.files(collectSourceRoots().map { project.fileTree(File(it.path)) }) + project.files(includes) + project.files(samples.map { project.fileTree(it) }) @@ -252,6 +338,13 @@ open class DokkaTask : DefaultTask() { companion object { const val COLORS_ENABLED_PROPERTY = "kotlin.colors.enabled" + const val ABSTRACT_KOTLIN_COMPILE = "org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile" + + private fun getAbstractKotlinCompileFor(task: Task) = try { + task.project.buildscript.classLoader.loadClass(ABSTRACT_KOTLIN_COMPILE) + } catch (e: ClassNotFoundException) { + null + } } } @@ -262,6 +355,10 @@ class SourceRoot : DokkaConfiguration.SourceRoot { } override var platforms: List<String> = arrayListOf() + + override fun toString(): String { + return "${platforms.joinToString()}::$path" + } } open class LinkMapping : Serializable, DokkaConfiguration.SourceLinkDefinition { @@ -315,36 +412,3 @@ class PackageOptions : DokkaConfiguration.PackageOptions { override var reportUndocumented: Boolean = true override var skipDeprecated: Boolean = false } -/** - * A provider for SDKs that can be used if a project uses classes that live outside the JDK or uses a - * different method to determine the source directories. - * - * For example an Android library project configures its sources through the Android extension instead - * of the basic java convention. Also it has its custom classes located in the SDK installation directory. - */ -interface SdkProvider { - /** - * The name of this provider. Only used for logging purposes. - */ - val name: String - - /** - * Checks whether this provider has everything it needs to provide the source directories. - */ - val isValid: Boolean - - /** - * Provides additional classpath files where Dokka should search for external classes. - * The file list is injected **after** JDK Jars and **before** project dependencies. - * - * This is only called if [isValid] returns `true`. - */ - val classpath: List<File> - - /** - * Provides a list of directories where Dokka should search for source files. - * - * This is only called if [isValid] returns `true`. - */ - val sourceDirs: Set<File>? -} |