diff options
Diffstat (limited to 'dokka-runners')
83 files changed, 7515 insertions, 0 deletions
diff --git a/dokka-runners/runner-cli/api/runner-cli.api b/dokka-runners/runner-cli/api/runner-cli.api new file mode 100644 index 00000000..cfa173c5 --- /dev/null +++ b/dokka-runners/runner-cli/api/runner-cli.api @@ -0,0 +1,100 @@ +public final class org/jetbrains/dokka/ArgTypeArgument : kotlinx/cli/ArgType { + public fun <init> (Lkotlinx/cli/CLIEntity;)V + public final fun component1 ()Lkotlinx/cli/CLIEntity; + public synthetic fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; + public fun convert (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet; + public final fun copy (Lkotlinx/cli/CLIEntity;)Lorg/jetbrains/dokka/ArgTypeArgument; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/ArgTypeArgument;Lkotlinx/cli/CLIEntity;ILjava/lang/Object;)Lorg/jetbrains/dokka/ArgTypeArgument; + public fun equals (Ljava/lang/Object;)Z + public fun getDescription ()Ljava/lang/String; + public final fun getModuleName ()Lkotlinx/cli/CLIEntity; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/ArgTypeFile : kotlinx/cli/ArgType { + public static final field INSTANCE Lorg/jetbrains/dokka/ArgTypeFile; + public fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/io/File; + public synthetic fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; + public fun getDescription ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/ArgTypeHelpSourceSet : kotlinx/cli/ArgType { + public fun <init> (Lkotlinx/cli/CLIEntity;)V + public final fun component1 ()Lkotlinx/cli/CLIEntity; + public fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; + public final fun copy (Lkotlinx/cli/CLIEntity;)Lorg/jetbrains/dokka/ArgTypeHelpSourceSet; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/ArgTypeHelpSourceSet;Lkotlinx/cli/CLIEntity;ILjava/lang/Object;)Lorg/jetbrains/dokka/ArgTypeHelpSourceSet; + public fun equals (Ljava/lang/Object;)Z + public fun getDescription ()Ljava/lang/String; + public final fun getModuleName ()Lkotlinx/cli/CLIEntity; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/ArgTypePlatform : kotlinx/cli/ArgType { + public static final field INSTANCE Lorg/jetbrains/dokka/ArgTypePlatform; + public synthetic fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; + public fun convert (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/Platform; + public fun getDescription ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/ArgTypePlugin : kotlinx/cli/ArgType { + public static final field INSTANCE Lorg/jetbrains/dokka/ArgTypePlugin; + public synthetic fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; + public fun convert (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/DokkaConfiguration$PluginConfiguration; + public fun getDescription ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/ArgTypeSourceLinkDefinition : kotlinx/cli/ArgType { + public static final field INSTANCE Lorg/jetbrains/dokka/ArgTypeSourceLinkDefinition; + public synthetic fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; + public fun convert (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/DokkaConfiguration$SourceLinkDefinition; + public fun getDescription ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/ArgTypeVisibility : kotlinx/cli/ArgType { + public static final field INSTANCE Lorg/jetbrains/dokka/ArgTypeVisibility; + public synthetic fun convert (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object; + public fun convert (Ljava/lang/String;Ljava/lang/String;)Lorg/jetbrains/dokka/DokkaConfiguration$Visibility; + public fun getDescription ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/GlobalArguments : org/jetbrains/dokka/DokkaConfiguration { + public fun <init> ([Ljava/lang/String;)V + public fun getCacheRoot ()Ljava/io/File; + public fun getDelayTemplateSubstitution ()Z + public fun getFailOnWarning ()Z + public fun getFinalizeCoroutines ()Z + public final fun getGlobalLinks ()Ljava/util/List; + public final fun getGlobalPackageOptions ()Ljava/util/List; + public final fun getGlobalSrcLink ()Ljava/util/List; + public final fun getHelpSourceSet ()Ljava/lang/Object; + public fun getIncludes ()Ljava/util/Set; + public final fun getJson ()Ljava/lang/String; + public final fun getLogger ()Lorg/jetbrains/dokka/utilities/DokkaLogger; + public final fun getLoggingLevel ()Lorg/jetbrains/dokka/utilities/LoggingLevel; + public fun getModuleName ()Ljava/lang/String; + public fun getModuleVersion ()Ljava/lang/String; + public fun getModules ()Ljava/util/List; + public final fun getNoSuppressObviousFunctions ()Z + public fun getOfflineMode ()Z + public fun getOutputDir ()Ljava/io/File; + public final fun getParser ()Lkotlinx/cli/ArgParser; + public fun getPluginsClasspath ()Ljava/util/List; + public fun getPluginsConfiguration ()Ljava/util/List; + public fun getSourceSets ()Ljava/util/List; + public fun getSuppressInheritedMembers ()Z + public fun getSuppressObviousFunctions ()Z +} + +public final class org/jetbrains/dokka/LinkMapperKt { + public static final fun defaultLinks (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Ljava/util/List; + public static final fun parseLinks (Ljava/util/List;)Ljava/util/List; +} + +public final class org/jetbrains/dokka/MainKt { + public static final fun initializeConfiguration (Lorg/jetbrains/dokka/GlobalArguments;)Lorg/jetbrains/dokka/DokkaConfiguration; + public static final fun main ([Ljava/lang/String;)V +} + diff --git a/dokka-runners/runner-cli/build.gradle.kts b/dokka-runners/runner-cli/build.gradle.kts new file mode 100644 index 00000000..c078e22c --- /dev/null +++ b/dokka-runners/runner-cli/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import dokkabuild.overridePublicationArtifactId + +plugins { + id("dokkabuild.kotlin-jvm") + id("dokkabuild.publish-shadow") + alias(libs.plugins.kotlinx.binaryCompatibilityValidator) +} + +overridePublicationArtifactId("dokka-cli") + +dependencies { + implementation("org.jetbrains.dokka:dokka-core") + implementation(libs.kotlinx.cli) + + testImplementation(kotlin("test")) +} + +tasks.shadowJar { + manifest { + attributes("Main-Class" to "org.jetbrains.dokka.MainKt") + } +} diff --git a/dokka-runners/runner-cli/gradle.properties b/dokka-runners/runner-cli/gradle.properties new file mode 100644 index 00000000..08f2ce2a --- /dev/null +++ b/dokka-runners/runner-cli/gradle.properties @@ -0,0 +1,11 @@ +# +# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + +# Project Settings +group=org.jetbrains.dokka +version=1.9.20-SNAPSHOT + +org.jetbrains.dokka.javaToolchain.mainCompiler=8 +org.jetbrains.dokka.javaToolchain.testLauncher=8 +org.jetbrains.dokka.kotlinLanguageLevel=1.4 diff --git a/dokka-runners/runner-cli/settings.gradle.kts b/dokka-runners/runner-cli/settings.gradle.kts new file mode 100644 index 00000000..37b36fda --- /dev/null +++ b/dokka-runners/runner-cli/settings.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UnstableApiUsage") + +rootProject.name = "runner-cli" + +pluginManagement { + includeBuild("../../build-logic") + + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + } + + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/CliArgumentTypes.kt b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/CliArgumentTypes.kt new file mode 100644 index 00000000..1c6b0ba4 --- /dev/null +++ b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/CliArgumentTypes.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.CLIEntity +import java.io.File +import java.nio.file.Paths + + +public object ArgTypeFile : ArgType<File>(true) { + override fun convert(value: kotlin.String, name: kotlin.String): File = Paths.get(value).toRealPath().toFile() + override val description: kotlin.String + get() = "{ String that represents a directory / file path }" +} + +public object ArgTypePlatform : ArgType<Platform>(true) { + override fun convert(value: kotlin.String, name: kotlin.String): Platform = Platform.fromString(value) + override val description: kotlin.String + get() = "{ String that represents a Kotlin platform. Possible values: jvm/js/native/common/android }" +} + +public object ArgTypeVisibility : ArgType<DokkaConfiguration.Visibility>(true) { + override fun convert(value: kotlin.String, name: kotlin.String): DokkaConfiguration.Visibility { + return DokkaConfiguration.Visibility.fromString(value) + } + + override val description: kotlin.String + get() = "{ String that represents a visibility modifier. Possible values: ${getPossibleVisibilityValues()}" + + private fun getPossibleVisibilityValues(): kotlin.String = + DokkaConfiguration.Visibility.values().joinToString(separator = ", ") +} + +public object ArgTypePlugin : ArgType<DokkaConfiguration.PluginConfiguration>(true) { + override fun convert( + value: kotlin.String, + name: kotlin.String + ): DokkaConfiguration.PluginConfiguration { + return value.split("=").let { + PluginConfigurationImpl( + fqPluginName = it[0], + serializationFormat = DokkaConfiguration.SerializationFormat.JSON, + values = it[1] + ) + } + } + + override val description: kotlin.String + get() = "{ String that represents plugin configuration. " + + "Format is {fullyQualifiedPluginName}={jsonConfiguration}. " + + "Quotation marks (`\"`) inside json must be escaped. }" +} + +public object ArgTypeSourceLinkDefinition : ArgType<DokkaConfiguration.SourceLinkDefinition>(true) { + override fun convert(value: kotlin.String, name: kotlin.String): DokkaConfiguration.SourceLinkDefinition { + return if (value.isNotEmpty() && value.contains("=")) + SourceLinkDefinitionImpl.parseSourceLinkDefinition(value) + else { + throw IllegalArgumentException( + "Warning: Invalid -srcLink syntax. " + + "Expected: <path>=<url>[#lineSuffix]. No source links will be generated." + ) + } + } + + override val description: kotlin.String + get() = "{ String that represent source links. Format: {srcPath}={remotePath#lineSuffix} }" +} + +public data class ArgTypeArgument(val moduleName: CLIEntity<kotlin.String>) : + ArgType<DokkaConfiguration.DokkaSourceSet>(true) { + override fun convert(value: kotlin.String, name: kotlin.String): DokkaConfiguration.DokkaSourceSet = + (if (moduleName.valueOrigin != ArgParser.ValueOrigin.UNSET && moduleName.valueOrigin != ArgParser.ValueOrigin.UNDEFINED) { + moduleName.value + } else { + DokkaDefaults.moduleName + }).let { moduleNameOrDefault -> + parseSourceSet(moduleNameOrDefault, value.split(" ").filter { it.isNotBlank() }.toTypedArray()) + } + + override val description: kotlin.String + get() = "" +} + +// Workaround for printing nested parsers help +public data class ArgTypeHelpSourceSet(val moduleName: CLIEntity<kotlin.String>) : ArgType<Any>(false) { + override fun convert(value: kotlin.String, name: kotlin.String): Any = Any().also { + parseSourceSet(moduleName.value, arrayOf("-h")) + } + + override val description: kotlin.String + get() = "" +} diff --git a/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/GlobalArguments.kt b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/GlobalArguments.kt new file mode 100644 index 00000000..5c95f63f --- /dev/null +++ b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/GlobalArguments.kt @@ -0,0 +1,168 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +import kotlinx.cli.* +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import org.jetbrains.dokka.utilities.cast +import java.io.File + +public class GlobalArguments(args: Array<String>) : DokkaConfiguration { + + public val parser: ArgParser = ArgParser("dokka-cli", prefixStyle = ArgParser.OptionPrefixStyle.JVM) + + public val json: String? by parser.argument(ArgType.String, description = "JSON configuration file path").optional() + + private val _moduleName = parser.option( + ArgType.String, + description = "Name of the project/module", + fullName = "moduleName" + ).default(DokkaDefaults.moduleName) + + override val moduleName: String by _moduleName + + override val moduleVersion: String? by parser.option( + ArgType.String, + description = "Documented version", + fullName = "moduleVersion" + ) + + override val outputDir: File by parser.option(ArgTypeFile, description = "Output directory path, ./dokka by default") + .default(DokkaDefaults.outputDir) + + override val cacheRoot: File? = null + + override val sourceSets: List<DokkaConfiguration.DokkaSourceSet> by parser.option( + ArgTypeArgument(_moduleName), + description = "Configuration for a Dokka source set. Contains nested configuration.", + fullName = "sourceSet" + ).multiple() + + override val pluginsConfiguration: List<DokkaConfiguration.PluginConfiguration> by parser.option( + ArgTypePlugin, + description = "Configuration for Dokka plugins. Accepts multiple values separated by `^^`." + ).delimiter("^^") + + override val pluginsClasspath: List<File> by parser.option( + ArgTypeFile, + fullName = "pluginsClasspath", + description = "List of jars with Dokka plugins and their dependencies. Accepts multiple paths separated by semicolons" + ).delimiter(";") + + override val offlineMode: Boolean by parser.option( + ArgType.Boolean, + description = "Whether to resolve remote files/links over network" + ).default(DokkaDefaults.offlineMode) + + override val failOnWarning: Boolean by parser.option( + ArgType.Boolean, + description = "Whether to fail documentation generation if Dokka has emitted a warning or an error" + ).default(DokkaDefaults.failOnWarning) + + override val delayTemplateSubstitution: Boolean by parser.option( + ArgType.Boolean, + description = "Delay substitution of some elements. Used in incremental builds of multimodule projects" + ).default(DokkaDefaults.delayTemplateSubstitution) + + public val noSuppressObviousFunctions: Boolean by parser.option( + ArgType.Boolean, + description = "Whether to suppress obvious functions such as inherited from `kotlin.Any` and `java.lang.Object`" + ).default(!DokkaDefaults.suppressObviousFunctions) + + override val suppressObviousFunctions: Boolean by lazy { !noSuppressObviousFunctions } + + private val _includes by parser.option( + ArgTypeFile, + fullName = "includes", + description = "Markdown files that contain module and package documentation. " + + "Accepts multiple values separated by semicolons" + ).delimiter(";") + + override val includes: Set<File> by lazy { _includes.toSet() } + + override val suppressInheritedMembers: Boolean by parser.option( + ArgType.Boolean, + description = "Whether to suppress inherited members that aren't explicitly overridden in a given class" + ).default(DokkaDefaults.suppressInheritedMembers) + + override val finalizeCoroutines: Boolean = true + + public val globalPackageOptions: List<String> by parser.option( + ArgType.String, + description = "Global list of package configurations in format " + + "\"matchingRegexp,-deprecated,-privateApi,+warnUndocumented,+suppress;...\". " + + "Accepts multiple values separated by semicolons. " + ).delimiter(";") + + public val globalLinks: List<String> by parser.option( + ArgType.String, + description = "Global external documentation links in format {url}^{packageListUrl}. " + + "Accepts multiple values separated by `^^`" + ).delimiter("^^") + + public val globalSrcLink: List<String> by parser.option( + ArgType.String, + description = "Global mapping between a source directory and a Web service for browsing the code. " + + "Accepts multiple paths separated by semicolons" + ).delimiter(";") + + public val helpSourceSet: Any? by parser.option( + ArgTypeHelpSourceSet(_moduleName), + description = "Prints help for nested -sourceSet configuration" + ) + + public val loggingLevel: LoggingLevel by parser.option( + ArgType.Choice(toVariant = { + when (it.toUpperCase().trim()) { + "DEBUG", "" -> LoggingLevel.DEBUG + "PROGRESS" -> LoggingLevel.PROGRESS + "INFO" -> LoggingLevel.INFO + "WARN" -> LoggingLevel.WARN + "ERROR" -> LoggingLevel.ERROR + else -> { + println("""Failed to deserialize logging level, got $it expected one of + |"DEBUG", "PROGRESS", "INFO", "WARN", "ERROR", falling back to PROGRESS""".trimMargin()) + LoggingLevel.PROGRESS + } + } + }, toString = { it.toString() } + )).default(LoggingLevel.PROGRESS) + + override val modules: List<DokkaConfiguration.DokkaModuleDescription> = emptyList() + + public val logger: DokkaLogger by lazy { + DokkaConsoleLogger(loggingLevel) + } + + init { + parser.parse(args) + + sourceSets.forEach { + it.perPackageOptions.cast<MutableList<DokkaConfiguration.PackageOptions>>() + .addAll(parsePerPackageOptions(globalPackageOptions)) + } + + sourceSets.forEach { + it.externalDocumentationLinks.cast<MutableSet<DokkaConfiguration.ExternalDocumentationLink>>().addAll(parseLinks(globalLinks)) + } + + globalSrcLink.forEach { + if (it.isNotEmpty() && it.contains("=")) + sourceSets.all { sourceSet -> + sourceSet.sourceLinks.cast<MutableSet<SourceLinkDefinitionImpl>>() + .add(SourceLinkDefinitionImpl.parseSourceLinkDefinition(it)) + } + else { + logger.warn("Invalid -srcLink syntax. Expected: <path>=<url>[#lineSuffix]. No source links will be generated.") + } + } + + sourceSets.forEach { + it.externalDocumentationLinks.cast<MutableSet<DokkaConfiguration.ExternalDocumentationLink>>().addAll(defaultLinks(it)) + } + } +} diff --git a/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/LinkMapper.kt b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/LinkMapper.kt new file mode 100644 index 00000000..dbfa8db1 --- /dev/null +++ b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/LinkMapper.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +import java.io.File +import java.net.MalformedURLException +import java.net.URL + +@OptIn(ExperimentalStdlibApi::class) // for buildList +public fun defaultLinks(config: DokkaConfiguration.DokkaSourceSet): MutableList<DokkaConfiguration.ExternalDocumentationLink> = + buildList<DokkaConfiguration.ExternalDocumentationLink> { + if (!config.noJdkLink) { + add(DokkaConfiguration.ExternalDocumentationLink.jdk(config.jdkVersion)) + } + + if (!config.noStdlibLink) { + add(DokkaConfiguration.ExternalDocumentationLink.kotlinStdlib()) + } + }.toMutableList() + + +public fun parseLinks(links: List<String>): List<DokkaConfiguration.ExternalDocumentationLink> { + val (parsedLinks, parsedOfflineLinks) = links + .map { it.split("^").map { it.trim() }.filter { it.isNotBlank() } } + .filter { it.isNotEmpty() } + .partition { it.size == 1 } + + return parsedLinks.map { (root) -> ExternalDocumentationLink(root) } + + parsedOfflineLinks.map { (root, packageList) -> + val rootUrl = URL(root) + val packageListUrl = + try { + URL(packageList) + } catch (ex: MalformedURLException) { + File(packageList).toURI().toURL() + } + ExternalDocumentationLink(rootUrl, packageListUrl) + } +} diff --git a/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/PackageOptionsParser.kt b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/PackageOptionsParser.kt new file mode 100644 index 00000000..377e32e6 --- /dev/null +++ b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/PackageOptionsParser.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +internal fun parsePerPackageOptions(args: List<String>): List<DokkaConfiguration.PackageOptions> = args.map { it.split(",") }.map { + val matchingRegex = it.first() + + val options = it.subList(1, it.size) + + val deprecated = options.find { it.endsWith("skipDeprecated") }?.startsWith("+") + ?: DokkaDefaults.skipDeprecated + + val reportUndocumented = options.find { it.endsWith("reportUndocumented") }?.startsWith("+") + ?: DokkaDefaults.reportUndocumented + + val privateApi = options.find { it.endsWith("includeNonPublic") }?.startsWith("+") + ?: DokkaDefaults.includeNonPublic + + val suppress = options.find { it.endsWith("suppress") }?.startsWith("+") + ?: DokkaDefaults.suppress + + val documentedVisibilities = options + .filter { it.matches(Regex("\\+visibility:.+")) } // matches '+visibility:' with at least one symbol after the semicolon + .map { DokkaConfiguration.Visibility.fromString(it.split(":")[1]) } + .toSet() + .ifEmpty { DokkaDefaults.documentedVisibilities } + + PackageOptionsImpl( + matchingRegex, + includeNonPublic = privateApi, + documentedVisibilities = documentedVisibilities, + reportUndocumented = reportUndocumented, + skipDeprecated = !deprecated, + suppress = suppress + ) +} diff --git a/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/SourceSetArgumentsParser.kt b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/SourceSetArgumentsParser.kt new file mode 100644 index 00000000..a1c44a13 --- /dev/null +++ b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/SourceSetArgumentsParser.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.default +import kotlinx.cli.delimiter + +internal fun parseSourceSet(moduleName: String, args: Array<String>): DokkaConfiguration.DokkaSourceSet { + + if (moduleName.contains(',')) { + // To figure out why this is needed and if it is still relevant, see the comment here: + // https://github.com/Kotlin/dokka/issues/3011#issuecomment-1568620493 + throw IllegalArgumentException("Module name cannot contain commas as it is used internally as a delimiter.") + } + + val parser = ArgParser("sourceSet", prefixStyle = ArgParser.OptionPrefixStyle.JVM) + + val sourceSetName by parser.option( + ArgType.String, + description = "Name of the source set" + ).default("main") + + val displayName by parser.option( + ArgType.String, + description = "Display name of the source set, used both internally and externally" + ).default(DokkaDefaults.sourceSetDisplayName) + + val classpath by parser.option( + ArgTypeFile, + description = "Classpath for analysis and interactive samples. Accepts multiple paths separated by semicolons" + ).delimiter(";") + + val sourceRoots by parser.option( + ArgTypeFile, + description = "Source code roots to be analyzed and documented. Accepts multiple paths separated by semicolons", + fullName = "src" + ).delimiter(";") + + val dependentSourceSets by parser.option( + ArgType.String, + description = "Names of dependent source sets in format \"moduleName/sourceSetName\". " + + "Accepts multiple paths separated by semicolons" + ).delimiter(";") + + val samples by parser.option( + ArgTypeFile, + description = "List of directories or files that contain sample functions. " + + "Accepts multiple paths separated by semicolons" + ).delimiter(";") + + val includes by parser.option( + ArgTypeFile, + description = "Markdown files that contain module and package documentation. " + + "Accepts multiple paths separated by semicolons" + ).delimiter(";") + + val includeNonPublic: Boolean by parser.option( + ArgType.Boolean, + description = "Deprecated, use documentedVisibilities") + .default(DokkaDefaults.includeNonPublic) + + val documentedVisibilities by parser.option( + ArgTypeVisibility, + description = "Visibilities to be documented. Accepts multiple values separated by semicolons" + ).delimiter(";") + + val reportUndocumented by parser.option(ArgType.Boolean, description = "Whether to report undocumented declarations") + .default(DokkaDefaults.reportUndocumented) + + val noSkipEmptyPackages by parser.option( + ArgType.Boolean, + description = "Whether to create pages for empty packages" + ).default(!DokkaDefaults.skipEmptyPackages) + + val skipEmptyPackages by lazy { !noSkipEmptyPackages } + + val skipDeprecated by parser.option(ArgType.Boolean, description = "Whether to skip deprecated declarations") + .default(DokkaDefaults.skipDeprecated) + + val jdkVersion by parser.option( + ArgType.Int, + description = "Version of JDK to use for linking to JDK Javadocs" + ).default(DokkaDefaults.jdkVersion) + + val languageVersion by parser.option( + ArgType.String, + description = "Language version used for setting up analysis and samples" + ) + + val apiVersion by parser.option( + ArgType.String, + description = "Kotlin API version used for setting up analysis and samples" + ) + + val noStdlibLink by parser.option(ArgType.Boolean, description = "Whether to generate links to Standard library") + .default(DokkaDefaults.noStdlibLink) + + val noJdkLink by parser.option(ArgType.Boolean, description = "Whether to generate links to JDK Javadocs") + .default(DokkaDefaults.noJdkLink) + + val suppressedFiles by parser.option( + ArgTypeFile, + description = "Paths to files to be suppressed. Accepts multiple paths separated by semicolons." + ).delimiter(";") + + val analysisPlatform: Platform by parser.option( + ArgTypePlatform, + description = "Platform used for setting up analysis" + ).default(DokkaDefaults.analysisPlatform) + + val perPackageOptions by parser.option( + ArgType.String, + description = "List of package source set configuration in format " + + "\"matchingRegexp,-deprecated,-privateApi,+warnUndocumented,+suppress;...\". " + + "Accepts multiple values separated by semicolons. " + ).delimiter(";") + + val externalDocumentationLinks by parser.option( + ArgType.String, + description = "External documentation links in format {url}^{packageListUrl}. " + + "Accepts multiple values separated by `^^`" + ).delimiter("^^") + + val sourceLinks by parser.option( + ArgTypeSourceLinkDefinition, + description = "Mapping between a source directory and a Web service for browsing the code. " + + "Accepts multiple paths separated by semicolons", + fullName = "srcLink" + ).delimiter(";") + + parser.parse(args) + + return object : DokkaConfiguration.DokkaSourceSet { + override val displayName = displayName + override val sourceSetID = DokkaSourceSetID(moduleName, sourceSetName) + override val classpath = classpath.toMutableList() + override val sourceRoots = sourceRoots.toMutableSet() + override val dependentSourceSets = dependentSourceSets + .map { dependentSourceSetName -> dependentSourceSetName.split('/').let { DokkaSourceSetID(it[0], it[1]) } } + .toMutableSet() + override val samples = samples.toMutableSet() + override val includes = includes.toMutableSet() + @Deprecated("Use [documentedVisibilities] property for a more flexible control over documented visibilities") + override val includeNonPublic = includeNonPublic + override val reportUndocumented = reportUndocumented + override val skipEmptyPackages = skipEmptyPackages + override val skipDeprecated = skipDeprecated + override val jdkVersion = jdkVersion + override val sourceLinks = sourceLinks.toMutableSet() + override val analysisPlatform = analysisPlatform + override val perPackageOptions = parsePerPackageOptions(perPackageOptions).toMutableList() + override val externalDocumentationLinks = parseLinks(externalDocumentationLinks).toMutableSet() + override val languageVersion = languageVersion + override val apiVersion = apiVersion + override val noStdlibLink = noStdlibLink + override val noJdkLink = noJdkLink + override val suppressedFiles = suppressedFiles.toMutableSet() + override val documentedVisibilities: Set<DokkaConfiguration.Visibility> = documentedVisibilities.toSet() + .ifEmpty { DokkaDefaults.documentedVisibilities } + } +} diff --git a/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/main.kt b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/main.kt new file mode 100644 index 00000000..e1949a93 --- /dev/null +++ b/dokka-runners/runner-cli/src/main/kotlin/org/jetbrains/dokka/main.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink +import org.jetbrains.dokka.utilities.* +import java.nio.file.Paths + +public fun main(args: Array<String>) { + val globalArguments = GlobalArguments(args) + val configuration = initializeConfiguration(globalArguments) + DokkaGenerator(configuration, globalArguments.logger).generate() +} + +public fun initializeConfiguration(globalArguments: GlobalArguments): DokkaConfiguration { + return if (globalArguments.json != null) { + val jsonContent = Paths.get(checkNotNull(globalArguments.json)).toFile().readText() + val globals = GlobalDokkaConfiguration(jsonContent) + val dokkaConfigurationImpl = DokkaConfigurationImpl(jsonContent) + + dokkaConfigurationImpl.apply(globals).apply { + sourceSets.forEach { + it.externalDocumentationLinks.cast<MutableSet<ExternalDocumentationLink>>().addAll(defaultLinks(it)) + } + } + } else { + globalArguments + } +} + diff --git a/dokka-runners/runner-cli/src/test/kotlin/org/jetbrains/dokka/CliTest.kt b/dokka-runners/runner-cli/src/test/kotlin/org/jetbrains/dokka/CliTest.kt new file mode 100644 index 00000000..48441e4c --- /dev/null +++ b/dokka-runners/runner-cli/src/test/kotlin/org/jetbrains/dokka/CliTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +import java.nio.file.Paths +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class CliIntegrationTest { + + @Test + fun `should apply global settings to all source sets`() { + val jsonPath = Paths.get(javaClass.getResource("/my-file.json")?.toURI() ?: throw IllegalStateException("No JSON found!")).toFile().toString() + val globalArguments = GlobalArguments(arrayOf(jsonPath)) + + val configuration = initializeConfiguration(globalArguments) + + configuration.sourceSets.forEach { + assertTrue(it.perPackageOptions.isNotEmpty()) + assertTrue(it.sourceLinks.isNotEmpty()) + assertTrue(it.externalDocumentationLinks.isNotEmpty()) + + assertTrue(it.externalDocumentationLinks.any { it.url.toString() == "https://docs.oracle.com/javase/8/docs/api/" }) + assertEquals(it.sourceLinks.single().localDirectory, "/home/Vadim.Mishenev/dokka/examples/cli/src/main/kotlin") + assertEquals(it.perPackageOptions.single().matchingRegex, "my-custom-regex") + } + + } + + @Test + fun `should not fail when no sourceset options are specified`() { + val jsonPath = Paths.get(javaClass.getResource("/my-file-no-sourceset-options.json")?.toURI() ?: throw IllegalStateException("No JSON found!")).toFile().toString() + val globalArguments = GlobalArguments(arrayOf(jsonPath)) + + val configuration = initializeConfiguration(globalArguments) + + configuration.sourceSets.forEach { + assertTrue(it.perPackageOptions.isEmpty()) + assertTrue(it.sourceLinks.isEmpty()) + assertTrue(it.externalDocumentationLinks.size == 2) // there are default values, java and kotlin stdlibs + } + } +} diff --git a/dokka-runners/runner-cli/src/test/resources/my-file-no-sourceset-options.json b/dokka-runners/runner-cli/src/test/resources/my-file-no-sourceset-options.json new file mode 100644 index 00000000..3a8643d1 --- /dev/null +++ b/dokka-runners/runner-cli/src/test/resources/my-file-no-sourceset-options.json @@ -0,0 +1,13 @@ +{ + "outputDir": "build/docs", + "sourceSets": [ + { + "moduleDisplayName": "Sample", + "sourceSetID": { + "scopeId": "sample", + "sourceSetName": "main" + }, + "sourceRoots": ["sample"] + } + ] +} diff --git a/dokka-runners/runner-cli/src/test/resources/my-file.json b/dokka-runners/runner-cli/src/test/resources/my-file.json new file mode 100644 index 00000000..49dda814 --- /dev/null +++ b/dokka-runners/runner-cli/src/test/resources/my-file.json @@ -0,0 +1,51 @@ +{ + "moduleName": "Dokka Example", + "moduleVersion": null, + "outputDir": "$outputPath", + "pluginsClasspath": ["$pluginsClasspath"], + "cacheRoot": null, + "offlineMode": false, + "sourceLinks": [{ + "localDirectory": "/home/Vadim.Mishenev/dokka/examples/cli/src/main/kotlin", + "remoteUrl": "https://github.com/Kotlin/dokka/tree/master/examples/gradle/dokka-gradle-example/src/main/kotlin", + "remoteLineSuffix": "#L" + }], + "externalDocumentationLinks": [{ + "url": "https://docs.oracle.com/javase/8/docs/api/", + "packageListUrl": "https://docs.oracle.com/javase/8/docs/api/package-list" + }], + "perPackageOptions": [{ + "matchingRegex": "my-custom-regex", + "skipDeprecated": "true", + "reportUndocumented": "true", + "includeNonPublic": "true", + "documentedVisibilities": ["PUBLIC", "PRIVATE", "PROTECTED", "INTERNAL", "PACKAGE"] + }], + "sourceSets": [ + { + "displayName": "jvm", + "sourceSetID": { + "scopeId": ":dokkaHtml", + "sourceSetName": "main" + }, + "sourceRoots": [ + "$projectPath" + ], + "dependentSourceSets": [], + "samples": [], + "includes": [], + "includeNonPublic": false, + "reportUndocumented": false, + "skipEmptyPackages": true, + "skipDeprecated": false, + "jdkVersion": 8, + "sourceLinks": [], + "perPackageOptions": [], + "externalDocumentationLinks": [], + "noStdlibLink": false, + "noJdkLink": false, + "suppressedFiles": [], + "analysisPlatform": "jvm" + } + ] +} diff --git a/dokka-runners/runner-gradle-plugin-classic/MIGRATION.md b/dokka-runners/runner-gradle-plugin-classic/MIGRATION.md new file mode 100644 index 00000000..7f881bfa --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/MIGRATION.md @@ -0,0 +1,167 @@ +### Configuring a dokka task +Dokka 1.4.x will create dedicated tasks for each format. +You can expect the following formats being registered and configured by default: +- `dokkaHtml` +- `dokkaJavadoc` +- `dokkaGfm` +- `dokkaJekyll` + +Therefore, you need to either select specifically which task you want to configure or configure all with Type `DokkaTask` + +```kotlin +/* 0.10.x */ +tasks.dokka.configure { /*...*/ } + +/* 1.4.x */ +// configure all formats +tasks.withType<DokkaTask>().configureEach { /*...*/ } + +// configure only html format e.g. +tasks.dokkaHtml.configure { /*...*/ } +``` + +#### properties +```kotlin +/* 0.10.x */ outputFormat = "html" +/* 1.4.x No equivalent. + Formats are only configured by plugins. + See `dokkaHtml`, `dokkaJavadoc`,... tasks */ + +/* 0.10.x */ outputDirectory = "$buildDir/javadoc" +/* 1.4.x */ outputDirectory.set(buildDir.resolve("javadoc")) + + +/* 0.10.x */ subProjects = ["subproject1", "subproject2"] +/* 1.4.x No equivalent. + See `DokkaCollectorTask` and `DokkaMultiModuleTask` */ + + +/* 0.10.x */ disableAutoconfiguration = false +/* 1.4.x No equivalent. + Source sets are synced with Kotlin Model by default. + All settings can still be overridden */ + +/* 0.10.x */ cacheRoot = "default" +/* 1.4.x */ cacheRoot.set(file("default")) + +``` + +### Configure a source set +```kotlin +/* 0.10.x */ +tasks.dokka.configure { + configuration { + // ... + } +} + +/* 1.4.x */ +tasks.dokkaHtml.configure { + dokkaSourceSets { + named("main") { /* configure main source set */ } + configureEach { /* configure all source sets */ } + register("custom") { /* register custom source set */ } + } +} +``` + +#### Properties +```kotlin +/* 0.10.x */ moduleName = "myModule" +/* 1.4.0 */ /* Use AbstractDokkaTask#moduleDisplayName instead */ +/* 1.4.10+ */ /* Use AbstractDokkaTask#moduleName instead */ + +/* 0.10.x */ includeNonPublic = false +/* 1.4.x */ includeNonPublic.set(false) + +/* 0.10.x */ skipDeprecated = false +/* 1.4.x */ skipDeprecated.set(false) + +/* 0.10.x */ reportUndocumented = true +/* 1.4.x */ reportUndocumented.set(true) + +/* 0.10.x */ skipEmptyPackages = true +/* 1.4.x */ skipEmptyPackages.set(true) + +/* 0.10.x */ targets = ["JVM"] +/* 1.4.x */ /* No equivalent */ + /* Use platform and displayName instead */ + +/* 0.10.x */ platform = "JVM" +/* 1.4.x */ platform.set(org.jetbrains.dokka.Platform.jvm) +/* 1.4.x */ platform.set(Platform.jvm) // with import + +/* 0.10.x */ classpath = [new File("$buildDir/other.jar")] +/* 1.4.x */ classpath.setFrom(buildDir.resolve("other.jar")) // setting classpath +/* 1.4.x */ classpath.from(buildDir.resolve("other.jar")) // adding to existing classpath + +/* 0.10.x */ sourceRoots = [files("src/main/kotlin")] +/* 1.4.x */ sourceRoots.setFrom(file("src/main/kotlin")) // setting all source roots +/* 1.4.x */ sourceRoots.from(file("src/main/kotlin")) // adding to existing source roots + +/* 0.10.x */ includes = ["packages.md", "extra.md"] +/* 1.4.x */ includes.setFrom(files("packages.md", "extra.md")) // setting all includes +/* 1.4.x */ includes.from(files("packages.md", "extra.md")) // adding to existing includes + +/* 0.10.x */ samples = ["samples/basic.kt", "samples/advanced.kt"] +/* 1.4.x */ samples.setFrom(files("samples/basic.kt", "samples/advanced.kt")) +/* 1.4.x */ samples.from(files("samples/basic.kt", "samples/advanced.kt")) + +/* 0.10.x */ kotlinTasks { /* ... */ } +/* 1.4.x */ /* No *direct* equivalent */ + /* Source sets synced with Kotlin Gradle Plugin will be configured properly */ + /* Custom source sets can use extension `kotlinSourceSet(...)` */ + +/* 0.10.x */ jdkVersion = 6 +/* 1.4.x */ jdkVersion.set(6) + +/* 0.10.x */ noStdlibLink = false +/* 1.4.x */ noStdlibLink.set(false) + +/* 0.10.x */ noJdkLink = false +/* 1.4.x */ noJdkLink.set(false) + +sourceLink { + /* 0.10.x */ path = "src/main/kotlin" + /* 1.4.x */ localDirectory.set(file("src/main/kotlin")) + + /* 0.10.x */ url = "https://github.com/myproject/blob/master/src/main/kotlin" + /* 1.4.x */ remoteUrl.set(java.net.URL("https://github.com/myproject/blob/master/src/main/kotlin")) + /* 1.4.x */ remoteUrl.set(uri("https://github.com/myproject/blob/master/src/main/kotlin").toURL()) + + /* 0.10.x */ lineSuffix = "#L" + /* 1.4.x */ remoteLineSuffix.set("#L") +} + + +externalDocumentationLink { + /* 0.10.x */ url = URL("https://example.com/docs/") + /* 1.4.x */ url.set(URL("https://example.com/docs/")) + + /* 0.10.x */ packageListUrl = URL("file:///home/user/localdocs/package-list") + /* 1.4.x */ packageListUrl.set(URL("file:///home/user/localdocs/package-list")) +} + +// Allows to customize documentation generation options on a per-package basis +// Repeat for multiple packageOptions +perPackageOption { + /* 0.10.x */ prefix = "kotlin" + /* 1.4.x */ prefix.set("kotlin") + /* 1.4.20+ */ matchingRegex("kotlin($|\\.).*") + + /* 0.10.x */ skipDeprecated = false + /* 1.4.x */ skipDeprecated.set(false) + + /* 0.10.x */ reportUndocumented = true + /* 1.4.x */ reportUndocumented.set(true) + + /* 0.10.x */ includeNonPublic = false + /* 1.4.x */ includeNonPublic.set(false) + + /* 0.10.x */ suppress = true + /* 1.4.x */ suppress.set(true) + +} +``` + +For more information or help, feel free to ask questions in the [official Kotlin Slack Channel](https://kotlinlang.slack.com/) diff --git a/dokka-runners/runner-gradle-plugin-classic/api/runner-gradle-plugin-classic.api b/dokka-runners/runner-gradle-plugin-classic/api/runner-gradle-plugin-classic.api new file mode 100644 index 00000000..7e10ebf1 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/api/runner-gradle-plugin-classic.api @@ -0,0 +1,186 @@ +public abstract class org/jetbrains/dokka/gradle/AbstractDokkaLeafTask : org/jetbrains/dokka/gradle/AbstractDokkaTask { + public fun <init> ()V + public final fun getDokkaSourceSets ()Lorg/gradle/api/NamedDomainObjectContainer; + protected final fun getUnsuppressedSourceSets ()Ljava/util/List; +} + +public abstract class org/jetbrains/dokka/gradle/AbstractDokkaParentTask : org/jetbrains/dokka/gradle/AbstractDokkaTask { + public fun <init> ()V + public final fun addChildTask (Ljava/lang/String;)V + public final fun addChildTask (Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;)V + public final fun addChildTasks (Ljava/lang/Iterable;Ljava/lang/String;)V + public final fun addSubprojectChildTasks (Ljava/lang/String;)V + public final fun removeChildTask (Ljava/lang/String;)V + public final fun removeChildTask (Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;)V + public final fun removeChildTasks (Ljava/lang/Iterable;)V + public final fun removeChildTasks (Ljava/lang/Iterable;Ljava/lang/String;)V + public final fun removeChildTasks (Lorg/gradle/api/Project;)V + public final fun removeSubprojectChildTasks (Ljava/lang/String;)V +} + +public abstract class org/jetbrains/dokka/gradle/AbstractDokkaTask : org/gradle/api/DefaultTask { + public fun <init> ()V + public final fun doFirst (Lgroovy/lang/Closure;)Lorg/gradle/api/Task; + public final fun doFirst (Lorg/gradle/api/Action;)Lorg/gradle/api/Task; + public abstract fun getCacheRoot ()Lorg/gradle/api/file/DirectoryProperty; + public final fun getFailOnWarning ()Lorg/gradle/api/provider/Property; + public final fun getModuleName ()Lorg/gradle/api/provider/Property; + public final fun getModuleVersion ()Lorg/gradle/api/provider/Property; + public final fun getOfflineMode ()Lorg/gradle/api/provider/Property; + public abstract fun getOutputDirectory ()Lorg/gradle/api/file/DirectoryProperty; + public final fun getPlugins ()Lorg/gradle/api/artifacts/Configuration; + public final fun getPluginsConfiguration ()Lorg/gradle/api/provider/ListProperty; + public final fun getPluginsMapConfiguration ()Lorg/gradle/api/provider/MapProperty; + public final fun getRuntime ()Lorg/gradle/api/artifacts/Configuration; + public final fun getSuppressInheritedMembers ()Lorg/gradle/api/provider/Property; + public final fun getSuppressObviousFunctions ()Lorg/gradle/api/provider/Property; +} + +public final class org/jetbrains/dokka/gradle/DokkaBootstrapFactoryKt { + public static final fun DokkaBootstrap (Lorg/gradle/api/artifacts/Configuration;Lkotlin/reflect/KClass;)Lorg/jetbrains/dokka/DokkaBootstrap; +} + +public abstract class org/jetbrains/dokka/gradle/DokkaCollectorTask : org/jetbrains/dokka/gradle/AbstractDokkaParentTask { + public fun <init> ()V +} + +public abstract interface class org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout { + public abstract fun targetChildOutputDirectory (Lorg/jetbrains/dokka/gradle/DokkaMultiModuleTask;Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;)Lorg/gradle/api/provider/Provider; +} + +public final class org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout$CompactInParent : org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout { + public static final field INSTANCE Lorg/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout$CompactInParent; + public fun targetChildOutputDirectory (Lorg/jetbrains/dokka/gradle/DokkaMultiModuleTask;Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;)Lorg/gradle/api/provider/Provider; +} + +public final class org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout$NoCopy : org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout { + public static final field INSTANCE Lorg/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout$NoCopy; + public fun targetChildOutputDirectory (Lorg/jetbrains/dokka/gradle/DokkaMultiModuleTask;Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;)Lorg/gradle/api/provider/Provider; +} + +public abstract class org/jetbrains/dokka/gradle/DokkaMultiModuleTask : org/jetbrains/dokka/gradle/AbstractDokkaParentTask { + public fun <init> ()V + public final fun getFileLayout ()Lorg/gradle/api/provider/Property; + public abstract fun getIncludes ()Lorg/gradle/api/file/ConfigurableFileCollection; + public fun getTaskDependencies ()Lorg/gradle/api/internal/tasks/TaskDependencyInternal; + public synthetic fun getTaskDependencies ()Lorg/gradle/api/tasks/TaskDependency; +} + +public class org/jetbrains/dokka/gradle/DokkaPlugin : org/gradle/api/Plugin { + public fun <init> ()V + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V +} + +public final class org/jetbrains/dokka/gradle/DokkaPlugin$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { + public fun <init> (Lkotlin/jvm/functions/Function1;)V + public final synthetic fun execute (Ljava/lang/Object;)V +} + +public abstract class org/jetbrains/dokka/gradle/DokkaTask : org/jetbrains/dokka/gradle/AbstractDokkaLeafTask { + public fun <init> ()V +} + +public abstract class org/jetbrains/dokka/gradle/DokkaTaskPartial : org/jetbrains/dokka/gradle/AbstractDokkaLeafTask { + public fun <init> ()V +} + +public class org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder : org/jetbrains/dokka/DokkaConfigurationBuilder { + public fun <init> (Ljava/lang/String;Lorg/gradle/api/Project;Lorg/gradle/api/NamedDomainObjectFactory;)V + public final fun DokkaSourceSetID (Ljava/lang/String;)Lorg/jetbrains/dokka/DokkaSourceSetID; + public synthetic fun build ()Ljava/lang/Object; + public fun build ()Lorg/jetbrains/dokka/DokkaSourceSetImpl; + public final fun dependsOn (Ljava/lang/String;)V + public final fun dependsOn (Lorg/gradle/api/tasks/SourceSet;)V + public final fun dependsOn (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)V + public final fun dependsOn (Lorg/jetbrains/dokka/DokkaSourceSetID;)V + public final fun dependsOn (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;)V + public final fun externalDocumentationLink (Lgroovy/lang/Closure;)V + public final fun externalDocumentationLink (Ljava/lang/String;Ljava/lang/String;)V + public final fun externalDocumentationLink (Ljava/net/URL;Ljava/net/URL;)V + public final fun externalDocumentationLink (Lorg/gradle/api/Action;)V + public static synthetic fun externalDocumentationLink$default (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V + public static synthetic fun externalDocumentationLink$default (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;Ljava/net/URL;Ljava/net/URL;ILjava/lang/Object;)V + public final fun getApiVersion ()Lorg/gradle/api/provider/Property; + public final fun getClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getDependentSourceSets ()Lorg/gradle/api/provider/SetProperty; + public final fun getDisplayName ()Lorg/gradle/api/provider/Property; + public final fun getDocumentedVisibilities ()Lorg/gradle/api/provider/SetProperty; + public final fun getExternalDocumentationLinks ()Lorg/gradle/api/provider/SetProperty; + public final fun getIncludeNonPublic ()Lorg/gradle/api/provider/Property; + public final fun getIncludes ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getJdkVersion ()Lorg/gradle/api/provider/Property; + public final fun getLanguageVersion ()Lorg/gradle/api/provider/Property; + public final fun getName ()Ljava/lang/String; + public final fun getNoAndroidSdkLink ()Lorg/gradle/api/provider/Property; + public final fun getNoJdkLink ()Lorg/gradle/api/provider/Property; + public final fun getNoStdlibLink ()Lorg/gradle/api/provider/Property; + public final fun getPerPackageOptions ()Lorg/gradle/api/provider/ListProperty; + public final fun getPlatform ()Lorg/gradle/api/provider/Property; + public final fun getReportUndocumented ()Lorg/gradle/api/provider/Property; + public final fun getSamples ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getSkipDeprecated ()Lorg/gradle/api/provider/Property; + public final fun getSkipEmptyPackages ()Lorg/gradle/api/provider/Property; + public final fun getSourceLinks ()Lorg/gradle/api/provider/SetProperty; + public final fun getSourceRoots ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getSourceSetID ()Lorg/jetbrains/dokka/DokkaSourceSetID; + public final fun getSuppress ()Lorg/gradle/api/provider/Property; + public final fun getSuppressGeneratedFiles ()Lorg/gradle/api/provider/Property; + public final fun getSuppressedFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun perPackageOption (Lgroovy/lang/Closure;)V + public final fun perPackageOption (Lorg/gradle/api/Action;)V + public final fun sourceLink (Lgroovy/lang/Closure;)V + public final fun sourceLink (Lorg/gradle/api/Action;)V + public final fun sourceRoot (Ljava/io/File;)V + public final fun sourceRoot (Ljava/lang/String;)V +} + +public final class org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderExtensionsKt { + public static final fun dependsOn (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;Lcom/android/build/api/dsl/AndroidSourceSet;)V + public static final fun dependsOn (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;Lcom/android/build/gradle/api/AndroidSourceSet;)V + public static final fun dependsOn (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;Lorg/jetbrains/kotlin/gradle/plugin/KotlinSourceSet;)V + public static final fun kotlinSourceSet (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;Lorg/jetbrains/kotlin/gradle/plugin/KotlinSourceSet;)V +} + +public final class org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderFactoryKt { + public static final fun gradleDokkaSourceSetBuilderFactory (Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;)Lorg/gradle/api/NamedDomainObjectFactory; +} + +public final class org/jetbrains/dokka/gradle/GradleExternalDocumentationLinkBuilder : org/jetbrains/dokka/DokkaConfigurationBuilder { + public fun <init> (Lorg/gradle/api/Project;)V + public synthetic fun build ()Ljava/lang/Object; + public fun build ()Lorg/jetbrains/dokka/ExternalDocumentationLinkImpl; + public final fun getPackageListUrl ()Lorg/gradle/api/provider/Property; + public final fun getUrl ()Lorg/gradle/api/provider/Property; +} + +public final class org/jetbrains/dokka/gradle/GradlePackageOptionsBuilder : org/jetbrains/dokka/DokkaConfigurationBuilder { + public fun <init> (Lorg/gradle/api/Project;)V + public synthetic fun build ()Ljava/lang/Object; + public fun build ()Lorg/jetbrains/dokka/PackageOptionsImpl; + public final fun getDocumentedVisibilities ()Lorg/gradle/api/provider/SetProperty; + public final fun getIncludeNonPublic ()Lorg/gradle/api/provider/Property; + public final fun getMatchingRegex ()Lorg/gradle/api/provider/Property; + public final fun getReportUndocumented ()Lorg/gradle/api/provider/Property; + public final fun getSkipDeprecated ()Lorg/gradle/api/provider/Property; + public final fun getSuppress ()Lorg/gradle/api/provider/Property; +} + +public final class org/jetbrains/dokka/gradle/GradleSourceLinkBuilder : org/jetbrains/dokka/DokkaConfigurationBuilder { + public fun <init> (Lorg/gradle/api/Project;)V + public synthetic fun build ()Ljava/lang/Object; + public fun build ()Lorg/jetbrains/dokka/SourceLinkDefinitionImpl; + public final fun getLocalDirectory ()Lorg/gradle/api/provider/Property; + public final fun getRemoteLineSuffix ()Lorg/gradle/api/provider/Property; + public final fun getRemoteUrl ()Lorg/gradle/api/provider/Property; +} + +public final class org/jetbrains/dokka/gradle/SourceSetKotlinGistConfigurationKt { + public static final fun configureWithKotlinSourceSet (Lorg/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder;Lorg/jetbrains/kotlin/gradle/plugin/KotlinSourceSet;)V +} + +public final class org/jetbrains/dokka/gradle/internal/AbstractDokkaTaskExtensionsKt { + public static final fun buildJsonConfiguration (Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;Z)Ljava/lang/String; + public static synthetic fun buildJsonConfiguration$default (Lorg/jetbrains/dokka/gradle/AbstractDokkaTask;ZILjava/lang/Object;)Ljava/lang/String; +} + diff --git a/dokka-runners/runner-gradle-plugin-classic/build.gradle.kts b/dokka-runners/runner-gradle-plugin-classic/build.gradle.kts new file mode 100644 index 00000000..4b142cc8 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/build.gradle.kts @@ -0,0 +1,69 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import dokkabuild.PublicationName +import dokkabuild.overridePublicationArtifactId + +plugins { + id("dokkabuild.gradle-plugin") + alias(libs.plugins.kotlinx.binaryCompatibilityValidator) +} + +overridePublicationArtifactId("dokka-gradle-plugin", PublicationName.GRADLE_PLUGIN) + +dependencies { + // the version is required for Gradle plugin publishing + api("org.jetbrains.dokka:dokka-core:$version") + + compileOnly(libs.gradlePlugin.kotlin) + compileOnly(libs.gradlePlugin.kotlin.klibCommonizerApi) + compileOnly(libs.gradlePlugin.android) + + testImplementation(kotlin("test")) + testImplementation(libs.gradlePlugin.kotlin) + testImplementation(libs.gradlePlugin.kotlin.klibCommonizerApi) + testImplementation(libs.gradlePlugin.android) +} + +@Suppress("UnstableApiUsage") +gradlePlugin { + plugins { + create("dokka") { + id = "org.jetbrains.dokka" + implementationClass = "org.jetbrains.dokka.gradle.DokkaPlugin" + + displayName = "Dokka plugin" + description = "Dokka is an API documentation engine for Kotlin" + tags.addAll("dokka", "kotlin", "kdoc", "android", "documentation", "api") + } + } +} + +// Gradle will put its own version of the stdlib in the classpath, so not pull our own we will end up with +// warnings like 'Runtime JAR files in the classpath should have the same version' +listOf( + configurations.api, + configurations.implementation, + configurations.runtimeOnly +).forEach { + it.configure { excludeGradleCommonDependencies() } +} + +/** + * These dependencies will be provided by Gradle, and we should prevent version conflict + * Code taken from the Kotlin Gradle plugin: + * https://github.com/JetBrains/kotlin/blob/70e15b281cb43379068facb82b8e4bcb897a3c4f/buildSrc/src/main/kotlin/GradleCommon.kt#L72 + */ +fun Configuration.excludeGradleCommonDependencies() { + dependencies + .withType<ModuleDependency>() + .configureEach { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk8") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-common") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-reflect") + exclude(group = "org.jetbrains.kotlin", module = "kotlin-script-runtime") + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/gradle.properties b/dokka-runners/runner-gradle-plugin-classic/gradle.properties new file mode 100644 index 00000000..cf8d7228 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/gradle.properties @@ -0,0 +1,13 @@ +# +# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# + +# Project Settings +group=org.jetbrains.dokka +version=1.9.20-SNAPSHOT + +kotlin.stdlib.default.dependency=false + +org.jetbrains.dokka.javaToolchain.mainCompiler=8 +org.jetbrains.dokka.javaToolchain.testLauncher=8 +org.jetbrains.dokka.kotlinLanguageLevel=1.4 diff --git a/dokka-runners/runner-gradle-plugin-classic/settings.gradle.kts b/dokka-runners/runner-gradle-plugin-classic/settings.gradle.kts new file mode 100644 index 00000000..24f21fe1 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/settings.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UnstableApiUsage") + +rootProject.name = "runner-gradle-plugin-classic" + +pluginManagement { + includeBuild("../../build-logic") + + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + } + + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaArtifacts.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaArtifacts.kt new file mode 100644 index 00000000..241c0449 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaArtifacts.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.jetbrains.dokka.DokkaVersion + +internal val Project.dokkaArtifacts get() = DokkaArtifacts(this) + +internal class DokkaArtifacts(private val project: Project) { + private fun fromModuleName(name: String): Dependency = + project.dependencies.create("org.jetbrains.dokka:$name:${DokkaVersion.version}") + + // TODO [beresnev] analysis switcher + val analysisKotlinDescriptors get() = fromModuleName("analysis-kotlin-descriptors") + val analysisKotlinSymbols get() = fromModuleName("analysis-kotlin-symbols") + + val allModulesPage get() = fromModuleName("all-modules-page-plugin") + val dokkaCore get() = fromModuleName("dokka-core") + val dokkaBase get() = fromModuleName("dokka-base") + val javadocPlugin get() = fromModuleName("javadoc-plugin") + val gfmPlugin get() = fromModuleName("gfm-plugin") + val gfmTemplateProcessing get() = fromModuleName("gfm-template-processing-plugin") + val jekyllTemplateProcessing get() = fromModuleName("jekyll-template-processing-plugin") + val jekyllPlugin get() = fromModuleName("jekyll-plugin") +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout.kt new file mode 100644 index 00000000..b6120129 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayout.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider +import org.jetbrains.dokka.DokkaException +import org.jetbrains.dokka.gradle.DokkaMultiModuleFileLayout.CompactInParent +import org.jetbrains.dokka.gradle.DokkaMultiModuleFileLayout.NoCopy +import java.io.File + +/** + * @see DokkaMultiModuleFileLayout.targetChildOutputDirectory + * @see NoCopy + * @see CompactInParent + */ +fun interface DokkaMultiModuleFileLayout { + + /** + * @param parent: The [DokkaMultiModuleTask] that is initiating a composite documentation run + * @param child: Some child task registered in [parent] + * @return The target output directory of the [child] dokka task referenced by [parent]. This should + * be unique for all registered child tasks. + */ + fun targetChildOutputDirectory(parent: DokkaMultiModuleTask, child: AbstractDokkaTask): Provider<Directory> + + /** + * Will link to the original [AbstractDokkaTask.outputDirectory]. This requires no copying of the output files. + */ + object NoCopy : DokkaMultiModuleFileLayout { + override fun targetChildOutputDirectory( + parent: DokkaMultiModuleTask, + child: AbstractDokkaTask + ): Provider<Directory> = child.outputDirectory + } + + /** + * Will point to a subfolder inside the output directory of the parent. + * The subfolder will follow the structure of the gradle project structure + * e.g. + * :parentProject:firstAncestor:secondAncestor will be be resolved to + * {parent output directory}/firstAncestor/secondAncestor + */ + object CompactInParent : DokkaMultiModuleFileLayout { + override fun targetChildOutputDirectory( + parent: DokkaMultiModuleTask, + child: AbstractDokkaTask + ): Provider<Directory> { + val relativeProjectPath = parent.project.relativeProjectPath(child.project.path) + val relativeFilePath = relativeProjectPath.replace(":", File.separator) + check(!File(relativeFilePath).isAbsolute) { "Unexpected absolute path $relativeFilePath" } + return parent.outputDirectory.dir(relativeFilePath) + } + } +} + +internal fun DokkaMultiModuleTask.targetChildOutputDirectory( + child: AbstractDokkaTask +): Provider<Directory> = fileLayout.get().targetChildOutputDirectory(this, child) + + +internal fun DokkaMultiModuleTask.copyChildOutputDirectories() { + childDokkaTasks.forEach { child -> + this.copyChildOutputDirectory(child) + } +} + +internal fun DokkaMultiModuleTask.copyChildOutputDirectory(child: AbstractDokkaTask) { + val targetChildOutputDirectory = project.file(fileLayout.get().targetChildOutputDirectory(this, child)) + val sourceChildOutputDirectory = child.outputDirectory.asFile.get() + + /* Pointing to the same directory -> No copy necessary */ + if (sourceChildOutputDirectory.absoluteFile == targetChildOutputDirectory.absoluteFile) { + return + } + + /* Cannot target *inside* the original folder */ + if (targetChildOutputDirectory.absoluteFile.startsWith(sourceChildOutputDirectory.absoluteFile)) { + throw DokkaException( + "Cannot re-locate output directory into itself.\n" + + "sourceChildOutputDirectory=${sourceChildOutputDirectory.path}\n" + + "targetChildOutputDirectory=${targetChildOutputDirectory.path}" + ) + } + + /* Source output directory is empty -> No copy necessary */ + if (!sourceChildOutputDirectory.exists()) { + return + } + + sourceChildOutputDirectory.copyRecursively(targetChildOutputDirectory, overwrite = true) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaPlugin.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaPlugin.kt new file mode 100644 index 00000000..77fba8f2 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaPlugin.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import org.gradle.util.GradleVersion +import org.jetbrains.dokka.DokkaDefaults + +open class DokkaPlugin : Plugin<Project> { + override fun apply(project: Project) { + if (GradleVersion.version(project.gradle.gradleVersion) < GradleVersion.version("5.6")) { + project.logger.warn("Dokka: Build is using unsupported gradle version, expected at least 5.6 but got ${project.gradle.gradleVersion}. This may result in strange errors") + } + if (project.shouldUseK2()) + project.logger.warn( + "Dokka's K2 Analysis is being used. " + + "It is still under active development and is thus experimental. " + + "It can be the cause of failed builds or incorrectly generated documentation. " + + "If you encounter an issue, please consider reporting it: https://github.com/Kotlin/dokka/issues" + ) + + project.setupDokkaTasks("dokkaHtml") { + description = "Generates documentation in 'html' format" + } + + project.setupDokkaTasks( + name = "dokkaJavadoc", + multiModuleTaskSupported = false + ) { + plugins.dependencies.add(project.dokkaArtifacts.javadocPlugin) + description = "Generates documentation in 'javadoc' format" + } + + project.setupDokkaTasks( + "dokkaGfm", + allModulesPageAndTemplateProcessing = project.dokkaArtifacts.gfmTemplateProcessing + ) { + plugins.dependencies.add(project.dokkaArtifacts.gfmPlugin) + description = "Generates documentation in GitHub flavored markdown format" + } + + project.setupDokkaTasks( + "dokkaJekyll", + allModulesPageAndTemplateProcessing = project.dokkaArtifacts.jekyllTemplateProcessing + ) { + plugins.dependencies.add(project.dokkaArtifacts.jekyllPlugin) + description = "Generates documentation in Jekyll flavored markdown format" + } + + project.configureEachAbstractDokkaTask() + project.configureEachDokkaMultiModuleTask() + } + + /** + * Creates [DokkaTask], [DokkaMultiModuleTask] for the given + * name and configuration. + */ + private fun Project.setupDokkaTasks( + name: String, + multiModuleTaskSupported: Boolean = true, + allModulesPageAndTemplateProcessing: Dependency = project.dokkaArtifacts.allModulesPage, + configuration: AbstractDokkaTask.() -> Unit = {} + ) { + project.maybeCreateDokkaPluginConfiguration(name) + project.maybeCreateDokkaRuntimeConfiguration(name) + project.tasks.register<DokkaTask>(name) { + configuration() + } + + if (project.parent != null) { + val partialName = "${name}Partial" + project.maybeCreateDokkaPluginConfiguration(partialName) + project.maybeCreateDokkaRuntimeConfiguration(partialName) + project.tasks.register<DokkaTaskPartial>(partialName) { + configuration() + } + } + + if (project.subprojects.isNotEmpty()) { + if (multiModuleTaskSupported) { + val multiModuleName = "${name}MultiModule" + project.maybeCreateDokkaPluginConfiguration(multiModuleName, setOf(allModulesPageAndTemplateProcessing)) + project.maybeCreateDokkaRuntimeConfiguration(multiModuleName) + + project.tasks.register<DokkaMultiModuleTask>(multiModuleName) { + @Suppress("DEPRECATION") + addSubprojectChildTasks("${name}Partial") + configuration() + description = "Runs all subprojects '$name' tasks and generates module navigation page" + } + + project.tasks.register<DefaultTask>("${name}Multimodule") { + group = "deprecated" + description = "DEPRECATED: 'Multimodule' is deprecated. Use 'MultiModule' instead." + dependsOn(multiModuleName) + doLast { + logger.warn("'Multimodule' is deprecated. Use 'MultiModule' instead") + } + } + } + + project.tasks.register<DokkaCollectorTask>("${name}Collector") { + @Suppress("DEPRECATION") + addSubprojectChildTasks(name) + description = + "Generates documentation merging all subprojects '$name' tasks into one virtual module" + } + } + } + + private fun Project.configureEachAbstractDokkaTask() { + tasks.withType<AbstractDokkaTask>().configureEach { + val formatClassifier = name.removePrefix("dokka").decapitalize() + outputDirectory.convention(project.layout.buildDirectory.dir("dokka/$formatClassifier")) + cacheRoot.convention(project.layout.dir(providers.provider { DokkaDefaults.cacheRoot })) + } + } + + private fun Project.configureEachDokkaMultiModuleTask() { + tasks.withType<DokkaMultiModuleTask>().configureEach { + sourceChildOutputDirectories.from({ childDokkaTasks.map { it.outputDirectory } }) + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaProperty.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaProperty.kt new file mode 100644 index 00000000..6c6e967d --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaProperty.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.provider.Provider + + +internal fun Provider<String>.getValidVersionOrNull() = orNull?.takeIf { it != "unspecified" } diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaSourceSetMapper.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaSourceSetMapper.kt new file mode 100644 index 00000000..c0112719 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/DokkaSourceSetMapper.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.jetbrains.dokka.* +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink +import java.io.File + +internal fun GradleDokkaSourceSetBuilder.toDokkaSourceSetImpl(): DokkaSourceSetImpl = DokkaSourceSetImpl( + classpath = classpath.toList(), + displayName = displayNameOrDefault(), + sourceSetID = sourceSetID, + sourceRoots = sourceRoots.toSet(), + dependentSourceSets = dependentSourceSets.get().toSet(), + samples = samples.toSet(), + includes = includes.toSet(), + includeNonPublic = includeNonPublic.get(), + documentedVisibilities = documentedVisibilities.get(), + reportUndocumented = reportUndocumented.get(), + skipEmptyPackages = skipEmptyPackages.get(), + skipDeprecated = skipDeprecated.get(), + jdkVersion = jdkVersion.get(), + sourceLinks = sourceLinks.get().build().toSet(), + perPackageOptions = perPackageOptions.get().build(), + externalDocumentationLinks = externalDocumentationLinksWithDefaults(), + languageVersion = languageVersion.orNull, + apiVersion = apiVersion.orNull, + noStdlibLink = noStdlibLink.get(), + noJdkLink = noJdkLink.get(), + suppressedFiles = suppressedFilesWithDefaults(), + analysisPlatform = platform.get() +) + +private fun GradleDokkaSourceSetBuilder.displayNameOrDefault(): String { + displayName.orNull?.let { return it } + if (name.endsWith("Main") && name != "Main") { + return name.removeSuffix("Main") + } + + return name +} + +private fun GradleDokkaSourceSetBuilder.externalDocumentationLinksWithDefaults(): Set<ExternalDocumentationLinkImpl> { + return externalDocumentationLinks.get().build() + .run { + if (noJdkLink.get()) this + else this + ExternalDocumentationLink.jdk(jdkVersion.get()) + } + .run { + if (noStdlibLink.get()) this + else this + ExternalDocumentationLink.kotlinStdlib() + } + .run { + if (noAndroidSdkLink.get() || !project.isAndroidProject()) this + else this + + ExternalDocumentationLink.androidSdk() + + ExternalDocumentationLink.androidX() + } + .toSet() +} + +private fun GradleDokkaSourceSetBuilder.suppressedFilesWithDefaults(): Set<File> { + val suppressedGeneratedFiles = if (suppressGeneratedFiles.get()) { + val generatedRoot = project.buildDir.resolve("generated").absoluteFile + sourceRoots + .filter { it.startsWith(generatedRoot) } + .flatMap { it.walk().toList() } + .toSet() + } else { + emptySet() + } + + return suppressedFiles.toSet() + suppressedGeneratedFiles +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder.kt new file mode 100644 index 00000000..aca3721a --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder.kt @@ -0,0 +1,483 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import groovy.lang.Closure +import org.gradle.api.* +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.property +import org.gradle.kotlin.dsl.setProperty +import org.jetbrains.dokka.* +import java.io.File +import java.net.URL + +/** + * [Source set](https://kotlinlang.org/docs/multiplatform-discover-project.html#source-sets) level configuration. + * + * Can be configured in the following way with Gradle Kotlin DSL: + * + * ```kotlin + * import org.jetbrains.dokka.gradle.DokkaTask + * + * tasks.dokkaHtml { + * dokkaSourceSets { + * // configure individual source set by name + * named("customSourceSet") { + * suppress.set(true) + * } + * + * // configure all source sets at once + * configureEach { + * reportUndocumented.set(true) + * } + * } + * } + * ``` + */ +open class GradleDokkaSourceSetBuilder( + @Transient @get:Input val name: String, + @Transient @get:Internal internal val project: Project, + @Transient @get:Internal internal val sourceSetIdFactory: NamedDomainObjectFactory<DokkaSourceSetID>, +) : DokkaConfigurationBuilder<DokkaSourceSetImpl> { + + @Input + val sourceSetID: DokkaSourceSetID = sourceSetIdFactory.create(name) + + /** + * Whether this source set should be skipped when generating documentation. + * + * Default is `false`. + */ + @Input + val suppress: Property<Boolean> = project.objects.property<Boolean>() + .convention(false) + + /** + * Display name used to refer to the source set. + * + * The name will be used both externally (for example, source set name visible to documentation readers) and + * internally (for example, for logging messages of [reportUndocumented]). + * + * By default, the value is deduced from information provided by the Kotlin Gradle plugin. + */ + @Input + @Optional + val displayName: Property<String?> = project.objects.property() + + /** + * List of Markdown files that contain + * [module and package documentation](https://kotlinlang.org/docs/dokka-module-and-package-docs.html). + * + * Contents of specified files will be parsed and embedded into documentation as module and package descriptions. + * + * Example of such a file: + * + * ```markdown + * # Module kotlin-demo + * + * The module shows the Dokka usage. + * + * # Package org.jetbrains.kotlin.demo + * + * Contains assorted useful stuff. + * + * ## Level 2 heading + * + * Text after this heading is also part of documentation for `org.jetbrains.kotlin.demo` + * + * # Package org.jetbrains.kotlin.demo2 + * + * Useful stuff in another package. + * ``` + */ + @InputFiles + @Optional + @PathSensitive(PathSensitivity.RELATIVE) + val includes: ConfigurableFileCollection = project.files() + + /** + * Set of visibility modifiers that should be documented. + * + * This can be used if you want to document protected/internal/private declarations, + * as well as if you want to exclude public declarations and only document internal API. + * + * Can be configured on per-package basis, see [GradlePackageOptionsBuilder.documentedVisibilities]. + * + * Default is [DokkaConfiguration.Visibility.PUBLIC]. + */ + @Input + val documentedVisibilities: SetProperty<DokkaConfiguration.Visibility> = + project.objects.setProperty<DokkaConfiguration.Visibility>() + .convention(DokkaDefaults.documentedVisibilities) + + /** + * Specifies source sets that current source set depends on. + * + * Among other things, this information is needed to resolve + * [expect/actual](https://kotlinlang.org/docs/multiplatform-connect-to-apis.html) declarations. + * + * Prefer using [dependsOn] function to append dependent source sets to this list. + * + * By default, the values are deduced from information provided by the Kotlin Gradle plugin. + */ + @Input + val dependentSourceSets: SetProperty<DokkaSourceSetID> = project.objects.setProperty<DokkaSourceSetID>() + .convention(emptySet()) + + /** + * Classpath for analysis and interactive samples. + * + * Useful if some types that come from dependencies are not resolved/picked up automatically. + * Property accepts both `.jar` and `.klib` files. + * + * By default, classpath is deduced from information provided by the Kotlin Gradle plugin. + */ + @Classpath + @Optional + val classpath: ConfigurableFileCollection = project.files() + + /** + * Source code roots to be analyzed and documented. + * Accepts directories and individual `.kt` / `.java` files. + * + * Prefer using [sourceRoot] function to append source roots to this list. + * + * By default, source roots are deduced from information provided by the Kotlin Gradle plugin. + */ + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + val sourceRoots: ConfigurableFileCollection = project.objects.fileCollection() + + /** + * List of directories or files that contain sample functions which are referenced via + * [@sample](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) KDoc tag. + */ + @InputFiles + @Optional + @PathSensitive(PathSensitivity.RELATIVE) + val samples: ConfigurableFileCollection = project.files() + + /** + * Whether to emit warnings about visible undocumented declarations, that is declarations without KDocs + * after they have been filtered by [documentedVisibilities]. + * + * This setting works well with [AbstractDokkaTask.failOnWarning]. + * + * Can be overridden for a specific package by setting [GradlePackageOptionsBuilder.reportUndocumented]. + * + * Default is `false`. + */ + @Input + val reportUndocumented: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.reportUndocumented) + + /** + * Specifies the location of the project source code on the Web. If provided, Dokka generates + * "source" links for each declaration. See [GradleSourceLinkBuilder] for more details. + * + * Prefer using [sourceLink] action/closure for adding source links. + */ + @Nested + val sourceLinks: SetProperty<GradleSourceLinkBuilder> = project.objects.setProperty<GradleSourceLinkBuilder>() + .convention(emptySet()) + + /** + * Allows to customize documentation generation options on a per-package basis. + * + * @see GradlePackageOptionsBuilder for details + */ + @Nested + val perPackageOptions: ListProperty<GradlePackageOptionsBuilder> = + project.objects.listProperty<GradlePackageOptionsBuilder>() + .convention(emptyList()) + + /** + * Allows linking to Dokka/Javadoc documentation of the project's dependencies. + * + * Prefer using [externalDocumentationLink] action/closure for adding links. + */ + @Nested + val externalDocumentationLinks: SetProperty<GradleExternalDocumentationLinkBuilder> = + project.objects.setProperty<GradleExternalDocumentationLinkBuilder>() + .convention(emptySet()) + + /** + * Platform to be used for setting up code analysis and samples. + * + * The default value is deduced from information provided by the Kotlin Gradle plugin. + */ + @Input + @Optional + val platform: Property<Platform> = project.objects.property<Platform>() + .convention(Platform.DEFAULT) + + /** + * Whether to skip packages that contain no visible declarations after + * various filters have been applied. + * + * For instance, if [skipDeprecated] is set to `true` and your package contains only + * deprecated declarations, it will be considered to be empty. + * + * Default is `true`. + */ + @Input + val skipEmptyPackages: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.skipEmptyPackages) + + /** + * Whether to document declarations annotated with [Deprecated]. + * + * Can be overridden on package level by setting [GradlePackageOptionsBuilder.skipDeprecated]. + * + * Default is `false`. + */ + @Input + val skipDeprecated: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.skipDeprecated) + + /** + * Directories or individual files that should be suppressed, meaning declarations from them + * will be not documented. + * + * Will be concatenated with generated files if [suppressGeneratedFiles] is set to `false`. + */ + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + val suppressedFiles: ConfigurableFileCollection = project.files() + + /** + * Whether to document/analyze generated files. + * + * Generated files are expected to be present under `{project}/{buildDir}/generated` directory. + * If set to `true`, it effectively adds all files from that directory to [suppressedFiles], so + * you can configure it manually. + * + * Default is `true`. + */ + @Input + val suppressGeneratedFiles: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.suppressGeneratedFiles) + + /** + * Whether to generate external documentation links that lead to API reference + * documentation for Kotlin's standard library when declarations from it are used. + * + * Default is `false`, meaning links will be generated. + */ + @Input + val noStdlibLink: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.noStdlibLink) + + /** + * Whether to generate external documentation links to JDK's Javadocs + * when declarations from it are used. + * + * The version of JDK Javadocs is determined by [jdkVersion] property. + * + * Default is `false`, meaning links will be generated. + */ + @Input + val noJdkLink: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.noJdkLink) + + /** + * Whether to generate external documentation links for Android SDK API reference + * when declarations from it are used. + * + * Only relevant in Android projects, ignored otherwise. + * + * Default is `false`, meaning links will be generated. + */ + @Input + val noAndroidSdkLink: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.noAndroidSdkLink) + + /** + * [Kotlin language version](https://kotlinlang.org/docs/compatibility-modes.html) + * used for setting up analysis and [@sample](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) + * environment. + * + * By default, the latest language version available to Dokka's embedded compiler will be used. + */ + @Input + @Optional + val languageVersion: Property<String?> = project.objects.property() + + /** + * [Kotlin API version](https://kotlinlang.org/docs/compatibility-modes.html) + * used for setting up analysis and [@sample](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) + * environment. + * + * By default, it will be deduced from [languageVersion]. + */ + @Input + @Optional + val apiVersion: Property<String?> = project.objects.property() + + /** + * JDK version to use when generating external documentation links for Java types. + * + * For instance, if you use [java.util.UUID] from JDK in some public declaration signature, + * and this property is set to `8`, Dokka will generate an external documentation link + * to [JDK 8 Javadocs](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) for it. + * + * Default is JDK 8. + */ + @Input + val jdkVersion: Property<Int> = project.objects.property<Int>() + .convention(DokkaDefaults.jdkVersion) + + /** + * Deprecated. Use [documentedVisibilities] instead. + */ + @Input + val includeNonPublic: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.includeNonPublic) + + fun DokkaSourceSetID(sourceSetName: String): DokkaSourceSetID = sourceSetIdFactory.create(sourceSetName) + + /** + * Convenient override to **append** source sets to [dependentSourceSets] + */ + fun dependsOn(sourceSet: SourceSet) { + dependsOn(DokkaSourceSetID(sourceSet.name)) + } + + /** + * Convenient override to **append** source sets to [dependentSourceSets] + */ + fun dependsOn(sourceSet: GradleDokkaSourceSetBuilder) { + dependsOn(sourceSet.sourceSetID) + } + + /** + * Convenient override to **append** source sets to [dependentSourceSets] + */ + fun dependsOn(sourceSet: DokkaConfiguration.DokkaSourceSet) { + dependsOn(sourceSet.sourceSetID) + } + + /** + * Convenient override to **append** source sets to [dependentSourceSets] + */ + fun dependsOn(sourceSetName: String) { + dependsOn(DokkaSourceSetID(sourceSetName)) + } + + /** + * Convenient override to **append** source sets to [dependentSourceSets] + */ + fun dependsOn(sourceSetID: DokkaSourceSetID) { + dependentSourceSets.add(sourceSetID) + } + + /** + * Convenient override to **append** source roots to [sourceRoots] + */ + fun sourceRoot(file: File) { + sourceRoots.from(file) + } + + /** + * Convenient override to **append** source roots to [sourceRoots] + */ + fun sourceRoot(path: String) { + sourceRoot(project.file(path)) + } + + /** + * Closure for configuring source links, appending to [sourceLinks]. + * + * @see [GradleSourceLinkBuilder] for details. + */ + @Suppress("DEPRECATION") + fun sourceLink(c: Closure<in GradleSourceLinkBuilder>) { + val configured = org.gradle.util.ConfigureUtil.configure(c, GradleSourceLinkBuilder(project)) + sourceLinks.add(configured) + } + + /** + * Action for configuring source links, appending to [sourceLinks]. + * + * @see [GradleSourceLinkBuilder] for details. + */ + fun sourceLink(action: Action<in GradleSourceLinkBuilder>) { + val sourceLink = GradleSourceLinkBuilder(project) + action.execute(sourceLink) + sourceLinks.add(sourceLink) + } + + /** + * Closure for configuring package options, appending to [perPackageOptions]. + * + * @see [GradlePackageOptionsBuilder] for details. + */ + @Suppress("DEPRECATION") + fun perPackageOption(c: Closure<in GradlePackageOptionsBuilder>) { + val configured = org.gradle.util.ConfigureUtil.configure(c, GradlePackageOptionsBuilder(project)) + perPackageOptions.add(configured) + } + + /** + * Action for configuring package options, appending to [perPackageOptions]. + * + * @see [GradlePackageOptionsBuilder] for details. + */ + fun perPackageOption(action: Action<in GradlePackageOptionsBuilder>) { + val option = GradlePackageOptionsBuilder(project) + action.execute(option) + perPackageOptions.add(option) + } + + /** + * Closure for configuring external documentation links, appending to [externalDocumentationLinks]. + * + * @see [GradleExternalDocumentationLinkBuilder] for details. + */ + @Suppress("DEPRECATION") + fun externalDocumentationLink(c: Closure<in GradleExternalDocumentationLinkBuilder>) { + val link = org.gradle.util.ConfigureUtil.configure(c, GradleExternalDocumentationLinkBuilder(project)) + externalDocumentationLinks.add(link) + } + + /** + * Action for configuring external documentation links, appending to [externalDocumentationLinks]. + * + * See [GradleExternalDocumentationLinkBuilder] for details. + */ + fun externalDocumentationLink(action: Action<in GradleExternalDocumentationLinkBuilder>) { + val link = GradleExternalDocumentationLinkBuilder(project) + action.execute(link) + externalDocumentationLinks.add(link) + } + + /** + * Convenient override to **append** external documentation links to [externalDocumentationLinks]. + */ + fun externalDocumentationLink(url: String, packageListUrl: String? = null) { + externalDocumentationLink(URL(url), packageListUrl = packageListUrl?.let(::URL)) + } + + /** + * Convenient override to **append** external documentation links to [externalDocumentationLinks]. + */ + fun externalDocumentationLink(url: URL, packageListUrl: URL? = null) { + externalDocumentationLinks.add( + GradleExternalDocumentationLinkBuilder(project).apply { + this.url.convention(url) + if (packageListUrl != null) { + this.packageListUrl.convention(packageListUrl) + } + } + ) + } + + override fun build(): DokkaSourceSetImpl = toDokkaSourceSetImpl() +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderExtensions.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderExtensions.kt new file mode 100644 index 00000000..fd962acc --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderExtensions.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +/** + * Convenient override to **append** source sets to [GradleDokkaSourceSetBuilder.dependentSourceSets] + */ +fun GradleDokkaSourceSetBuilder.dependsOn(sourceSet: KotlinSourceSet) { + dependsOn(DokkaSourceSetID(sourceSet.name)) +} + +/** + * Convenient override to **append** source sets to [GradleDokkaSourceSetBuilder.dependentSourceSets] + */ +fun GradleDokkaSourceSetBuilder.dependsOn(@Suppress("DEPRECATION") sourceSet: com.android.build.gradle.api.AndroidSourceSet) { + dependsOn(DokkaSourceSetID(sourceSet.name)) +} + +/** + * Convenient override to **append** source sets to [GradleDokkaSourceSetBuilder.dependentSourceSets] + */ +fun GradleDokkaSourceSetBuilder.dependsOn(@Suppress("UnstableApiUsage") sourceSet: com.android.build.api.dsl.AndroidSourceSet) { + dependsOn(DokkaSourceSetID(sourceSet.name)) +} + +/** + * Extension allowing configuration of Dokka source sets via Kotlin Gradle plugin source sets. + */ +fun GradleDokkaSourceSetBuilder.kotlinSourceSet(kotlinSourceSet: KotlinSourceSet) { + configureWithKotlinSourceSet(kotlinSourceSet) +} + diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderFactory.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderFactory.kt new file mode 100644 index 00000000..b2fc0394 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderFactory.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.NamedDomainObjectFactory + +@Suppress("ObjectLiteralToLambda") // Will fail at runtime in Gradle versions <= 6.6 +fun AbstractDokkaTask.gradleDokkaSourceSetBuilderFactory(): NamedDomainObjectFactory<GradleDokkaSourceSetBuilder> = + NamedDomainObjectFactory { name -> GradleDokkaSourceSetBuilder(name, project, DokkaSourceSetIdFactory()) } + diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleExternalDocumentationLinkBuilder.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleExternalDocumentationLinkBuilder.kt new file mode 100644 index 00000000..8137d7a9 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleExternalDocumentationLinkBuilder.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.kotlin.dsl.property +import org.jetbrains.dokka.DokkaConfigurationBuilder +import org.jetbrains.dokka.ExternalDocumentationLink +import org.jetbrains.dokka.ExternalDocumentationLinkImpl +import java.net.URL + +/** + * Configuration builder that allows creating links leading to externally hosted + * documentation of your dependencies. + * + * For instance, if you are using types from `kotlinx.serialization`, by default + * they will be unclickable in your documentation, as if unresolved. However, + * since API reference for `kotlinx.serialization` is also built by Dokka and is + * [published on kotlinlang.org](https://kotlinlang.org/api/kotlinx.serialization/), + * you can configure external documentation links for it, allowing Dokka to generate + * documentation links for used types, making them clickable and appear resolved. + * + * Example in Gradle Kotlin DSL: + * + * ```kotlin + * externalDocumentationLink { + * url.set(URL("https://kotlinlang.org/api/kotlinx.serialization/")) + * packageListUrl.set( + * rootProject.projectDir.resolve("serialization.package.list").toURL() + * ) + * } + * ``` + */ +class GradleExternalDocumentationLinkBuilder( + @Transient @get:Internal internal val project: Project +) : DokkaConfigurationBuilder<ExternalDocumentationLinkImpl> { + + /** + * Root URL of documentation to link with. **Must** contain a trailing slash. + * + * Dokka will do its best to automatically find `package-list` for the given URL, and link + * declarations together. + * + * It automatic resolution fails or if you want to use locally cached files instead, + * consider providing [packageListUrl]. + * + * Example: + * + * ```kotlin + * java.net.URL("https://kotlinlang.org/api/kotlinx.serialization/") + * ``` + */ + @Internal + val url: Property<URL> = project.objects.property() + + @Input // URL is deprecated in gradle inputs + internal fun getUrlString() = url.map(URL::toString) + + /** + * Specifies the exact location of a `package-list` instead of relying on Dokka + * automatically resolving it. Can also be a locally cached file to avoid network calls. + * + * Example: + * + * ```kotlin + * rootProject.projectDir.resolve("serialization.package.list").toURL() + * ``` + */ + @Internal + val packageListUrl: Property<URL> = project.objects.property() + + @Input // URL is deprecated in gradle inputs + @Optional + internal fun getPackageListUrlString() = packageListUrl.map(URL::toString) + + override fun build(): ExternalDocumentationLinkImpl = ExternalDocumentationLink( + url = checkNotNull(url.get()) { "url not specified " }, + packageListUrl = packageListUrl.orNull, + ) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradlePackageOptionsBuilder.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradlePackageOptionsBuilder.kt new file mode 100644 index 00000000..93b1f52c --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradlePackageOptionsBuilder.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.kotlin.dsl.* +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationBuilder +import org.jetbrains.dokka.DokkaDefaults +import org.jetbrains.dokka.PackageOptionsImpl + +/** + * Configuration builder that allows setting some options for specific packages + * matched by [matchingRegex]. + * + * Example in Gradle Kotlin DSL: + * + * ```kotlin + * tasks.dokkaHtml { + * dokkaSourceSets.configureEach { + * perPackageOption { + * matchingRegex.set(".*internal.*") + * suppress.set(true) + * } + * } + * } + * ``` + */ +class GradlePackageOptionsBuilder( + @Transient @get:Internal internal val project: Project +) : DokkaConfigurationBuilder<PackageOptionsImpl> { + + /** + * Regular expression that is used to match the package. + * + * Default is any string: `.*`. + */ + @Input + val matchingRegex: Property<String> = project.objects.property<String>() + .convention(".*") + + /** + * Whether this package should be skipped when generating documentation. + * + * Default is `false`. + */ + @Input + val suppress: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.suppress) + + /** + * Set of visibility modifiers that should be documented. + * + * This can be used if you want to document protected/internal/private declarations within a + * specific package, as well as if you want to exclude public declarations and only document internal API. + * + * Can be configured for a whole source set, see [GradleDokkaSourceSetBuilder.documentedVisibilities]. + * + * Default is [DokkaConfiguration.Visibility.PUBLIC]. + */ + @Input + val documentedVisibilities: SetProperty<DokkaConfiguration.Visibility> = + project.objects.setProperty<DokkaConfiguration.Visibility>() + .convention(DokkaDefaults.documentedVisibilities) + + /** + * Whether to document declarations annotated with [Deprecated]. + * + * Can be overridden on source set level by setting [GradleDokkaSourceSetBuilder.skipDeprecated]. + * + * Default is `false`. + */ + @Input + val skipDeprecated: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.skipDeprecated) + + /** + * Whether to emit warnings about visible undocumented declarations, that is declarations from + * this package and without KDocs, after they have been filtered by [documentedVisibilities]. + * + * This setting works well with [AbstractDokkaTask.failOnWarning]. + * + * Can be overridden on source set level by setting [GradleDokkaSourceSetBuilder.reportUndocumented]. + * + * Default is `false`. + */ + @Input + val reportUndocumented: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.reportUndocumented) + + /** + * Deprecated. Use [documentedVisibilities] instead. + */ + @Input + val includeNonPublic: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.includeNonPublic) + + + override fun build(): PackageOptionsImpl = PackageOptionsImpl( + matchingRegex = matchingRegex.get(), + includeNonPublic = includeNonPublic.get(), + documentedVisibilities = documentedVisibilities.get(), + reportUndocumented = reportUndocumented.get(), + skipDeprecated = skipDeprecated.get(), + suppress = suppress.get() + ) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleSourceLinkBuilder.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleSourceLinkBuilder.kt new file mode 100644 index 00000000..5a267962 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/GradleSourceLinkBuilder.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.kotlin.dsl.property +import org.jetbrains.dokka.DokkaConfigurationBuilder +import org.jetbrains.dokka.SourceLinkDefinitionImpl +import java.io.File +import java.net.URL + +/** + * Configuration builder that allows adding a `source` link to each signature + * which leads to [remoteUrl] with a specific line number (configurable by setting [remoteLineSuffix]), + * letting documentation readers find source code for each declaration. + * + * Example in Gradle Kotlin DSL: + * + * ```kotlin + * sourceLink { + * localDirectory.set(projectDir.resolve("src")) + * remoteUrl.set(URL("https://github.com/kotlin/dokka/tree/master/src")) + * remoteLineSuffix.set("#L") + * } + * ``` + */ +class GradleSourceLinkBuilder( + @Transient @get:Internal internal val project: Project +) : DokkaConfigurationBuilder<SourceLinkDefinitionImpl> { + + /** + * Path to the local source directory. The path must be relative to the root of current project. + * + * This path is used to find relative paths of the source files from which the documentation is built. + * These relative paths are then combined with the base url of a source code hosting service specified with + * the [remoteUrl] property to create source links for each declaration. + * + * Example: + * + * ```kotlin + * projectDir.resolve("src") + * ``` + */ + @Internal // changing contents of the directory should not invalidate the task + val localDirectory: Property<File?> = project.objects.property() + + /** + * The relative path to [localDirectory] from the project directory. Declared as an input to invalidate the task if that path changes. + * Should not be used anywhere directly. + */ + @Suppress("unused") + @get:Input + internal val localDirectoryPath: Provider<String?> = + localDirectory.map { it.relativeToOrSelf(project.projectDir).invariantSeparatorsPath } + + /** + * URL of source code hosting service that can be accessed by documentation readers, + * like GitHub, GitLab, Bitbucket, etc. This URL will be used to generate + * source code links of declarations. + * + * Example: + * + * ```kotlin + * java.net.URL("https://github.com/username/projectname/tree/master/src")) + * ``` + */ + @Internal + val remoteUrl: Property<URL> = project.objects.property() + + @Input // TODO: URL is deprecated in grapdle inputs + internal fun getRemoteUrlString() = remoteUrl.map(URL::toString) + + + /** + * Suffix used to append source code line number to the URL. This will help readers navigate + * not only to the file, but to the specific line number of the declaration. + * + * The number itself will be appended to the specified suffix. For instance, + * if this property is set to `#L` and the line number is 10, resulting URL suffix + * will be `#L10` + * + * Suffixes used by popular services: + * - GitHub: `#L` + * - GitLab: `#L` + * - Bitbucket: `#lines-` + * + * Default is `#L`. + */ + @Optional + @Input + val remoteLineSuffix: Property<String> = project.objects.property<String>() + .convention("#L") + + override fun build(): SourceLinkDefinitionImpl { + return SourceLinkDefinitionImpl( + localDirectory = localDirectory.orNull?.canonicalPath ?: project.projectDir.canonicalPath, + remoteUrl = remoteUrl.get(), + remoteLineSuffix = remoteLineSuffix.get(), + ) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/TaskDependencyInternalWithAdditions.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/TaskDependencyInternalWithAdditions.kt new file mode 100644 index 00000000..d3469f69 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/TaskDependencyInternalWithAdditions.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Task +import org.gradle.api.internal.tasks.DefaultTaskDependency +import org.gradle.api.internal.tasks.TaskDependencyInternal + +internal operator fun TaskDependencyInternal.plus(tasks: Iterable<Task>): TaskDependencyInternal = + TaskDependencyInternalWithAdditions(this, tasks.toSet()) + +private class TaskDependencyInternalWithAdditions( + dependency: TaskDependencyInternal, + additionalTaskDependencies: Set<Task>, +) : DefaultTaskDependency() { + + init { + add(dependency, additionalTaskDependencies) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/automagicTypedProxy.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/automagicTypedProxy.kt new file mode 100644 index 00000000..8e397a4a --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/automagicTypedProxy.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import java.lang.reflect.InvocationHandler +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.lang.reflect.Proxy + + +/** + * Warning! Hard reflection magic used here. + * + * Creates [java.lang.reflect.Proxy] with pass through invocation algorithm, + * to create access proxy for [delegate] into [targetClassLoader]. + */ +internal inline fun <reified T : Any> automagicTypedProxy(targetClassLoader: ClassLoader, delegate: Any): T = + automagicProxy(targetClassLoader, T::class.java, delegate) as T + + +/** + * Warning! Hard reflection magic used here. + * + * Creates [java.lang.reflect.Proxy] with pass through invocation algorithm, + * to create access proxy for [delegate] into [targetClassLoader]. + * + */ +private fun automagicProxy(targetClassLoader: ClassLoader, targetType: Class<*>, delegate: Any): Any = + Proxy.newProxyInstance( + targetClassLoader, + arrayOf(targetType), + DelegatedInvocationHandler(delegate) + ) + +private class DelegatedInvocationHandler(private val delegate: Any) : InvocationHandler { + + @Throws(Throwable::class) + override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? { + val delegateMethod = delegate.javaClass.getMethod(method.name, *method.parameterTypes) + try { + delegateMethod.isAccessible = true + return delegateMethod.invoke(delegate, *(args ?: emptyArray())) + } catch (ex: InvocationTargetException) { + throw ex.targetException + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/checkChildDokkaTasksIsNotEmpty.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/checkChildDokkaTasksIsNotEmpty.kt new file mode 100644 index 00000000..64fc1f9f --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/checkChildDokkaTasksIsNotEmpty.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.jetbrains.dokka.DokkaException + +internal fun AbstractDokkaParentTask.checkChildDokkaTasksIsNotEmpty() { + if (childDokkaTaskPaths.isEmpty()) { + throw DokkaException( + """ + The ${this::class.java.simpleName} $path has no configured child tasks. + Add some dokka tasks like e.g.: + + tasks.named<AbstractDokkaParentTask>("$name") { + addChildTask(..) + addChildTasks(subprojects, "...") + //... + } + """.trimIndent() + ) + } + + if (childDokkaTasks.isEmpty()) { + throw DokkaException( + """ + The ${this::class.java.simpleName} $path could not find any registered child task. + child tasks: $childDokkaTaskPaths + + Please make sure to apply the dokka plugin to all included (sub)-projects individually e.g.: + + // subproject build.gradle.kts + plugins { + id("org.jetbrains.dokka") + } + + or + + // parent build.gradle.kts + subprojects { + plugins.apply("org.jetbrains.dokka") + } + """ + ) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/checkDependentSourceSets.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/checkDependentSourceSets.kt new file mode 100644 index 00000000..630f2dca --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/checkDependentSourceSets.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.jetbrains.dokka.DokkaSourceSetID + +internal fun checkSourceSetDependencies(sourceSets: List<GradleDokkaSourceSetBuilder>) { + checkSourceSetDependencies(sourceSets.associateBy { it.sourceSetID }) +} + +private fun checkSourceSetDependencies(sourceSets: Map<DokkaSourceSetID, GradleDokkaSourceSetBuilder>) { + sourceSets.values.forEach { sourceSet -> + sourceSet.dependentSourceSets.get().forEach { dependentSourceSetID -> + val dependentSourceSet = requireNotNull(sourceSets[dependentSourceSetID]) { + "Dokka source set \"${sourceSet.name}\": Cannot find dependent source set \"$dependentSourceSetID\"" + } + + if (sourceSet.suppress.get().not() && dependentSourceSet.suppress.get()) { + throw IllegalArgumentException( + "Dokka source set: \"${sourceSet.name}\": " + + "Unsuppressed source set cannot depend on suppressed source set \"$dependentSourceSetID\"" + ) + } + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaBootstrapFactory.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaBootstrapFactory.kt new file mode 100644 index 00000000..21a2e8d1 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaBootstrapFactory.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.artifacts.Configuration +import org.jetbrains.dokka.DokkaBootstrap +import java.net.URLClassLoader +import kotlin.reflect.KClass + +fun DokkaBootstrap(configuration: Configuration, bootstrapClass: KClass<out DokkaBootstrap>): DokkaBootstrap { + val runtimeJars = configuration.resolve() + val runtimeClassLoader = URLClassLoader( + runtimeJars.map { it.toURI().toURL() }.toTypedArray(), + ClassLoader.getSystemClassLoader().parent + ) + + val runtimeClassloaderBootstrapClass = runtimeClassLoader.loadClass(bootstrapClass.qualifiedName) + val runtimeClassloaderBootstrapInstance = runtimeClassloaderBootstrapClass.constructors.first().newInstance() + return automagicTypedProxy(DokkaPlugin::class.java.classLoader, runtimeClassloaderBootstrapInstance) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaDefaultOutputDirectory.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaDefaultOutputDirectory.kt new file mode 100644 index 00000000..d92b84a1 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaDefaultOutputDirectory.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Task +import java.io.File + +internal fun Task.defaultDokkaOutputDirectory(): File { + return defaultDokkaOutputDirectory(project.buildDir, name) +} + +internal fun defaultDokkaOutputDirectory(buildDir: File, taskName: String): File { + val formatClassifier = taskName.removePrefix("dokka").decapitalize() + return File(buildDir, "dokka${File.separator}$formatClassifier") +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaSourceSetIDFactory.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaSourceSetIDFactory.kt new file mode 100644 index 00000000..b658e5f6 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/dokkaSourceSetIDFactory.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.NamedDomainObjectFactory +import org.gradle.api.Task +import org.jetbrains.dokka.DokkaSourceSetID + +internal fun DokkaSourceSetID(task: Task, sourceSetName: String): DokkaSourceSetID { + return DokkaSourceSetID(task.path, sourceSetName) +} + +@Suppress("FunctionName") +internal fun Task.DokkaSourceSetIdFactory() = NamedDomainObjectFactory<DokkaSourceSetID> { name -> + DokkaSourceSetID(this@DokkaSourceSetIdFactory, name) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/gradleConfigurations.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/gradleConfigurations.kt new file mode 100644 index 00000000..63424e1e --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/gradleConfigurations.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.Dependency +import org.gradle.api.attributes.Usage +import org.gradle.kotlin.dsl.named + +internal fun Project.shouldUseK2() = + (findProperty("org.jetbrains.dokka.experimental.tryK2") as? String)?.toBoolean() ?: false + +internal fun Project.maybeCreateDokkaDefaultPluginConfiguration(): Configuration { + return configurations.maybeCreate("dokkaPlugin") { + attributes.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.JAVA_RUNTIME)) + isCanBeConsumed = false + } +} + +internal fun Project.maybeCreateDokkaDefaultRuntimeConfiguration(): Configuration { + return configurations.maybeCreate("dokkaRuntime") { + attributes.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.JAVA_RUNTIME)) + isCanBeConsumed = false + } +} + +internal fun Project.maybeCreateDokkaPluginConfiguration(dokkaTaskName: String, additionalDependencies: Collection<Dependency> = emptySet()): Configuration { + return project.configurations.maybeCreate("${dokkaTaskName}Plugin") { + extendsFrom(maybeCreateDokkaDefaultPluginConfiguration()) + attributes.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.JAVA_RUNTIME)) + isCanBeConsumed = false + dependencies.add( + if (shouldUseK2()) project.dokkaArtifacts.analysisKotlinSymbols + else project.dokkaArtifacts.analysisKotlinDescriptors + ) + dependencies.add(project.dokkaArtifacts.dokkaBase) + dependencies.addAll(additionalDependencies) + } +} + +internal fun Project.maybeCreateDokkaRuntimeConfiguration(dokkaTaskName: String): Configuration { + return project.configurations.maybeCreate("${dokkaTaskName}Runtime") { + extendsFrom(maybeCreateDokkaDefaultRuntimeConfiguration()) + attributes.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.JAVA_RUNTIME)) + isCanBeConsumed = false + defaultDependencies { + add(project.dokkaArtifacts.dokkaCore) + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/internal/AbstractDokkaTaskExtensions.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/internal/AbstractDokkaTaskExtensions.kt new file mode 100644 index 00000000..7675c69b --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/internal/AbstractDokkaTaskExtensions.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.internal + +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.gradle.AbstractDokkaTask +import org.jetbrains.dokka.toPrettyJsonString +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.toCompactJsonString + +/** + * Serializes [DokkaConfiguration] of this [AbstractDokkaTask] as json + * + * Should be used for short-term debugging only, no guarantees are given for the support of this API. + * + * Better alternative should be introduced as part of [#2873](https://github.com/Kotlin/dokka/issues/2873). + */ +@InternalDokkaApi +fun AbstractDokkaTask.buildJsonConfiguration(prettyPrint: Boolean = true): String { + val configuration = this.buildDokkaConfiguration() + return if (prettyPrint) { + configuration.toPrettyJsonString() + } else { + configuration.toCompactJsonString() + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinGradlePluginVersion.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinGradlePluginVersion.kt new file mode 100644 index 00000000..72f1a626 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinGradlePluginVersion.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion + +internal typealias KotlinGradlePluginVersion = KotlinVersion + +internal fun Project.getKgpVersion(): KotlinGradlePluginVersion? = parseKotlinVersion(this.getKotlinPluginVersion()) + +/** + * Accepts a full version string that contains the major, minor + * and patch versions divided by dots, such as "1.7.10". + * + * Does NOT parse and store custom suffixes, so `1.8.20-RC2` + * or `1.8.20-dev-42` will be viewed as `1.8.20`. + */ +internal fun parseKotlinVersion(fullVersionString: String): KotlinVersion? { + val versionParts = fullVersionString + .split(".", "-", limit = 4) + .takeIf { parts -> parts.size >= 3 && parts.subList(0, 3).all { it.isNumeric() } } + ?: return null + + return KotlinVersion( + major = versionParts[0].toInt(), + minor = versionParts[1].toInt(), + patch = versionParts[2].toInt() + ) +} + +private fun String.isNumeric() = this.isNotEmpty() && this.all { it.isDigit() } diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinNativeDistributionAccessor.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinNativeDistributionAccessor.kt new file mode 100644 index 00000000..3180efef --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinNativeDistributionAccessor.kt @@ -0,0 +1,39 @@ +@file:Suppress("INVISIBLE_REFERENCE") +package org.jetbrains.dokka.gradle.kotlin + +import java.io.File +import org.gradle.api.Project +import org.jetbrains.kotlin.commonizer.KonanDistribution +import org.jetbrains.kotlin.commonizer.platformLibsDir +import org.jetbrains.kotlin.commonizer.stdlib +import org.jetbrains.kotlin.compilerRunner.konanHome +import org.jetbrains.kotlin.konan.target.KonanTarget + +/** + * Provides access to the Kotlin/Native distribution components: + * * [stdlibDir] -- stdlib directory + * * [platformDependencies] -- list of directories to platform dependencies + * + * It uses Kotlin Gradle Plugin API that is guaranteed to be present in: + * 1.5 <= kotlinVersion <= 1.9 + * + * It should not be used with Kotlin versions later than 1.9 + */ +internal class KotlinNativeDistributionAccessor( + project: Project +) { + private val konanDistribution = KonanDistribution( + @Suppress("INVISIBLE_MEMBER") + project.konanHome + ) + + val stdlibDir: File = konanDistribution.stdlib + + fun platformDependencies(target: KonanTarget): List<File> = konanDistribution + .platformLibsDir + .resolve(target.name) + .listLibraryFiles() + + private fun File.listLibraryFiles(): List<File> = listFiles().orEmpty() + .filter { it.isDirectory || it.extension == "klib" } +}
\ No newline at end of file diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinSourceSetGist.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinSourceSetGist.kt new file mode 100644 index 00000000..18d7ebb2 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinSourceSetGist.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.Provider +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +internal data class KotlinSourceSetGist( + val name: String, + val platform: Provider<KotlinPlatformType>, + val isMain: Provider<Boolean>, + val classpath: Provider<FileCollection>, + val sourceRoots: FileCollection, + val dependentSourceSetNames: Provider<Set<String>>, +) + +internal fun Project.gistOf(sourceSet: KotlinSourceSet): KotlinSourceSetGist = KotlinSourceSetGist( + name = sourceSet.name, + platform = project.provider { platformOf(sourceSet) }, + isMain = project.provider { isMainSourceSet(sourceSet) }, + classpath = project.provider { classpathOf(sourceSet).filter { it.exists() } }, + // TODO: Needs to respect filters. + // We probably need to change from "sourceRoots" to support "sourceFiles" + // https://github.com/Kotlin/dokka/issues/1215 + sourceRoots = sourceSet.kotlin.sourceDirectories.filter { it.exists() }, + dependentSourceSetNames = project.provider { sourceSet.dependsOn.map { it.name }.toSet() }, +) + diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/isMainSourceSet.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/isMainSourceSet.kt new file mode 100644 index 00000000..b8abaca3 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/isMainSourceSet.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import com.android.build.gradle.api.ApplicationVariant +import com.android.build.gradle.api.LibraryVariant +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation + +internal fun Project.isMainSourceSet(sourceSet: KotlinSourceSet): Boolean { + return isMainSourceSet(allCompilationsOf(sourceSet)) +} + +internal fun isMainSourceSet(compilations: List<KotlinCompilation>): Boolean { + return compilations.any { compilation -> isMainCompilation(compilation) } +} + +private fun isMainCompilation(compilation: KotlinCompilation): Boolean { + try { + val androidVariant = compilation.run { this as? KotlinJvmAndroidCompilation }?.androidVariant + if (androidVariant != null) { + return androidVariant is LibraryVariant || androidVariant is ApplicationVariant + } + } catch (e: NoSuchMethodError) { + // Kotlin Plugin version below 1.4 + return !compilation.name.toLowerCase().endsWith("test") + } + return compilation.name == "main" +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinClasspathUtils.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinClasspathUtils.kt new file mode 100644 index 00000000..778261a7 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinClasspathUtils.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.jetbrains.dokka.gradle.isAndroidTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.AbstractKotlinNativeCompilation +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +internal fun Project.classpathOf(sourceSet: KotlinSourceSet): FileCollection { + val compilations = compilationsOf(sourceSet) + return if (compilations.isNotEmpty()) { + compilations + .map { compilation -> compilation.compileClasspathOf(project = this) } + .reduce(FileCollection::plus) + } else { + // Dokka suppresses source sets that do no have compilations + // since such configuration is invalid, it reports a warning or an error + sourceSet.withAllDependentSourceSets() + .map { it.kotlin.sourceDirectories } + .reduce(FileCollection::plus) + } +} + +private fun KotlinCompilation.compileClasspathOf(project: Project): FileCollection { + val kgpVersion = project.getKgpVersion() + + // if KGP version < 1.9 or org.jetbrains.dokka.classpath.useOldResolution=true + // we will use old (pre 1.9) resolution of classpath + if (kgpVersion == null || + kgpVersion < KotlinGradlePluginVersion(1, 9, 0) || + project.classpathProperty("useOldResolution", default = false) + ) { + return oldCompileClasspathOf(project) + } + + return newCompileClasspathOf(project) +} + +private fun KotlinCompilation.newCompileClasspathOf(project: Project): FileCollection { + if (this.target.isAndroidTarget()) { // Workaround for https://youtrack.jetbrains.com/issue/KT-33893 + return this.classpathOf(project) + } + + val result = project.objects.fileCollection() + result.from({ compileDependencyFiles }) + + val kgpVersion = project.getKgpVersion() + // Since Kotlin 2.0 native distributiuon dependencies will be included to compileDependencyFiles + if (kgpVersion != null && kgpVersion <= KotlinGradlePluginVersion(1, 9, 255)) { + if (this is AbstractKotlinNativeCompilation) { + val kotlinNativeDistributionAccessor = KotlinNativeDistributionAccessor(project) + result.from(kotlinNativeDistributionAccessor.stdlibDir) + result.from(kotlinNativeDistributionAccessor.platformDependencies(konanTarget)) + } + } + + return result +} + +private fun KotlinCompilation.oldCompileClasspathOf(project: Project): FileCollection { + if (this.target.isAndroidTarget()) { // Workaround for https://youtrack.jetbrains.com/issue/KT-33893 + return this.classpathOf(project) + } + + return this.compileDependencyFiles + platformDependencyFiles(project) + this.classpathOf(project) +} + +private fun KotlinCompilation.classpathOf(project: Project): FileCollection { + val kgpVersion = project.getKgpVersion() + val kotlinCompile = this.getKotlinCompileTask(kgpVersion) ?: return project.files() + + val shouldKeepBackwardsCompatibility = (kgpVersion != null && kgpVersion < KotlinGradlePluginVersion(1, 7, 0)) + return if (shouldKeepBackwardsCompatibility) { + // removed since 1.9.0, left for compatibility with < Kotlin 1.7 + val classpathGetter = kotlinCompile::class.members + .first { it.name == "getClasspath" } + classpathGetter.call(kotlinCompile) as FileCollection + } else { + kotlinCompile.libraries // introduced in 1.7.0 + } +} + +private fun KotlinCompilation.getKotlinCompileTask(kgpVersion: KotlinGradlePluginVersion? = null): KotlinCompile? { + val shouldKeepBackwardsCompatibility = (kgpVersion != null && kgpVersion < KotlinGradlePluginVersion(1, 8, 0)) + return if (shouldKeepBackwardsCompatibility) { + @Suppress("DEPRECATION") // for `compileKotlinTask` property, deprecated with warning since 1.8.0 + this.compileKotlinTask as? KotlinCompile + } else { + this.compileTaskProvider.get() as? KotlinCompile // introduced in 1.8.0 + } +} + +private fun KotlinCompilation.platformDependencyFiles(project: Project): FileCollection { + val excludePlatformDependencyFiles = project.classpathProperty("excludePlatformDependencyFiles", default = false) + + if (excludePlatformDependencyFiles) return project.files() + return (this as? AbstractKotlinNativeCompilation) + ?.target?.project?.configurations + ?.findByName(@Suppress("DEPRECATION") this.defaultSourceSet.implementationMetadataConfigurationName) // KT-58640 + ?: project.files() +} + +private fun Project.classpathProperty(name: String, default: Boolean): Boolean = + (findProperty("org.jetbrains.dokka.classpath.$name") as? String)?.toBoolean() ?: default diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinCompilationUtils.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinCompilationUtils.kt new file mode 100644 index 00000000..0b1b7419 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinCompilationUtils.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import org.gradle.api.Project +import org.jetbrains.dokka.gradle.kotlin +import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation + +internal typealias KotlinCompilation = + org.jetbrains.kotlin.gradle.plugin.KotlinCompilation<KotlinCommonOptions> + +internal fun Project.compilationsOf(sourceSet: KotlinSourceSet): List<KotlinCompilation> { + //KT-45412 Make sure .kotlinSourceSets and .allKotlinSourceSets include the default source set + val compilations = allCompilationsOf(sourceSet).filter { compilation -> + sourceSet in compilation.kotlinSourceSets || sourceSet == compilation.defaultSourceSet + } + + val hasAdditionalCommonCompatibilityMetadataVariant = compilations.size >= 2 + && this.isHmppEnabled() + && compilations.any { it is KotlinCommonCompilation && it.compilationName == "main" } + && compilations.any { it is KotlinCommonCompilation && it.compilationName == "commonMain" } + + return if (hasAdditionalCommonCompatibilityMetadataVariant) { + // If the project has `kotlin.mpp.enableCompatibilityMetadataVariant` set to `true` + // and it produces a legacy variant for common, we filter it out because one of the dependencies + // might be published without it, and it would lead to the following error: + // + // > Execution failed for task ':project:dokkaHtmlPartial'. + // > Could not resolve all files for configuration ':project:metadataCompileClasspath'. + // > Could not resolve com.example.dependency:0.1.0. + // > The consumer was configured to find a usage of 'kotlin-api' of a library, preferably optimized for + // non-jvm, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'common'. However we + // cannot choose between the following variants of com.example.dependency:0.1.0: + // + // This can be reproduced consistently on Ktor of version 2.3.2 + compilations.filterNot { it is KotlinCommonCompilation && it.compilationName == "main" } + } else { + compilations + } +} + +private fun Project.isHmppEnabled(): Boolean { + // [KotlinCommonCompilation.isKlibCompilation] is internal, so we use this property instead. + // The property name might seem misleading, but it's set by KGP if HMPP is enabled: + // https://github.com/JetBrains/kotlin/blob/1.9.0/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/internal/hierarchicalStructureMigrationHandling.kt#L33 + return (this.findProperty("kotlin.mpp.enableGranularSourceSetsMetadata") as? String)?.toBoolean() + ?: false +} + +internal fun Project.allCompilationsOf( + sourceSet: KotlinSourceSet +): List<KotlinCompilation> { + return when (val kotlin = kotlin) { + is KotlinMultiplatformExtension -> allCompilationsOf(kotlin, sourceSet) + is KotlinSingleTargetExtension<*> -> allCompilationsOf(kotlin, sourceSet) + else -> emptyList() + } +} + +private fun allCompilationsOf( + kotlin: KotlinMultiplatformExtension, + sourceSet: KotlinSourceSet +): List<KotlinCompilation> { + val allCompilations = kotlin.targets.flatMap { target -> target.compilations } + return allCompilations.filter { compilation -> + sourceSet in compilation.allKotlinSourceSets || sourceSet == compilation.defaultSourceSet + } +} + +private fun allCompilationsOf( + kotlin: KotlinSingleTargetExtension<*>, + sourceSet: KotlinSourceSet +): List<KotlinCompilation> { + return kotlin.target.compilations.filter { compilation -> sourceSet in compilation.allKotlinSourceSets } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinSourceSetUtils.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinSourceSetUtils.kt new file mode 100644 index 00000000..f5afd6cb --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/kotlinSourceSetUtils.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + + +internal fun KotlinSourceSet.withAllDependentSourceSets(): Sequence<KotlinSourceSet> { + return sequence { + yield(this@withAllDependentSourceSets) + for (dependentSourceSet in dependsOn) { + yieldAll(dependentSourceSet.withAllDependentSourceSets()) + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/platformOfSourceSet.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/platformOfSourceSet.kt new file mode 100644 index 00000000..8677d890 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/kotlin/platformOfSourceSet.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import org.gradle.api.Project +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +internal fun Project.platformOf(sourceSet: KotlinSourceSet): KotlinPlatformType { + val targetNames = allCompilationsOf(sourceSet).map { compilation -> compilation.target.platformType }.distinct() + return when (targetNames.size) { + 1 -> targetNames.single() + else -> KotlinPlatformType.common + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/sourceSetKotlinGistConfiguration.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/sourceSetKotlinGistConfiguration.kt new file mode 100644 index 00000000..8f21b9d3 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/sourceSetKotlinGistConfiguration.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.gradle.kotlin.KotlinSourceSetGist +import org.jetbrains.dokka.gradle.kotlin.gistOf +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet + +fun GradleDokkaSourceSetBuilder.configureWithKotlinSourceSet(sourceSet: KotlinSourceSet) { + configureWithKotlinSourceSetGist(project.gistOf(sourceSet)) +} + +internal fun GradleDokkaSourceSetBuilder.configureWithKotlinSourceSetGist(sourceSet: KotlinSourceSetGist) { + val dependentSourceSetIds = sourceSet.dependentSourceSetNames.map { sourceSetNames -> + sourceSetNames.map { sourceSetName -> DokkaSourceSetID(sourceSetName) } + } + + this.suppress.convention(sourceSet.isMain.map { !it }) + this.sourceRoots.from(sourceSet.sourceRoots) + this.classpath.from(sourceSet.classpath) + this.platform.convention(sourceSet.platform.map { Platform.fromString(it.name) }) + this.dependentSourceSets.convention(dependentSourceSetIds) + this.displayName.convention(sourceSet.platform.map { platform -> + sourceSet.name.substringBeforeLast( + delimiter = "Main", + missingDelimiterValue = platform.name + ) + }) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaLeafTask.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaLeafTask.kt new file mode 100644 index 00000000..03d40d8b --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaLeafTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.internal.plugins.DslObject +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.kotlin.dsl.container +import org.gradle.work.DisableCachingByDefault + +@DisableCachingByDefault(because = "Abstract super-class, not to be instantiated directly") +abstract class AbstractDokkaLeafTask : AbstractDokkaTask() { + + @get:Internal + val dokkaSourceSets: NamedDomainObjectContainer<GradleDokkaSourceSetBuilder> = + project.container(GradleDokkaSourceSetBuilder::class, gradleDokkaSourceSetBuilderFactory()).also { container -> + DslObject(this).extensions.add("dokkaSourceSets", container) + project.kotlinOrNull?.sourceSets?.all sourceSet@{ + container.register(name) { + configureWithKotlinSourceSet(this@sourceSet) + } + } + } + + /** + * Only contains source sets that are marked with `isDocumented`. + * Non documented source sets are not relevant for Gradle's UP-TO-DATE mechanism, as well + * as task dependency graph. + */ + @get:Nested + protected val unsuppressedSourceSets: List<GradleDokkaSourceSetBuilder> + get() = dokkaSourceSets + .toList() + .also(::checkSourceSetDependencies) + .filterNot { it.suppress.get() } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaParentTask.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaParentTask.kt new file mode 100644 index 00000000..62e98c30 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaParentTask.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested +import org.gradle.work.DisableCachingByDefault + +private const val DEPRECATION_MESSAGE = """ + It is an anti-pattern to declare cross-project dependencies as it leads to various build problems. + For this reason, this API wil be removed with the introduction of project isolation. + When it happens, we will provide a migration guide. In the meantime, you can keep using this API + if you have to, but please don't rely on it if possible. If you don't want to document a certain project, + don't apply the Dokka plugin for it, or disable individual project tasks using the Gradle API . +""" + +@Suppress("DEPRECATION") +@DisableCachingByDefault(because = "Abstract super-class, not to be instantiated directly") +abstract class AbstractDokkaParentTask : AbstractDokkaTask() { + + @get:Internal + internal var childDokkaTaskPaths: Set<String> = emptySet() + private set + + @get:Nested + internal val childDokkaTasks: Set<AbstractDokkaTask> + get() = childDokkaTaskPaths + .mapNotNull { path -> project.tasks.findByPath(path) } + .map(::checkIsAbstractDokkaTask) + .toSet() + + /* By task reference */ + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun addChildTask(task: AbstractDokkaTask) { + childDokkaTaskPaths = childDokkaTaskPaths + task.path + } + + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun removeChildTask(task: AbstractDokkaTask) { + childDokkaTaskPaths = childDokkaTaskPaths - task.path + } + + /* By path */ + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun addChildTask(path: String) { + childDokkaTaskPaths = childDokkaTaskPaths + project.absoluteProjectPath(path) + } + + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun removeChildTask(path: String) { + childDokkaTaskPaths = childDokkaTaskPaths - project.absoluteProjectPath(path) + } + + /* By project reference and name */ + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun addChildTasks(projects: Iterable<Project>, childTasksName: String) { + projects.forEach { project -> + addChildTask(project.absoluteProjectPath(childTasksName)) + } + } + + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun removeChildTasks(projects: Iterable<Project>, childTasksName: String) { + projects.forEach { project -> + removeChildTask(project.absoluteProjectPath(childTasksName)) + } + } + + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun addSubprojectChildTasks(childTasksName: String) { + addChildTasks(project.subprojects, childTasksName) + } + + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun removeSubprojectChildTasks(childTasksName: String) { + removeChildTasks(project.subprojects, childTasksName) + } + + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun removeChildTasks(project: Project) { + childDokkaTaskPaths = childDokkaTaskPaths.filter { path -> + parsePath(path).parent != parsePath(project.path) + }.toSet() + } + + @Deprecated(message = DEPRECATION_MESSAGE, level = DeprecationLevel.WARNING) + fun removeChildTasks(projects: Iterable<Project>) { + projects.forEach { project -> removeChildTasks(project) } + } + + private fun checkIsAbstractDokkaTask(task: Task): AbstractDokkaTask { + if (task is AbstractDokkaTask) { + return task + } + throw IllegalArgumentException( + "Only tasks of type ${AbstractDokkaTask::class.java.name} can be added as child for " + + "${AbstractDokkaParentTask::class.java.name} tasks.\n" + + "Found task ${task.path} of type ${task::class.java.name} added to $path" + ) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaTask.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaTask.kt new file mode 100644 index 00000000..169ca050 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/AbstractDokkaTask.kt @@ -0,0 +1,257 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import groovy.lang.Closure +import org.gradle.api.Action +import org.gradle.api.DefaultTask +import org.gradle.api.Task +import org.gradle.api.artifacts.Configuration +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.plugins.JavaBasePlugin +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.mapProperty +import org.gradle.kotlin.dsl.property +import org.gradle.work.DisableCachingByDefault +import org.jetbrains.dokka.* +import org.jetbrains.dokka.plugability.ConfigurableBlock +import org.jetbrains.dokka.plugability.DokkaPlugin +import java.util.concurrent.atomic.AtomicReference +import java.util.function.BiConsumer +import kotlin.reflect.full.createInstance +import kotlin.reflect.full.memberFunctions + +@DisableCachingByDefault(because = "Abstract super-class, not to be instantiated directly") +abstract class AbstractDokkaTask : DefaultTask() { + + /** + * Display name used to refer to the module. Used for ToC, navigation, logging, etc. + * + * If set for a single-project build or a MultiModule task, will be used as project name. + * + * Default is Gradle project name. + */ + @Input + val moduleName: Property<String> = project.objects.property<String>() + .convention(project.name) + + /** + * Module version. + * + * If set for a single-project build or a MultiModule task, will be used + * as project version by the versioning plugin. + * + * Default is Gradle project version. + */ + @Input + val moduleVersion: Property<String> = project.objects.property<String>() + .convention(project.provider { project.version.toString() }) + + /** + * Directory to which documentation will be generated, regardless of format. + * Can be set on per-task basis. + * + * Default is `project/buildDir/taskName.removePrefix("dokka").decapitalize()`, so + * for `dokkaHtmlMultiModule` task it will be `project/buildDir/htmlMultiModule` + */ + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + /** + * Configuration for Dokka plugins. This property is not expected to be used directly - if possible, use + * [pluginConfiguration] blocks (preferred) or [pluginsMapConfiguration] instead. + */ + @Input + val pluginsConfiguration: ListProperty<in DokkaConfiguration.PluginConfiguration> = project.objects.listProperty() + + /** + * JSON configuration of Dokka plugins. + * + * Key is fully qualified Dokka plugin name, value is its configuration in JSON. + * + * Example: + * + * ```kotlin + * tasks.dokkaHtml { + * val dokkaBaseConfiguration = """ + * { + * "customAssets": ["${file("assets/my-image.png")}"], + * "customStyleSheets": ["${file("assets/my-styles.css")}"], + * "footerMessage": "(c) 2022 MyOrg" + * } + * """ + * pluginsMapConfiguration.set( + * mapOf("org.jetbrains.dokka.base.DokkaBase" to dokkaBaseConfiguration) + * ) + * } + * ``` + */ + @Input + val pluginsMapConfiguration: MapProperty<String, String> = project.objects.mapProperty() + + /** + * Whether to suppress obvious functions. + * + * A function is considered to be obvious if it is: + * - Inherited from `kotlin.Any`, `Kotlin.Enum`, `java.lang.Object` or `java.lang.Enum`, + * such as `equals`, `hashCode`, `toString`. + * - Synthetic (generated by the compiler) and does not have any documentation, such as + * `dataClass.componentN` or `dataClass.copy`. + * + * Default is `true` + */ + @Input + val suppressObviousFunctions: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.suppressObviousFunctions) + + /** + * Whether to suppress inherited members that aren't explicitly overridden in a given class. + * + * Note: this can suppress functions such as `equals`/`hashCode`/`toString`, but cannot suppress + * synthetic functions such as `dataClass.componentN` and `dataClass.copy`. Use [suppressObviousFunctions] + * for that. + * + * Default is `false`. + */ + @Input + val suppressInheritedMembers: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.suppressInheritedMembers) + + /** + * Whether to resolve remote files/links over network. + * + * This includes package-lists used for generating external documentation links: + * for instance, to make classes from standard library clickable. + * + * Setting this to `true` can significantly speed up build times in certain cases, + * but can also worsen documentation quality and user experience, for instance by + * not resolving some dependency's class/member links. + * + * When using offline mode, you can cache fetched files locally and provide them to + * Dokka as local paths. For instance, see [GradleExternalDocumentationLinkBuilder]. + * + * Default is `false`. + */ + @Input + val offlineMode: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.offlineMode) + + /** + * Whether to fail documentation generation if Dokka has emitted a warning or an error. + * Will wait until all errors and warnings have been emitted first. + * + * This setting works well with [GradleDokkaSourceSetBuilder.reportUndocumented] + * + * Default is `false`. + */ + @Input + val failOnWarning: Property<Boolean> = project.objects.property<Boolean>() + .convention(DokkaDefaults.failOnWarning) + + @get:Optional + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val cacheRoot: DirectoryProperty + + /** + * Type-safe configuration for a Dokka plugin. + * + * Note: this is available in Kotlin DSL only, if Dokka Gradle plugin was applied through `plugins` block + * and the configured plugin can be found on classpath, which may require adding a classpath dependency + * to `buildscript` block in case of external plugins. Some Dokka plugins, such as + * [org.jetbrains.dokka.base.DokkaBase], are on classpath by default. + * + * Example: + * + * ```kotlin + * import org.jetbrains.dokka.base.DokkaBase + * import org.jetbrains.dokka.base.DokkaBaseConfiguration + * + * tasks.dokkaHtml { + * pluginConfiguration<DokkaBase, DokkaBaseConfiguration> { + * footerMessage = "Test" + * } + * } + * ``` + * + * @param P Plugin class that extends [DokkaPlugin] + * @param T Plugin configuration class that extends [ConfigurableBlock] + */ + inline fun <reified P : DokkaPlugin, reified T : ConfigurableBlock> pluginConfiguration(block: T.() -> Unit) { + val instance = T::class.createInstance().apply(block) + val pluginConfiguration = PluginConfigurationImpl( + fqPluginName = P::class.qualifiedName!!, + serializationFormat = DokkaConfiguration.SerializationFormat.JSON, + values = instance.toCompactJsonString() + ) + pluginsConfiguration.add(pluginConfiguration) + } + + @Classpath + val plugins: Configuration = project.maybeCreateDokkaPluginConfiguration(name) + + @Classpath + val runtime: Configuration = project.maybeCreateDokkaRuntimeConfiguration(name) + + final override fun doFirst(action: Action<in Task>): Task = super.doFirst(action) + + final override fun doFirst(action: Closure<*>): Task = super.doFirst(action) + + @TaskAction + internal open fun generateDocumentation() { + DokkaBootstrap(runtime, DokkaBootstrapImpl::class).apply { + configure(buildDokkaConfiguration().toCompactJsonString(), createProxyLogger()) + val uncaughtExceptionHolder = AtomicReference<Throwable?>() + /** + * Run in a new thread to avoid memory leaks that are related to ThreadLocal (that keeps `URLCLassLoader`) + * Currently, all `ThreadLocal`s leaking are in the compiler/IDE codebase. + */ + Thread { generate() }.apply { + setUncaughtExceptionHandler { _, throwable -> uncaughtExceptionHolder.set(throwable) } + start() + join() + } + uncaughtExceptionHolder.get()?.let { throw it } + } + } + + internal abstract fun buildDokkaConfiguration(): DokkaConfigurationImpl + + private fun createProxyLogger(): BiConsumer<String, String> = BiConsumer { level, message -> + when (level) { + "debug" -> logger.debug(message) + "info" -> logger.info(message) + "progress" -> logger.lifecycle(message) + "warn" -> logger.warn(message) + "error" -> logger.error(message) + } + } + + init { + group = JavaBasePlugin.DOCUMENTATION_GROUP + // notCompatibleWithConfigurationCache was introduced in Gradle 7.4 + val containsNotCompatibleWithConfigurationCache = this::class.memberFunctions.any { it.name == "notCompatibleWithConfigurationCache" && it.parameters.firstOrNull()?.name == "reason" } + if (containsNotCompatibleWithConfigurationCache) { + super.notCompatibleWithConfigurationCache("Dokka tasks are not yet compatible with the Gradle configuration cache. See https://github.com/Kotlin/dokka/issues/1217") + } + } + + internal fun buildPluginsConfiguration(): List<PluginConfigurationImpl> { + val manuallyConfigured = pluginsMapConfiguration.get().entries.map { entry -> + PluginConfigurationImpl( + entry.key, + DokkaConfiguration.SerializationFormat.JSON, + entry.value + ) + } + return pluginsConfiguration.get().mapNotNull { it as? PluginConfigurationImpl } + manuallyConfigured + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaCollectorTask.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaCollectorTask.kt new file mode 100644 index 00000000..a45eec33 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaCollectorTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.api.tasks.CacheableTask +import org.jetbrains.dokka.DokkaConfigurationImpl + +@CacheableTask +abstract class DokkaCollectorTask : AbstractDokkaParentTask() { + + override fun generateDocumentation() { + checkChildDokkaTasksIsNotEmpty() + super.generateDocumentation() + } + + override fun buildDokkaConfiguration(): DokkaConfigurationImpl { + val initialDokkaConfiguration = DokkaConfigurationImpl( + moduleName = moduleName.get(), + outputDir = outputDirectory.asFile.get(), + cacheRoot = cacheRoot.asFile.orNull, + failOnWarning = failOnWarning.get(), + offlineMode = offlineMode.get(), + pluginsClasspath = plugins.resolve().toList(), + pluginsConfiguration = buildPluginsConfiguration(), + suppressObviousFunctions = suppressObviousFunctions.get(), + suppressInheritedMembers = suppressInheritedMembers.get(), + ) + + val subprojectDokkaConfigurations = childDokkaTasks.map { dokkaTask -> dokkaTask.buildDokkaConfiguration() } + return subprojectDokkaConfigurations.fold(initialDokkaConfiguration) { acc, it: DokkaConfigurationImpl -> + acc.copy( + sourceSets = acc.sourceSets + it.sourceSets, + pluginsClasspath = acc.pluginsClasspath + it.pluginsClasspath + ) + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaMultiModuleTask.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaMultiModuleTask.kt new file mode 100644 index 00000000..2893704a --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaMultiModuleTask.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.internal.tasks.TaskDependencyInternal +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.property +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.DokkaModuleDescriptionImpl +import java.io.File + +@Suppress("unused") // Shall provide source compatibility if possible +@Deprecated("Use 'DokkaMultiModuleTask' instead", ReplaceWith("DokkaMultiModuleTask"), DeprecationLevel.ERROR) +typealias DokkaMultimoduleTask = DokkaMultiModuleTask + +private typealias TaskPath = String + +@CacheableTask +abstract class DokkaMultiModuleTask : AbstractDokkaParentTask() { + + /** + * List of Markdown files that contain + * [module and package documentation](https://kotlinlang.org/docs/dokka-module-and-package-docs.html). + * + * Contents of specified files will be parsed and embedded into documentation as module and package descriptions. + * + * Example of such a file: + * + * ```markdown + * # Module kotlin-demo + * + * The module shows the Dokka usage. + * + * # Package org.jetbrains.kotlin.demo + * + * Contains assorted useful stuff. + * + * ## Level 2 heading + * + * Text after this heading is also part of documentation for `org.jetbrains.kotlin.demo` + * + * # Package org.jetbrains.kotlin.demo2 + * + * Useful stuff in another package. + * ``` + */ + @get:InputFiles + @get:Optional + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val includes: ConfigurableFileCollection + + @Internal + val fileLayout: Property<DokkaMultiModuleFileLayout> = project.objects.property<DokkaMultiModuleFileLayout>() + .convention(DokkaMultiModuleFileLayout.CompactInParent) + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + internal abstract val sourceChildOutputDirectories: ConfigurableFileCollection + + @get:OutputDirectories + internal val targetChildOutputDirectories: Provider<Iterable<Directory>> = project.provider { + childDokkaTasks.map { task -> targetChildOutputDirectory(task).get() } + } + + @get:Input + internal val childDokkaTaskIncludes: Map<TaskPath, Set<File>> + get() = childDokkaTasks.filterIsInstance<DokkaTaskPartial>().associate { task -> + task.path to task.dokkaSourceSets.flatMap { it.includes }.toSet() + } + + // The method contains a reference to internal Gradle API that is nice not to use. + // There was an attempt to get rid of it, but it was not successful + // See: https://github.com/Kotlin/dokka/pull/2835 + @Internal + override fun getTaskDependencies(): TaskDependencyInternal = + super.getTaskDependencies() + childDokkaTasks + + + override fun generateDocumentation() { + checkChildDokkaTasksIsNotEmpty() + super.generateDocumentation() + } + + override fun buildDokkaConfiguration(): DokkaConfigurationImpl { + return DokkaConfigurationImpl( + moduleName = moduleName.get(), + moduleVersion = moduleVersion.getValidVersionOrNull(), + outputDir = outputDirectory.asFile.get(), + cacheRoot = cacheRoot.asFile.orNull, + pluginsConfiguration = buildPluginsConfiguration(), + failOnWarning = failOnWarning.get(), + offlineMode = offlineMode.get(), + pluginsClasspath = plugins.resolve().toList(), + modules = childDokkaTasks.map { dokkaTask -> + DokkaModuleDescriptionImpl( + name = dokkaTask.moduleName.get(), + relativePathToOutputDirectory = targetChildOutputDirectory(dokkaTask).get().asFile.relativeTo( + outputDirectory.asFile.get() + ), + includes = childDokkaTaskIncludes[dokkaTask.path].orEmpty(), + sourceOutputDirectory = dokkaTask.outputDirectory.asFile.get(), + ) + }, + includes = includes.toSet(), + ) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTask.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTask.kt new file mode 100644 index 00000000..551ab62e --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTask.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.api.tasks.* +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.build + +@CacheableTask +abstract class DokkaTask : AbstractDokkaLeafTask() { + override fun buildDokkaConfiguration(): DokkaConfigurationImpl = + DokkaConfigurationImpl( + moduleName = moduleName.get(), + moduleVersion = moduleVersion.getValidVersionOrNull(), + outputDir = outputDirectory.asFile.get(), + cacheRoot = cacheRoot.asFile.orNull, + offlineMode = offlineMode.get(), + failOnWarning = failOnWarning.get(), + sourceSets = unsuppressedSourceSets.build(), + pluginsConfiguration = buildPluginsConfiguration(), + pluginsClasspath = plugins.resolve().toList(), + suppressObviousFunctions = suppressObviousFunctions.get(), + suppressInheritedMembers = suppressInheritedMembers.get(), + ) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTaskPartial.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTaskPartial.kt new file mode 100644 index 00000000..ae7d2066 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTaskPartial.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.api.tasks.* +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.build + +@CacheableTask +abstract class DokkaTaskPartial : AbstractDokkaLeafTask() { + + override fun buildDokkaConfiguration(): DokkaConfigurationImpl { + return DokkaConfigurationImpl( + moduleName = moduleName.get(), + moduleVersion = moduleVersion.orNull, + outputDir = outputDirectory.asFile.get(), + cacheRoot = cacheRoot.asFile.orNull, + offlineMode = offlineMode.get(), + failOnWarning = failOnWarning.get(), + sourceSets = unsuppressedSourceSets.build(), + pluginsConfiguration = buildPluginsConfiguration(), + pluginsClasspath = plugins.resolve().toList(), + delayTemplateSubstitution = true, + suppressObviousFunctions = suppressObviousFunctions.get(), + suppressInheritedMembers = suppressInheritedMembers.get(), + ) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/utils.kt b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/utils.kt new file mode 100644 index 00000000..c17653aa --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/main/kotlin/org/jetbrains/dokka/gradle/utils.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.Project +import org.gradle.api.UnknownDomainObjectException +import org.gradle.util.Path +import org.gradle.kotlin.dsl.findByType +import org.gradle.kotlin.dsl.getByType +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget + + +/** Parse a Gradle path, e.g. `:project:subproject:taskName` */ +internal fun parsePath(path: String): Path = Path.path(path) + +internal val Project.kotlinOrNull: KotlinProjectExtension? + get() = try { + project.extensions.findByType() + } catch (e: Throwable) { + when (e) { + // if the user project doesn't have KGP applied, we won't be able to load the class; + // TypeNotPresentException is possible if it's loaded through reified generics. + is NoClassDefFoundError, is TypeNotPresentException, is ClassNotFoundException -> null + else -> throw e + } + } + +internal val Project.kotlin: KotlinProjectExtension + get() = project.extensions.getByType() + +internal fun Project.isAndroidProject() = try { + project.extensions.getByName("android") + true +} catch (e: UnknownDomainObjectException) { + false +} catch (e: ClassNotFoundException) { + false +} + +internal fun KotlinTarget.isAndroidTarget() = this.platformType == KotlinPlatformType.androidJvm + +internal fun <T : Any> NamedDomainObjectContainer<T>.maybeCreate(name: String, configuration: T.() -> Unit): T { + return findByName(name) ?: create(name, configuration) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AbstractDokkaParentTaskTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AbstractDokkaParentTaskTest.kt new file mode 100644 index 00000000..02b7a0f9 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AbstractDokkaParentTaskTest.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("DEPRECATION") + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.getByName +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.gradle.utils.subprojects_ +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class AbstractDokkaParentTaskTest { + + private val rootProject = ProjectBuilder.builder().build() + private val subproject0 = ProjectBuilder.builder().withName("subproject0").withParent(rootProject).build() + private val subproject1 = ProjectBuilder.builder().withName("subproject1").withParent(rootProject).build() + private val subSubproject0 = ProjectBuilder.builder().withName("subSubproject0").withParent(subproject0).build() + + init { + rootProject.subprojects_ { + tasks.create<DokkaTask>("dokkaTask") + } + } + + private val parentTask = rootProject.tasks.create<TestDokkaParentTask>("parent") + + + @Test + fun `add and remove tasks by reference`() { + assertEquals( + emptySet(), parentTask.childDokkaTasks, + "Expected no childDokkaTasks by default" + ) + + parentTask.addChildTask(subproject0.dokkaTask) + assertEquals( + setOf(subproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subproject0.dokkaTask.path} being registered as child task" + ) + + parentTask.addChildTask(subproject1.dokkaTask) + assertEquals( + setOf(subproject0.dokkaTask, subproject1.dokkaTask), parentTask.childDokkaTasks, + "Expected both dokka tasks being present" + ) + + parentTask.removeChildTask(subproject0.dokkaTask) + assertEquals( + setOf(subproject1.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subproject0.dokkaTask.path} being removed from child tasks" + ) + + parentTask.addChildTask(subSubproject0.dokkaTask) + assertEquals( + setOf(subproject1.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subSubproject0.dokkaTask.path} being added as child task" + ) + + parentTask.addChildTask(subSubproject0.dokkaTask) + assertEquals( + setOf(subproject1.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected no effect for adding a task twice" + ) + } + + @Test + fun `add and remove by absolute path`() { + parentTask.addChildTask(":subproject0:dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subproject0.dokkaTask.path} as child task" + ) + + parentTask.addChildTask(":subproject0:subSubproject0:dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subSubproject0.dokkaTask.path} being added as child task" + ) + + parentTask.removeChildTask(":subproject0:dokkaTask") + assertEquals( + setOf(subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subproject0.dokkaTask.path} being removed as child task" + ) + } + + @Test + fun `add and remove by relative path`() { + parentTask.addChildTask("subproject0:dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subproject0.dokkaTask.path} as child task" + ) + + parentTask.addChildTask("subproject0:subSubproject0:dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subSubproject0.dokkaTask.path} being added as child task" + ) + + parentTask.removeChildTask("subproject0:dokkaTask") + assertEquals( + setOf(subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subproject0.dokkaTask.path} being removed as child task" + ) + } + + @Test + fun `add and remove by relative path ob subproject0`() { + val parentTask = subproject0.tasks.create<TestDokkaParentTask>("parent") + + parentTask.addChildTask("subSubproject0:dokkaTask") + assertEquals( + setOf(subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subSubproject0.dokkaTask.path} being registered as child" + ) + + parentTask.removeChildTask("subSubproject0:dokkaTask") + assertEquals( + emptySet(), parentTask.childDokkaTasks, + "Expected ${subSubproject0.dokkaTask.path} being removed as child" + ) + } + + @Test + fun `add and remove by project and name`() { + parentTask.addChildTasks(rootProject.subprojects, "dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask, subproject1.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected all subproject tasks being registered as child task" + ) + + parentTask.removeChildTasks(rootProject.subprojects, "dokkaTask") + assertEquals( + emptySet(), parentTask.childDokkaTasks, + "Expected all tasks being removed" + ) + + parentTask.addChildTasks(listOf(subproject0), "dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected only ${subproject0.dokkaTask.path} being registered as child" + ) + + parentTask.addSubprojectChildTasks("dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask, subproject1.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected all subproject tasks being registered as child task" + ) + + parentTask.removeSubprojectChildTasks("dokkaTask") + assertEquals( + emptySet(), parentTask.childDokkaTasks, + "Expected all tasks being removed" + ) + + parentTask.addSubprojectChildTasks("dokkaTask") + assertEquals( + setOf(subproject0.dokkaTask, subproject1.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected all subproject tasks being registered as child task" + ) + + parentTask.removeChildTasks(subproject0) + assertEquals( + setOf(subproject1.dokkaTask, subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected only ${subproject0.dokkaTask.path} being removed" + ) + + parentTask.addSubprojectChildTasks("dokkaTask") + parentTask.removeChildTasks(listOf(subproject0, subproject1)) + assertEquals( + setOf(subSubproject0.dokkaTask), parentTask.childDokkaTasks, + "Expected ${subproject0.dokkaTask.path} and ${subproject1.dokkaTask.path} being removed" + ) + } + + @Test + fun `adding invalid path will not throw exception`() { + parentTask.addChildTask(":some:stupid:path") + parentTask.childDokkaTasks + } + + @Test + fun `adding non dokka task will throw exception`() { + val badTask = rootProject.tasks.create("badTask") + parentTask.addChildTask(badTask.path) + assertFailsWith<IllegalArgumentException> { parentTask.childDokkaTasks } + } +} + +internal abstract class TestDokkaParentTask : AbstractDokkaParentTask() { + override fun buildDokkaConfiguration(): DokkaConfigurationImpl { + throw NotImplementedError() + } +} + +private val Project.dokkaTask: DokkaTask get() = tasks.getByName<DokkaTask>("dokkaTask") diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AndroidAutoConfigurationTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AndroidAutoConfigurationTest.kt new file mode 100644 index 00000000..eb772df9 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AndroidAutoConfigurationTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import com.android.build.gradle.LibraryExtension +import org.gradle.api.artifacts.ResolveException +import org.gradle.api.internal.project.ProjectInternal +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import kotlin.test.* + +class AndroidAutoConfigurationTest { + + private val project = ProjectBuilder.builder().build().also { project -> + project.plugins.apply("com.android.library") + project.plugins.apply("org.jetbrains.kotlin.android") + project.plugins.apply("org.jetbrains.dokka") + project.extensions.configure<LibraryExtension> { + compileSdkVersion(28) + } + } + + @Test + fun `at least one dokka task created`() { + val dokkaTasks = project.tasks.withType<DokkaTask>().toList() + assertTrue(dokkaTasks.isNotEmpty(), "Expected at least one dokka task") + } + + @Test + fun `all default source sets are present in dokka`() { + val dokkaTasks = project.tasks.withType<DokkaTask>().toList() + dokkaTasks.forEach { task -> + val sourceSets = task.dokkaSourceSets.toList() + assertEquals( + listOf( + "androidTest", "androidTestDebug", "debug", "main", + "release", "test", "testDebug", "testRelease", "androidTestRelease" + ).sorted(), + sourceSets.map { it.name }.sorted(), + "Expected all default source sets being registered" + ) + } + } + + @Ignore // TODO: find where `maven` plugin is used, which was removed in Gradle 8 + @Test + fun `test source sets are suppressed`() { + val dokkaTasks = project.tasks.withType<DokkaTask>().toList() + project as ProjectInternal + project.evaluate() + dokkaTasks.flatMap { it.dokkaSourceSets }.forEach { sourceSet -> + if ("test" in sourceSet.name.toLowerCase()) { + assertTrue( + sourceSet.suppress.get(), + "Expected source set `${sourceSet.name}` to be suppressed by default" + ) + } else { + assertFalse( + sourceSet.suppress.get(), + "Expected source set `${sourceSet.name}`to not be suppressed by default" + ) + } + } + } + + @Ignore // TODO: find where `maven` plugin is used, which was removed in Gradle 8 + @Test + fun `source sets have non-empty classpath`() { + val dokkaTasks = project.tasks.withType<DokkaTask>().toList() + project as ProjectInternal + project.evaluate() + + dokkaTasks.flatMap { it.dokkaSourceSets } + .filterNot { it.name == "androidTestRelease" && it.suppress.get() } // androidTestRelease has empty classpath, but it makes no sense for suppressed source set + .forEach { sourceSet -> + /* + + There is no better way of checking for empty classpath at the moment (without resolving dependencies). + We assume, that an empty classpath can be resolved + We assume, that a non-empty classpath will not be able to resolve (no repositories defined) + */ + assertFailsWith<ResolveException>("SourceSet: " + sourceSet.name) { sourceSet.classpath.files } + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AutomagicProxyTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AutomagicProxyTest.kt new file mode 100644 index 00000000..c8f58f27 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/AutomagicProxyTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.jetbrains.dokka.DokkaBootstrap +import org.jetbrains.dokka.gradle.AutomagicProxyTest.TestInterface +import java.util.function.BiConsumer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + + +class AutomagicProxyTest { + + private class TestException(message: String, cause: Throwable?) : Exception(message, cause) + + private fun interface TestInterface { + @Throws(Throwable::class) + operator fun invoke(): Int + } + + @Test + fun `simple method invocation`() { + val instance = TestInterface { 0 } + val proxy = automagicTypedProxy<TestInterface>(instance.javaClass.classLoader, instance) + assertEquals(0, proxy()) + } + + @Test + fun `exception throw in DokkaBootstrap is not wrapped inside UndeclaredThrowableException`() { + val instanceThrowingTestException = object : DokkaBootstrap { + override fun configure(serializedConfigurationJSON: String, logger: BiConsumer<String, String>) = Unit + override fun generate() { + throw TestException("Test Exception Message", Exception("Cause Exception Message")) + } + } + + val proxy = automagicTypedProxy<DokkaBootstrap>( + instanceThrowingTestException.javaClass.classLoader, + instanceThrowingTestException + ) + + val exception = assertFailsWith<TestException> { + proxy.generate() + } + + assertEquals("Test Exception Message", exception.message) + assertEquals("Cause Exception Message", exception.cause?.message) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/CheckSourceSetDependenciesTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/CheckSourceSetDependenciesTest.kt new file mode 100644 index 00000000..92adc0e5 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/CheckSourceSetDependenciesTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.testfixtures.ProjectBuilder +import java.lang.IllegalArgumentException +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class CheckSourceSetDependenciesTest { + + private val project = ProjectBuilder.builder().build() + + @Test + fun `passes when properly configured`() { + val sourceSets = listOf( + GradleDokkaSourceSetBuilder("common", project), + GradleDokkaSourceSetBuilder("jvmAndJsCommon", project).apply { + dependsOn("common") + }, + GradleDokkaSourceSetBuilder("jvm", project).apply { + dependsOn("jvmAndJsCommon") + }, + GradleDokkaSourceSetBuilder("js", project).apply { + dependsOn("jvmAndJsCommon") + } + ) + checkSourceSetDependencies(sourceSets) + } + + @Test + fun `throws exception when dependent source set id cant be found`() { + val sourceSets = listOf( + GradleDokkaSourceSetBuilder("main", project), + GradleDokkaSourceSetBuilder("bad", project).apply { + dependsOn("missing") + } + ) + + val exception = assertFailsWith<IllegalArgumentException> { + checkSourceSetDependencies(sourceSets) + } + + assertTrue("bad" in exception.message.orEmpty(), "Expected name of source set mentioned") + assertTrue("missing" in exception.message.orEmpty(), "Expected name of missing source set mentioned") + } + + @Test + fun `throws exception when documented source set depends on suppressed source set`() { + val sourceSets = listOf( + GradleDokkaSourceSetBuilder("common", project), + GradleDokkaSourceSetBuilder("intermediate", project).apply { + dependsOn("common") + suppress.set(true) + }, + GradleDokkaSourceSetBuilder("jvm", project).apply { + dependsOn("intermediate") + } + ) + + val exception = assertFailsWith<IllegalArgumentException> { + checkSourceSetDependencies(sourceSets) + } + + assertTrue("intermediate" in exception.message.orEmpty()) + assertTrue("jvm" in exception.message.orEmpty()) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/ConfigureWithKotlinSourceSetGistTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/ConfigureWithKotlinSourceSetGistTest.kt new file mode 100644 index 00000000..55acbf2f --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/ConfigureWithKotlinSourceSetGistTest.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.artifacts.FileCollectionDependency +import org.gradle.kotlin.dsl.get +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.gradle.kotlin.KotlinSourceSetGist +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import kotlin.test.Test +import kotlin.test.assertEquals +import org.jetbrains.dokka.gradle.utils.withDependencies_ +import kotlin.test.assertTrue + +class ConfigureWithKotlinSourceSetGistTest { + @Test + fun `example gist`() { + val project = ProjectBuilder.builder().build() + + val f1Jar = project.file("f1.jar") + val f2Jar = project.file("f2.jar") + assertTrue(f1Jar.createNewFile()) + assertTrue(f2Jar.createNewFile()) + + val customSourceRoot = project.file("customSourceRoot") + assertTrue(customSourceRoot.mkdirs()) + + val gist = KotlinSourceSetGist( + name = "customName", + platform = project.provider { KotlinPlatformType.common }, + isMain = project.provider { true }, + classpath = project.provider { project.files(f1Jar, f2Jar) }, + sourceRoots = project.files(customSourceRoot), + dependentSourceSetNames = project.provider { setOf("customRootSourceSet") } + ) + + val sourceSet = GradleDokkaSourceSetBuilder("", project) + sourceSet.configureWithKotlinSourceSetGist(gist) + + assertEquals( + "common", sourceSet.build().displayName, + "Expected platform being used as default displayName for source set" + ) + + assertEquals( + Platform.common, sourceSet.build().analysisPlatform, + "Expected common platform being set" + ) + + assertEquals( + listOf(f1Jar, f2Jar), sourceSet.build().classpath, + "Expected classpath being present" + ) + + assertEquals( + setOf(sourceSet.DokkaSourceSetID("customRootSourceSet")), sourceSet.build().dependentSourceSets, + "Expected customRootSourceSet being present in dependentSourceSets after build" + ) + + assertEquals( + setOf(customSourceRoot), sourceSet.build().sourceRoots, + "Expected customSourceRoot being present in sourceRoots after build" + ) + } + + @Test + fun `display name for source set customMain`() { + val project = ProjectBuilder.builder().build() + + val gist = KotlinSourceSetGist( + name = "customMain", + platform = project.provider { KotlinPlatformType.common }, + isMain = project.provider { true }, + classpath = project.provider { project.files() }, + sourceRoots = project.files(), + dependentSourceSetNames = project.provider { emptySet() } + ) + + val sourceSet = GradleDokkaSourceSetBuilder("", project) + sourceSet.configureWithKotlinSourceSetGist(gist) + + assertEquals( + "custom", sourceSet.build().displayName, + "Expected 'Main' being trimmed from source set name and used as display name" + ) + } + + @Suppress("UnstableApiUsage") + @Test + fun `configuration with kotlin source set is live`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.kotlin.jvm") + val kotlin = project.kotlin as KotlinJvmProjectExtension + val mainSourceSet = kotlin.sourceSets["main"] + + /* Make sure that the source roots exist on filesystem */ + mainSourceSet.kotlin.sourceDirectories.elements.get().map { it.asFile }.forEach { it.mkdirs() } + + /* Make sure to remove dependencies that cannot be resolved during test */ + project.configurations.configureEach { + withDependencies_ { + removeIf { dependency -> dependency !is FileCollectionDependency } + } + } + + val dokkaSourceSet = GradleDokkaSourceSetBuilder("main", project) + dokkaSourceSet.kotlinSourceSet(mainSourceSet) + + assertEquals( + listOf(project.file("src/main/kotlin"), project.file("src/main/java")), + dokkaSourceSet.sourceRoots.elements.get().map { it.asFile }, + "Expected default source roots being present in dokkaSourceSet" + ) + + val customSourceRoot = project.file("src/main/customRoot") + assertTrue(customSourceRoot.mkdirs()) + mainSourceSet.kotlin.srcDir(customSourceRoot) + + assertEquals( + listOf(project.file("src/main/kotlin"), project.file("src/main/java"), project.file("src/main/customRoot")), + dokkaSourceSet.sourceRoots.elements.get().map { it.asFile }, + "Expected customRoot being added to source roots in dokkaSourceSet" + ) + } + + @Test + fun `changing classpath`() { + val project = ProjectBuilder.builder().build() + val dokkaSourceSet = GradleDokkaSourceSetBuilder("main", project) + var classpath = project.files() + + dokkaSourceSet.configureWithKotlinSourceSetGist( + KotlinSourceSetGist( + name = "gist", + platform = project.provider { KotlinPlatformType.common }, + isMain = project.provider { true }, + dependentSourceSetNames = project.provider { emptySet() }, + sourceRoots = project.files(), + classpath = project.provider { classpath } + ) + ) + + dokkaSourceSet.classpath.from("base.jar") + classpath.from("f1.jar") + classpath.from("f2.jar") + assertEquals( + setOf(project.file("f1.jar"), project.file("f2.jar"), project.file("base.jar")), + dokkaSourceSet.classpath.files, + "Expected files from initial gist classpath and manually added file base.jar to be present in classpath" + ) + + /* + Swapping the original file collection in favour of a new one. + We expect that the base.jar is still present, as it was configured on the dokka source set. + We also expect, that the new files from the new file collection are replacing old ones + */ + classpath = project.files("f3.jar", "f4.jar") + assertEquals( + setOf(project.file("f3.jar"), project.file("f4.jar"), project.file("base.jar")), + dokkaSourceSet.classpath.files, + "Expected files from changed gist classpath and manually added file base.jar to be present in classpath" + ) + } + + +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaConfigurationJsonTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaConfigurationJsonTest.kt new file mode 100644 index 00000000..c2a05eb5 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaConfigurationJsonTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.gradle.utils.create_ +import org.jetbrains.dokka.gradle.utils.externalDocumentationLink_ +import org.jetbrains.dokka.gradle.utils.withDependencies_ +import org.jetbrains.dokka.toCompactJsonString +import java.io.File +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class DokkaConfigurationJsonTest { + + @Test + fun `DokkaTask configuration toJsonString then parseJson`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + val dokkaTask = project.tasks.withType<DokkaTask>().first() + dokkaTask.plugins.withDependencies_ { clear() } + dokkaTask.apply { + this.failOnWarning.set(true) + this.offlineMode.set(true) + this.outputDirectory.set(File("customOutputDir")) + this.cacheRoot.set(File("customCacheRoot")) + this.pluginsConfiguration.add( + PluginConfigurationImpl( + "A", + DokkaConfiguration.SerializationFormat.JSON, + """ { "key" : "value1" } """ + ) + ) + this.pluginsConfiguration.add( + PluginConfigurationImpl( + "B", + DokkaConfiguration.SerializationFormat.JSON, + """ { "key" : "value2" } """ + ) + ) + this.dokkaSourceSets.create_("main") { + displayName.set("customSourceSetDisplayName") + reportUndocumented.set(true) + + externalDocumentationLink_ { + packageListUrl.set(URL("http://some.url")) + url.set(URL("http://some.other.url")) + } + perPackageOption { + includeNonPublic.set(true) + reportUndocumented.set(true) + skipDeprecated.set(true) + documentedVisibilities.set(setOf(DokkaConfiguration.Visibility.PRIVATE)) + } + } + } + + val sourceConfiguration = dokkaTask.buildDokkaConfiguration() + val configurationJson = sourceConfiguration.toCompactJsonString() + val parsedConfiguration = DokkaConfigurationImpl(configurationJson) + + assertEquals(sourceConfiguration, parsedConfiguration) + println(parsedConfiguration) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaConfigurationSerializableTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaConfigurationSerializableTest.kt new file mode 100644 index 00000000..02fd728b --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaConfigurationSerializableTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.PluginConfigurationImpl +import org.jetbrains.dokka.gradle.utils.create_ +import org.jetbrains.dokka.gradle.utils.externalDocumentationLink_ +import org.jetbrains.dokka.gradle.utils.withDependencies_ +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.net.URL +import kotlin.test.Test +import kotlin.test.assertEquals + +class DokkaConfigurationSerializableTest { + + @Test + fun `DokkaTask configuration write to file then parse`(@TempDir tempDirectory: File) { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + val dokkaTask = project.tasks.withType<DokkaTask>().first() + dokkaTask.plugins.withDependencies_ { clear() } + dokkaTask.apply { + this.failOnWarning.set(true) + this.offlineMode.set(true) + this.outputDirectory.set(File("customOutputDir")) + this.cacheRoot.set(File("customCacheRoot")) + this.pluginsConfiguration.add( + PluginConfigurationImpl( + "A", + DokkaConfiguration.SerializationFormat.JSON, + """ { "key" : "value1" } """ + ) + ) + this.pluginsConfiguration.add( + PluginConfigurationImpl( + "B", + DokkaConfiguration.SerializationFormat.JSON, + """ { "key" : "value2" } """ + ) + ) + this.dokkaSourceSets.create_("main") { + displayName.set("customSourceSetDisplayName") + reportUndocumented.set(true) + + externalDocumentationLink_ { + packageListUrl.set(URL("http://some.url")) + url.set(URL("http://some.other.url")) + } + + perPackageOption { + includeNonPublic.set(true) + reportUndocumented.set(true) + skipDeprecated.set(true) + documentedVisibilities.set(setOf(DokkaConfiguration.Visibility.PRIVATE)) + } + } + } + + val sourceConfiguration = dokkaTask.buildDokkaConfiguration() + val configurationFile = tempDirectory.resolve("config.bin") + ObjectOutputStream(configurationFile.outputStream()).use { stream -> + stream.writeObject(sourceConfiguration) + } + val parsedConfiguration = ObjectInputStream(configurationFile.inputStream()).use { stream -> + stream.readObject() as DokkaConfiguration + } + + assertEquals(sourceConfiguration, parsedConfiguration) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayoutTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayoutTest.kt new file mode 100644 index 00000000..8acd3547 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaMultiModuleFileLayoutTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.kotlin.dsl.create +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.DokkaException +import org.jetbrains.dokka.gradle.DokkaMultiModuleFileLayout.CompactInParent +import org.jetbrains.dokka.gradle.DokkaMultiModuleFileLayout.NoCopy +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class DokkaMultiModuleFileLayoutTest { + + @Test + fun `no copy`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + val child = project.tasks.create<DokkaTask>("child") + val parent = project.tasks.create<DokkaMultiModuleTask>("parent") + child.outputDirectory.set(File("some/path")) + + assertEquals( + File("some/path"), + NoCopy.targetChildOutputDirectory(parent, child).get().asFile.relativeTo(project.projectDir), + "Expected original file path returned" + ) + } + + @Test + fun `compact in parent`() { + val rootProject = ProjectBuilder.builder().build() + + val parentProject = ProjectBuilder.builder().withName("parent").withParent(rootProject).build() + parentProject.plugins.apply("org.jetbrains.dokka") + + val intermediateProject = ProjectBuilder.builder().withName("intermediate").withParent(parentProject).build() + val childProject = ProjectBuilder.builder().withName("child").withParent(intermediateProject).build() + childProject.plugins.apply("org.jetbrains.dokka") + + val parentTask = parentProject.tasks.create<DokkaMultiModuleTask>("parentTask") + val childTask = childProject.tasks.create<DokkaTask>("childTask") + + val targetOutputDirectory = CompactInParent.targetChildOutputDirectory(parentTask, childTask) + assertEquals( + parentTask.outputDirectory.get().asFile.resolve("intermediate/child"), + targetOutputDirectory.get().asFile, + "Expected nested file structure representing project structure" + ) + } + + @Test + fun copyChildOutputDirectory() { + /* Prepare */ + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + val childTask = project.tasks.create<DokkaTask>("child") + val parentTask = project.tasks.create<DokkaMultiModuleTask>("parent") + + val sourceOutputDirectory = childTask.outputDirectory.get().asFile + sourceOutputDirectory.mkdirs() + sourceOutputDirectory.resolve("some.file").writeText("some text") + val subFolder = sourceOutputDirectory.resolve("subFolder") + subFolder.mkdirs() + subFolder.resolve("other.file").writeText("other text") + + parentTask.fileLayout.set(DokkaMultiModuleFileLayout { parent, _ -> + parent.project.provider { parent.project.layout.projectDirectory.dir("target/output") } + }) + parentTask.copyChildOutputDirectory(childTask) + + /* Assertions */ + val targetOutputDirectory = project.file("target/output") + assertTrue( + targetOutputDirectory.exists() && targetOutputDirectory.isDirectory, + "Expected target output directory ${targetOutputDirectory.path} to exist" + ) + + val targetSomeFile = targetOutputDirectory.resolve("some.file") + assertTrue( + targetSomeFile.exists() && targetSomeFile.isFile, + "Expected sample file to exist in target output directory" + ) + + assertEquals( + "some text", targetSomeFile.readText(), + "Expected content to be written into sample file" + ) + + val targetSubFolder = targetOutputDirectory.resolve("subFolder") + assertTrue( + targetSubFolder.exists() && targetSubFolder.isDirectory, + "Expected sub folder being present in target output directory" + ) + + val targetOtherFile = targetSubFolder.resolve("other.file") + assertTrue( + targetOtherFile.exists() && targetOtherFile.isFile, + "Expected nested 'other.file' being copied into target" + ) + + assertEquals( + "other text", targetOtherFile.readText(), + "Expected content to be written into 'other.file'" + ) + } + + @Test + fun `copyChildOutputDirectory target output directory within itself throws DokkaException`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + val childTask = project.tasks.create<DokkaTask>("child") + val parentTask = project.tasks.create<DokkaMultiModuleTask>("parent") + parentTask.fileLayout.set(DokkaMultiModuleFileLayout { _, child -> + child.outputDirectory.dir("subfolder") + }) + assertFailsWith<DokkaException> { parentTask.copyChildOutputDirectory(childTask) } + } + + @Test + fun `copyChildOutputDirectory NoCopy`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + val childTask = project.tasks.create<DokkaTask>("child") + val parentTask = project.tasks.create<DokkaMultiModuleTask>("parent") + parentTask.fileLayout.set(NoCopy) + parentTask.copyChildOutputDirectory(childTask) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaPluginApplyTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaPluginApplyTest.kt new file mode 100644 index 00000000..d41ba672 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/DokkaPluginApplyTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.plugins.JavaBasePlugin +import org.gradle.api.tasks.TaskContainer +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class DokkaPluginApplyTest { + + @Test + fun `one task per format is registered`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + assertTrue( + project.tasks.findByName("dokkaHtml") is DokkaTask, + "Expected DokkaTask: dokkaHtml" + ) + + assertTrue( + project.tasks.findByName("dokkaGfm") is DokkaTask, + "Expected DokkaTask: dokkaGfm" + ) + + assertTrue( + project.tasks.findByName("dokkaJekyll") is DokkaTask, + "Expected DokkaTask: dokkaJekyll" + ) + + assertTrue( + project.tasks.findByName("dokkaJavadoc") is DokkaTask, + "Expected DokkaTask: dokkaJavadoc" + ) + } + + @Test + fun `dokka plugin configurations extend dokkaPlugin`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + val dokkaPluginsConfiguration = project.maybeCreateDokkaDefaultPluginConfiguration() + + project.tasks.withType<DokkaTask>().forEach { dokkaTask -> + assertSame( + dokkaTask.plugins.extendsFrom.single(), dokkaPluginsConfiguration, + "Expected dokka plugins configuration to extend default ${dokkaPluginsConfiguration.name} configuration" + ) + } + } + + @Test + fun `all dokka tasks are part of the documentation group`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + assertDokkaTasksHaveDocumentationGroup(project.tasks) + } + + @Test + fun `all dokka tasks are part of the documentation group in a multi module setup`() { + val root = ProjectBuilder.builder().withName("root").build() + val child = ProjectBuilder.builder().withName("child").withParent(root).build() + root.plugins.apply("org.jetbrains.dokka") + child.plugins.apply("org.jetbrains.dokka") + assertDokkaTasksHaveDocumentationGroup(root.tasks) + assertDokkaTasksHaveDocumentationGroup(child.tasks) + } + + @Test + fun `old dokka tasks are part of the deprecated group in a multi module setup`() { + val root = ProjectBuilder.builder().withName("root").build() + val child = ProjectBuilder.builder().withName("child").withParent(root).build() + root.plugins.apply("org.jetbrains.dokka") + child.plugins.apply("org.jetbrains.dokka") + assertOldDokkaTasksHaveDeprecatedGroup(root.tasks) + assertOldDokkaTasksHaveDeprecatedGroup(child.tasks) + } + + @Test + fun `all dokka tasks provide a task description`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + assertDokkaTasksHaveDescription(project.tasks) + } + + @Test + fun `all dokka tasks provide a task description in a multi module setup`() { + val root = ProjectBuilder.builder().withName("root").build() + val child = ProjectBuilder.builder().withName("child").withParent(root).build() + root.plugins.apply("org.jetbrains.dokka") + child.plugins.apply("org.jetbrains.dokka") + assertDokkaTasksHaveDescription(root.tasks) + assertDokkaTasksHaveDescription(child.tasks) + } + + @Test + fun `parent dokka tasks have children configured`() { + val root = ProjectBuilder.builder().withName("root").build() + val child = ProjectBuilder.builder().withName("child").withParent(root).build() + root.plugins.apply("org.jetbrains.dokka") + child.plugins.apply("org.jetbrains.dokka") + + val parentTasks = root.tasks.withType<AbstractDokkaParentTask>() + assertTrue(parentTasks.isNotEmpty(), "Expected at least one parent task being created") + + parentTasks.toList().forEach { parentTask -> + assertEquals(1, parentTask.childDokkaTasks.size, "Expected one child dokka task") + assertEquals( + child, parentTask.childDokkaTasks.single().project, + "Expected child dokka task from child project" + ) + } + } +} + +private fun assertDokkaTasksHaveDocumentationGroup(taskContainer: TaskContainer) { + taskContainer.withType<AbstractDokkaTask>().forEach { dokkaTask -> + assertEquals( + JavaBasePlugin.DOCUMENTATION_GROUP, + dokkaTask.group, + "Expected task: ${dokkaTask.path} group to be \"${JavaBasePlugin.DOCUMENTATION_GROUP}\"" + ) + } +} + +private fun assertOldDokkaTasksHaveDeprecatedGroup(taskContainer: TaskContainer) { + taskContainer.names.filter { "Multimodule" in it }.forEach { dokkaTaskName -> + val dokkaTask = taskContainer.getByName(dokkaTaskName) + val expectedGroup = "deprecated" + assertEquals( + expectedGroup, + dokkaTask.group, + "Expected task: ${dokkaTask.path} group to be \"${expectedGroup}\"" + ) + } +} + +private fun assertDokkaTasksHaveDescription(taskContainer: TaskContainer) { + taskContainer.withType<AbstractDokkaTask>().forEach { dokkaTask -> + assertTrue( + @Suppress("UselessCallOnNotNull") // Task.description is nullable, but not inherited as Kotlin sees it. + dokkaTask.description.orEmpty().isNotEmpty(), + "Expected description for task ${dokkaTask.name}" + ) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder.kt new file mode 100644 index 00000000..b973ba53 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilder.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.api.Project +import org.jetbrains.dokka.DokkaSourceSetID + +@Suppress("TestFunctionName") +fun GradleDokkaSourceSetBuilder(name: String, project: Project, sourceSetScopeId: String = "${project.path}:test"): + GradleDokkaSourceSetBuilder { + return GradleDokkaSourceSetBuilder(name, project) { DokkaSourceSetID(sourceSetScopeId, it) } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderTest.kt new file mode 100644 index 00000000..76f6f58e --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/GradleDokkaSourceSetBuilderTest.kt @@ -0,0 +1,494 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import com.android.build.gradle.internal.api.DefaultAndroidSourceSet +import org.gradle.api.Project +import org.gradle.kotlin.dsl.closureOf +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.* +import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSet +import java.net.URL +import kotlin.test.* + +class GradleDokkaSourceSetBuilderTest { + + private val project = ProjectBuilder.builder().withName("root").build() + + @Test + fun sourceSetId() { + val sourceSet = GradleDokkaSourceSetBuilder("myName", project, "scopeId") + assertEquals( + DokkaSourceSetID("scopeId", "myName"), sourceSet.sourceSetID, + "Expected sourceSet.sourceSetID to match output of DokkaSourceSetID factory function" + ) + + assertEquals( + "scopeId/myName", sourceSet.sourceSetID.toString(), + "Expected SourceSetId's string representation" + ) + } + + @Test + fun classpath() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + sourceSet.classpath.from(project.file("path/to/file.jar")) + sourceSet.classpath.from(project.file("path/to/other.jar")) + + assertEquals( + listOf(project.file("path/to/file.jar"), project.file("path/to/other.jar")), sourceSet.classpath.toList(), + "Expected both file paths being present in classpath" + ) + + assertEquals( + listOf(project.file("path/to/file.jar"), project.file("path/to/other.jar")), + sourceSet.build().classpath.toList(), + "Expected both file paths being present in built classpath" + ) + } + + @Test + fun displayName() { + val sourceSet = GradleDokkaSourceSetBuilder("myName", project) + assertNull( + sourceSet.displayName.orNull, + "Expected no ${GradleDokkaSourceSetBuilder::displayName.name} being set by default" + ) + + assertEquals( + "myName", sourceSet.build().displayName, + "Expected source set name being used for ${DokkaConfiguration.DokkaSourceSet::displayName.name} " + + "after building source set with no ${GradleDokkaSourceSetBuilder::displayName.name} being set" + ) + + sourceSet.displayName.set("displayName") + assertEquals( + "displayName", sourceSet.build().displayName, + "Expected previously set ${GradleDokkaSourceSetBuilder::displayName.name} to be present after build" + ) + } + + @Test + fun `displayName default for sourceSet ending with Main`() { + val sourceSet = GradleDokkaSourceSetBuilder("jvmMain", project) + assertEquals( + "jvm", sourceSet.build().displayName, + "Expected 'Main' being stripped for source set display name after build" + ) + } + + @Test + fun sourceRoots() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + sourceSet.sourceRoots.from(project.file("root1")) + sourceSet.sourceRoot(project.file("root2")) + sourceSet.sourceRoot(project.file("root3").absolutePath) + sourceSet.sourceRoot("root4") + + assertEquals( + listOf("root1", "root2", "root3", "root4").map(project::file).toSet(), + sourceSet.build().sourceRoots, + "Expected all files being present" + ) + + sourceSet.build().sourceRoots.forEach { root -> + assertTrue( + root.startsWith(project.projectDir), + "Expected all roots to be inside the projectDir\n" + + "projectDir: ${project.projectDir}\n" + + "root: ${root.absolutePath})" + ) + } + } + + @Test + fun dependentSourceSets() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals(emptySet(), sourceSet.build().dependentSourceSets, "Expected no dependent sourceSets by default") + + sourceSet.dependentSourceSets.add(sourceSet.DokkaSourceSetID("s1")) + sourceSet.dependsOn("s2") + sourceSet.dependsOn(sourceSet.DokkaSourceSetID("s3")) + sourceSet.dependsOn(GradleDokkaSourceSetBuilder("s4", project)) + sourceSet.dependsOn(GradleDokkaSourceSetBuilder("s5", project).build()) + sourceSet.dependsOn(createDefaultKotlinSourceSet("s6")) + sourceSet.dependsOn(DefaultAndroidSourceSet("s7", project, false)) + + assertEquals( + listOf(":/s1", ":/s2", ":/s3", ":/s4", ":/s5", ":/s6", ":/s7"), + sourceSet.build().dependentSourceSets.map { it.toString() }, + "Expected all source sets being registered" + ) + } + + private fun createDefaultKotlinSourceSet(displayName: String): DefaultKotlinSourceSet { + return project.objects.newInstance(DefaultKotlinSourceSet::class.java, project, displayName) + } + + @Test + fun samples() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals(emptySet(), sourceSet.build().samples, "Expected no default samples") + sourceSet.samples.from(project.file("s1")) + sourceSet.samples.from(project.file("s2")) + assertEquals( + setOf(project.file("s1"), project.file("s2")), sourceSet.build().samples, + "Expected all samples being present after build" + ) + } + + @Test + fun includes() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals(emptySet(), sourceSet.build().includes, "Expected no default includees") + sourceSet.includes.from(project.file("i1")) + sourceSet.includes.from(project.file("i2")) + assertEquals( + setOf(project.file("i1"), project.file("i2")), sourceSet.build().includes, + "Expected all includes being present after build" + ) + } + + @Test + @Suppress("DEPRECATION") + fun includeNonPublic() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals( + DokkaDefaults.includeNonPublic, sourceSet.build().includeNonPublic, + "Expected default value for ${GradleDokkaSourceSetBuilder::includeNonPublic.name}" + ) + + sourceSet.includeNonPublic.set(!DokkaDefaults.includeNonPublic) + assertEquals( + !DokkaDefaults.includeNonPublic, sourceSet.build().includeNonPublic, + "Expected flipped value for ${GradleDokkaSourceSetBuilder::includeNonPublic.name}" + ) + } + + @Test + fun documentedVisibilities() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals( + DokkaDefaults.documentedVisibilities, sourceSet.build().documentedVisibilities, + "Expected default value for ${GradleDokkaSourceSetBuilder::documentedVisibilities.name}" + ) + + val visibilities = setOf(DokkaConfiguration.Visibility.PRIVATE, DokkaConfiguration.Visibility.INTERNAL) + sourceSet.documentedVisibilities.set(visibilities) + assertEquals( + visibilities, sourceSet.build().documentedVisibilities, + "Expected to see previously set value for ${GradleDokkaSourceSetBuilder::includeNonPublic.name}" + ) + } + + @Test + fun reportUndocumented() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals( + DokkaDefaults.reportUndocumented, sourceSet.build().reportUndocumented, + "Expected default value for ${GradleDokkaSourceSetBuilder::reportUndocumented.name}" + ) + + sourceSet.reportUndocumented.set(!DokkaDefaults.reportUndocumented) + assertEquals( + !DokkaDefaults.reportUndocumented, sourceSet.build().reportUndocumented, + "Expected flipped value for ${GradleDokkaSourceSetBuilder::reportUndocumented.name}" + ) + } + + @Test + fun jdkVersion() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals( + DokkaDefaults.jdkVersion, sourceSet.build().jdkVersion, + "Expected default value for ${GradleDokkaSourceSetBuilder::jdkVersion.name}" + ) + + sourceSet.jdkVersion.set(DokkaDefaults.jdkVersion + 1) + assertEquals( + DokkaDefaults.jdkVersion + 1, sourceSet.build().jdkVersion, + "Expected increased value for ${GradleDokkaSourceSetBuilder::jdkVersion.name}" + ) + } + + @Test + fun sourceLinks() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals(emptySet(), sourceSet.build().sourceLinks, "Expected no default source links") + + sourceSet.sourceLinks.add( + GradleSourceLinkBuilder(project).apply { + this.remoteLineSuffix.set("ls1") + this.localDirectory.set(project.file("p1")) + this.remoteUrl.set(URL("https://u1")) + }) + + sourceSet.sourceLink { + remoteLineSuffix.set("ls2") + localDirectory.set(project.file("p2")) + remoteUrl.set(URL("https://u2")) + } + + sourceSet.sourceLink(project.closureOf<GradleSourceLinkBuilder> { + this.remoteLineSuffix.set("ls3") + this.localDirectory.set(project.file("p3")) + this.remoteUrl.set(URL("https://u3")) + }) + + assertEquals( + setOf( + SourceLinkDefinitionImpl( + remoteLineSuffix = "ls1", + localDirectory = project.file("p1").absolutePath, + remoteUrl = URL("https://u1") + ), + SourceLinkDefinitionImpl( + remoteLineSuffix = "ls2", + localDirectory = project.file("p2").absolutePath, + remoteUrl = URL("https://u2") + ), + SourceLinkDefinitionImpl( + remoteLineSuffix = "ls3", + localDirectory = project.file("p3").absolutePath, + remoteUrl = URL("https://u3") + ) + ), + sourceSet.build().sourceLinks, + "Expected all source links being present after build" + ) + } + + @Test + fun perPackageOptions() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals(emptyList(), sourceSet.build().perPackageOptions, "Expected no default per package options") + + sourceSet.perPackageOptions.add(GradlePackageOptionsBuilder(project).apply { + this.matchingRegex.set("p1.*") + }) + + sourceSet.perPackageOption { + matchingRegex.set("p2.*") + } + + sourceSet.perPackageOption(project.closureOf<GradlePackageOptionsBuilder> { + this.matchingRegex.set("p3.*") + }) + + assertEquals( + listOf("p1.*", "p2.*", "p3.*").map { matchingRegex -> + PackageOptionsImpl( + matchingRegex = matchingRegex, + includeNonPublic = DokkaDefaults.includeNonPublic, + documentedVisibilities = DokkaDefaults.documentedVisibilities, + reportUndocumented = DokkaDefaults.reportUndocumented, + skipDeprecated = DokkaDefaults.skipDeprecated, + suppress = DokkaDefaults.suppress + ) + }, + sourceSet.build().perPackageOptions, + "Expected all package options being present after build" + ) + } + + @Test + fun externalDocumentationLink() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + sourceSet.noAndroidSdkLink.set(true) + sourceSet.noJdkLink.set(true) + sourceSet.noStdlibLink.set(true) + assertEquals( + emptySet(), sourceSet.build().externalDocumentationLinks, + "Expected no default external documentation links" + ) + + sourceSet.externalDocumentationLinks.add( + GradleExternalDocumentationLinkBuilder(project).apply { + this.url.set(URL("https://u1")) + this.packageListUrl.set(URL("https://pl1")) + } + ) + + sourceSet.externalDocumentationLink { + url.set(URL("https://u2")) + } + + sourceSet.externalDocumentationLink(project.closureOf<GradleExternalDocumentationLinkBuilder> { + url.set(URL("https://u3")) + }) + + sourceSet.externalDocumentationLink(url = "https://u4", packageListUrl = "https://pl4") + sourceSet.externalDocumentationLink(url = URL("https://u5")) + + assertEquals( + setOf( + ExternalDocumentationLinkImpl(URL("https://u1"), URL("https://pl1")), + ExternalDocumentationLinkImpl(URL("https://u2"), URL("https://u2/package-list")), + ExternalDocumentationLinkImpl(URL("https://u3"), URL("https://u3/package-list")), + ExternalDocumentationLinkImpl(URL("https://u4"), URL("https://pl4")), + ExternalDocumentationLinkImpl(URL("https://u5"), URL("https://u5/package-list")) + ), + sourceSet.build().externalDocumentationLinks, + "Expected all external documentation links being present after build" + ) + } + + @Test + fun languageVersion() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertNull(sourceSet.build().languageVersion, "Expected no language version being set by default") + + sourceSet.languageVersion.set("JAVA_20") + assertEquals( + "JAVA_20", sourceSet.build().languageVersion, + "Expected previously set language version to be present after build" + ) + } + + @Test + fun apiVersion() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertNull(sourceSet.build().apiVersion, "Expected no api version being set by default") + + sourceSet.apiVersion.set("20") + assertEquals( + "20", sourceSet.build().apiVersion, + "Expected previously set api version to be present after build" + ) + } + + @Test + fun noStdlibLink() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertFalse(sourceSet.noStdlibLink.get(), "Expected 'noStdlibLink' to be set to false by default") + + assertEquals(1, sourceSet.build().externalDocumentationLinks.count { + "https://kotlinlang.org/api" in it.url.toURI().toString() + }, "Expected kotlin stdlib in external documentation links") + + sourceSet.noStdlibLink.set(true) + + assertEquals( + 0, sourceSet.build().externalDocumentationLinks.count { + "https://kotlinlang.org/api" in it.url.toURI().toString() + }, "Expected no stdlib in external documentation link" + ) + } + + @Test + fun noJdkLink() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertFalse(sourceSet.noJdkLink.get(), "Expected 'noJdkLink' to be set to false by default") + + assertEquals(1, sourceSet.build().externalDocumentationLinks.count { + "https://docs.oracle.com/" in it.url.toURI().toString() + }, "Expected java jdk in external documentation links") + + sourceSet.noJdkLink.set(true) + + assertEquals( + 0, sourceSet.build().externalDocumentationLinks.count { + "https://docs.oracle.com/" in it.url.toURI().toString() + }, "Expected no java jdk in external documentation link" + ) + } + + + @Test + fun noAndroidSdkLink() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertFalse(sourceSet.noAndroidSdkLink.get(), "Expected 'noAndroidSdkLink' to be set to false by default") + + assertEquals(0, sourceSet.build().externalDocumentationLinks.count { + "https://developer.android.com/reference" in it.url.toURI().toString() + }, "Expected no android sdk in external documentation links (without android plugin)") + + assertEquals(0, sourceSet.build().externalDocumentationLinks.count { + "https://developer.android.com/reference/androidx" in it.packageListUrl.toURI().toString() + }, "Expected no androidx in external documentation links (without android plugin)") + + + project.plugins.apply("com.android.library") + + assertEquals(1, sourceSet.build().externalDocumentationLinks.count { + "https://developer.android.com/reference/kotlin/package-list" in it.packageListUrl.toURI().toString() + }, "Expected android sdk in external documentation links") + + assertEquals(1, sourceSet.build().externalDocumentationLinks.count { + "https://developer.android.com/reference/kotlin/androidx/package-list" in it.packageListUrl.toURI() + .toString() + }, "Expected androidx in external documentation links") + + + sourceSet.noAndroidSdkLink.set(true) + + assertEquals(0, sourceSet.build().externalDocumentationLinks.count { + "https://developer.android.com/reference" in it.url.toURI().toString() + }, "Expected no android sdk in external documentation links") + + assertEquals(0, sourceSet.build().externalDocumentationLinks.count { + "https://developer.android.com/reference/kotlin/androidx/package-list" in it.packageListUrl.toURI() + .toString() + }, "Expected no androidx in external documentation links") + } + + @Test + fun suppressedFiles() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertTrue(sourceSet.build().suppressedFiles.isEmpty(), "Expected no suppressed files by default") + + sourceSet.suppressedFiles.from(project.file("f1")) + sourceSet.suppressedFiles.from("f2") + + assertEquals( + setOf(project.file("f1"), project.file("f2")), sourceSet.build().suppressedFiles, + "Expected all suppressed files to be present after build" + ) + } + + @Test + fun suppressedFilesByDefault() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertTrue(sourceSet.build().suppressedFiles.isEmpty(), "Expected no suppressed files by default") + + val file = project.buildDir.resolve("generated").also { it.mkdirs() } + file.resolve("suppressed.kt").writeText("class A") + + sourceSet.sourceRoots.from(project.buildDir.resolve("generated")) + + val suppressedConfiguration = sourceSet.build() + sourceSet.suppressGeneratedFiles.set(false) + val unsuppressedConfiguration = sourceSet.build() + + assertEquals( + setOf( + project.buildDir.resolve("generated"), + project.buildDir.resolve("generated").resolve("suppressed.kt") + ), suppressedConfiguration.suppressedFiles, + "Expected all suppressed files to be present after build" + ) + + assertTrue( + unsuppressedConfiguration.suppressedFiles.isEmpty(), + "Expected no files to be suppressed by default" + ) + } + + @Test + fun platform() { + val sourceSet = GradleDokkaSourceSetBuilder("", project) + assertEquals(Platform.DEFAULT, sourceSet.build().analysisPlatform, "Expected default platform if not specified") + + sourceSet.platform.set(Platform.common) + assertEquals( + Platform.common, sourceSet.build().analysisPlatform, + "Expected previously set analysis platform being present after build" + ) + } +} + +@Suppress("TestFunctionName") +private fun GradleDokkaSourceSetBuilder(name: String, project: Project) = + GradleDokkaSourceSetBuilder(name, project, project.path) diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/KotlinDslDokkaTaskConfigurationTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/KotlinDslDokkaTaskConfigurationTest.kt new file mode 100644 index 00000000..f95dfdc2 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/KotlinDslDokkaTaskConfigurationTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.gradle.utils.configureEach_ +import org.jetbrains.dokka.gradle.utils.create_ +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals + +class KotlinDslDokkaTaskConfigurationTest { + @Test + fun `configure dokka task`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + project.tasks.withType<DokkaTask>().configureEach_ { + outputDirectory.set(File("test")) + } + + project.tasks.withType(DokkaTask::class.java).forEach { dokkaTask -> + assertEquals(File("test"), dokkaTask.outputDirectory.get().asFile.relativeTo(project.projectDir)) + } + } + + @Test + fun `sourceSet dependsOn by String`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + project.tasks.withType(DokkaTask::class.java).forEach { dokkaTask -> + dokkaTask.dokkaSourceSets.run { + val commonMain = create("commonMain") + val jvmMain = create_("jvmMain") { + dependsOn("commonMain") + } + + assertEquals( + 0, commonMain.dependentSourceSets.get().size, + "Expected no dependent source set in commonMain" + ) + + assertEquals( + 1, jvmMain.dependentSourceSets.get().size, + "Expected only one dependent source set in jvmMain" + ) + + assertEquals( + commonMain.sourceSetID, jvmMain.dependentSourceSets.get().single(), + "Expected jvmMain to depend on commonMain" + ) + + assertEquals( + DokkaSourceSetID(dokkaTask, "commonMain"), commonMain.sourceSetID + ) + } + } + } + + @Test + fun `sourceSet dependsOn by DokkaSourceSet`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + project.tasks.withType(DokkaTask::class.java).first().run { + dokkaSourceSets.run { + val commonMain = create("commonMain") + val jvmMain = create("jvmMain") { + dependsOn(commonMain) + } + + assertEquals( + commonMain.sourceSetID, jvmMain.dependentSourceSets.get().single() + ) + } + } + } + + @Test + fun `sourceSet dependsOn by KotlinSourceSet`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + project.plugins.apply("org.jetbrains.kotlin.jvm") + + val kotlin = project.extensions.getByName("kotlin") as KotlinJvmProjectExtension + + project.tasks.withType(DokkaTask::class.java).first().apply { + dokkaSourceSets.run { + val special = create("special") { + dependsOn(kotlin.sourceSets.getByName("main")) + } + + assertEquals( + DokkaSourceSetID(this@apply, "main"), special.dependentSourceSets.get().single() + ) + } + } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/KotlinSourceSetGistTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/KotlinSourceSetGistTest.kt new file mode 100644 index 00000000..5c99502a --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/KotlinSourceSetGistTest.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle + +import org.jetbrains.dokka.gradle.utils.withDependencies_ +import org.gradle.api.artifacts.FileCollectionDependency +import org.gradle.api.internal.project.DefaultProject +import org.gradle.kotlin.dsl.get +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.gradle.kotlin.gistOf +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class KotlinSourceSetGistTest { + + @Test + fun `main source set with kotlin jvm`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.kotlin.jvm") + val kotlin = project.kotlin as KotlinJvmProjectExtension + + val mainSourceSet = kotlin.sourceSets.getByName("main") + val mainSourceSetGist = project.gistOf(mainSourceSet) + + assertEquals( + "main", mainSourceSetGist.name, + "Expected correct source set name" + ) + + assertEquals( + KotlinPlatformType.jvm, mainSourceSetGist.platform.get(), + "Expected correct platform" + ) + + assertTrue( + mainSourceSetGist.isMain.get(), + "Expected main sources to be marked as 'isMain'" + ) + + assertEquals( + emptySet(), mainSourceSetGist.dependentSourceSetNames.get(), + "Expected no dependent source sets" + ) + } + + @Test + fun `test source set with kotlin jvm`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.kotlin.jvm") + val kotlin = project.kotlin as KotlinJvmProjectExtension + + val testSourceSet = kotlin.sourceSets.getByName("test") + val testSourceSetGist = project.gistOf(testSourceSet) + + assertFalse( + testSourceSetGist.isMain.get(), + "Expected test source set not being marked as 'isMain'" + ) + + assertEquals( + emptySet(), + testSourceSetGist.dependentSourceSetNames.get(), + "Expected no dependent source sets" + ) + } + + @Test + fun `sourceRoots of main source set with kotlin jvm`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.kotlin.jvm") + val kotlin = project.kotlin as KotlinJvmProjectExtension + val mainSourceSet = kotlin.sourceSets.getByName("main") + val mainSourceSetGist = project.gistOf(mainSourceSet) + + assertEquals( + emptySet(), mainSourceSetGist.sourceRoots.files, + "Expected no sourceRoots, because default source root does not exist on filesystem yet" + ) + + // Create default source root on filesystem + val defaultSourceRoot = project.file("src/main/kotlin") + defaultSourceRoot.mkdirs() + + assertEquals( + setOf(defaultSourceRoot), mainSourceSetGist.sourceRoots.files, + "Expected default source root in source roots, since it is present on the filesystem" + ) + + // Create custom source set (and make sure it exists on filesystem) + val customSourceRoot = project.file("src/main/custom").also(File::mkdirs) + mainSourceSet.kotlin.srcDir(customSourceRoot) + + assertEquals( + setOf(defaultSourceRoot, customSourceRoot), mainSourceSetGist.sourceRoots.files, + "Expected recently registered custom source root to be present" + ) + + // removing default source root + mainSourceSet.kotlin.setSrcDirs(listOf(customSourceRoot)) + + assertEquals( + setOf(customSourceRoot), mainSourceSetGist.sourceRoots.files, + "Expected only custom source root being present in source roots" + ) + } + + @Suppress("UnstableApiUsage") + @Test + fun `classpath of main source set with kotlin jvm`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.kotlin.jvm") + val kotlin = project.kotlin as KotlinJvmProjectExtension + val mainSourceSet = kotlin.sourceSets.getByName("main") + val mainSourceSetGist = project.gistOf(mainSourceSet) + + /* Only work with file dependencies */ + project.configurations.forEach { configuration -> + configuration.withDependencies_ { + removeIf { dependency -> + dependency !is FileCollectionDependency + } + } + } + + val implementationJar = project.file("implementation.jar") + val compileOnlyJar = project.file("compileOnly.jar") + val apiJar = project.file("api.jar") + val runtimeOnlyJar = project.file("runtimeOnly.jar") + + + mainSourceSet.dependencies { + implementation(project.files(implementationJar)) + compileOnly(project.files(compileOnlyJar)) + api(project.files(apiJar)) + runtimeOnly(project.files(runtimeOnlyJar)) + } + + assertEquals( + emptySet(), mainSourceSetGist.classpath.get().files, + "Expected no files on the classpath, since no file exists" + ) + + /* Creating dependency files */ + assertTrue(implementationJar.createNewFile()) + assertTrue(compileOnlyJar.createNewFile()) + assertTrue(apiJar.createNewFile()) + assertTrue(runtimeOnlyJar.createNewFile()) + + assertEquals( + setOf(implementationJar, compileOnlyJar, apiJar), mainSourceSetGist.classpath.get().files, + "Expected implementation, compileOnly and api dependencies on classpath" + ) + } + + @Test + fun `common, jvm and macos source sets with kotlin multiplatform`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.kotlin.multiplatform") + val kotlin = project.kotlin as KotlinMultiplatformExtension + kotlin.jvm() + kotlin.macosX64("macos") + + (project as DefaultProject).evaluate() + val commonMainSourceSet = kotlin.sourceSets.getByName("commonMain") + val commonMainSourceSetGist = project.gistOf(commonMainSourceSet) + + val jvmMainSourceSet = kotlin.sourceSets.getByName("jvmMain") + val jvmMainSourceSetGist = project.gistOf(jvmMainSourceSet) + + val macosMainSourceSet = kotlin.sourceSets.getByName("macosMain") + val macosMainSourceSetGist = project.gistOf(macosMainSourceSet) + + assertEquals( + "commonMain", commonMainSourceSetGist.name, + "Expected correct source set name" + ) + + assertEquals( + "jvmMain", jvmMainSourceSetGist.name, + "Expected correct source set name" + ) + + assertEquals( + "macosMain", macosMainSourceSetGist.name, + "Expected correct source set name" + ) + + assertEquals( + KotlinPlatformType.common, commonMainSourceSetGist.platform.get(), + "Expected common platform for commonMain source set" + ) + + assertEquals( + KotlinPlatformType.jvm, jvmMainSourceSetGist.platform.get(), + "Expected jvm platform for jvmMain source set" + ) + + assertEquals( + KotlinPlatformType.native, macosMainSourceSetGist.platform.get(), + "Expected native platform for macosMain source set" + ) + + assertTrue( + commonMainSourceSetGist.isMain.get(), + "Expected commonMain to be marked with 'isMain'" + ) + + assertTrue( + jvmMainSourceSetGist.isMain.get(), + "Expected jvmMain to be marked with 'isMain'" + ) + + assertTrue( + macosMainSourceSetGist.isMain.get(), + "Expected macosMain to be marked with 'isMain'" + ) + + assertFalse( + project.gistOf(kotlin.sourceSets["commonTest"]).isMain.get(), + "Expected commonTest not being marked with 'isMain'" + ) + + assertFalse( + project.gistOf(kotlin.sourceSets["jvmTest"]).isMain.get(), + "Expected jvmTest not being marked with 'isMain'" + ) + + assertFalse( + project.gistOf(kotlin.sourceSets["macosTest"]).isMain.get(), + "Expected macosTest not being marked with 'isMain'" + ) + + // requires `project.evaluate()` + assertEquals( + setOf("commonMain"), jvmMainSourceSetGist.dependentSourceSetNames.get(), + "Expected jvmMain to depend on commonMain by default" + ) + } + +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinGradlePluginVersionTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinGradlePluginVersionTest.kt new file mode 100644 index 00000000..e0731687 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/kotlin/KotlinGradlePluginVersionTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.kotlin + +import kotlin.test.* + +class KotlinGradlePluginVersionTest { + + @Test + fun `should parse versions`() { + assertParsedVersion(input = "1.7.22", expectedMajor = 1, expectedMinor = 7, expectedPatch = 22) + assertParsedVersion(input = "2.0.0", expectedMajor = 2, expectedMinor = 0, expectedPatch = 0) + assertParsedVersion(input = "1.8.22-RC2", expectedMajor = 1, expectedMinor = 8, expectedPatch = 22) + assertParsedVersion(input = "1.5.22-Beta", expectedMajor = 1, expectedMinor = 5, expectedPatch = 22) + assertParsedVersion(input = "2.7.22-mercury-500", expectedMajor = 2, expectedMinor = 7, expectedPatch = 22) + assertParsedVersion(input = "1.7.100-dev-800", expectedMajor = 1, expectedMinor = 7, expectedPatch = 100) + } + + @Test + fun `should return null on non parsable string`() { + assertNull(parse("1.7")) + assertNull(parse("1")) + assertNull(parse("6")) + assertNull(parse("17.0")) + assertNull(parse("1..7.0")) + assertNull(parse("1.7-Beta")) + assertNull(parse("1.7.0Beta")) + } + + @Test + fun `should compare simple versions`() { + assertEquals(0, KotlinGradlePluginVersion(1, 7, 0).compareTo(KotlinGradlePluginVersion(1, 7, 0))) + + assertTrue(KotlinGradlePluginVersion(1, 6, 10) >= KotlinGradlePluginVersion(1, 6, 10)) + assertTrue(KotlinGradlePluginVersion(1, 6, 10) < KotlinGradlePluginVersion(1, 6, 20)) + assertTrue(KotlinGradlePluginVersion(1, 6, 10) > KotlinGradlePluginVersion(1, 6, 0)) + + assertTrue(KotlinGradlePluginVersion(1, 4, 32) <= KotlinGradlePluginVersion(1, 4, 32)) + assertTrue(KotlinGradlePluginVersion(1, 4, 32) < KotlinGradlePluginVersion(1, 6, 20)) + assertTrue(KotlinGradlePluginVersion(1, 4, 32) > KotlinGradlePluginVersion(1, 3, 0)) + + assertTrue(KotlinGradlePluginVersion(2, 1, 0) > KotlinGradlePluginVersion(1, 8, 0)) + assertTrue(KotlinGradlePluginVersion(1, 5, 31) < KotlinGradlePluginVersion(1, 7, 0)) + } + + @Test + fun `should compare custom dev versions with trailing strings`() { + assertEquals(0, KotlinGradlePluginVersion(1, 7, 0).compareTo(parseNotNull("1.7.0"))) + + assertTrue(KotlinGradlePluginVersion(1, 6, 10) >= parseNotNull("1.6.10-Beta")) + assertTrue(KotlinGradlePluginVersion(1, 6, 10) < parseNotNull("1.6.20")) + assertTrue(KotlinGradlePluginVersion(1, 6, 10) > parseNotNull("1.6.0-RC2")) + + assertTrue(KotlinGradlePluginVersion(1, 4, 32) <= parseNotNull("1.4.32-dev-142")) + assertTrue(KotlinGradlePluginVersion(1, 4, 32) < parseNotNull("1.6.20-Beta")) + assertTrue(KotlinGradlePluginVersion(1, 4, 32) > parseNotNull("1.3.0-RC")) + + assertTrue(KotlinGradlePluginVersion(2, 1, 0) > parseNotNull("1.8.0-mercury-500")) + } + + private fun assertParsedVersion( + input: String, + expectedMajor: Int, + expectedMinor: Int, + expectedPatch: Int + ) { + val kgpVersion = parseNotNull(input) + assertEquals(expectedMajor, kgpVersion.major) + assertEquals(expectedMinor, kgpVersion.minor) + assertEquals(expectedPatch, kgpVersion.patch) + assertEquals(KotlinGradlePluginVersion(expectedMajor, expectedMinor, expectedPatch), kgpVersion) + } + + private fun parseNotNull(input: String): KotlinGradlePluginVersion = assertNotNull(parse(input)) + + private fun parse(input: String): KotlinGradlePluginVersion? = parseKotlinVersion(input) +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaCollectorTaskTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaCollectorTaskTest.kt new file mode 100644 index 00000000..07cb04d2 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaCollectorTaskTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.tasks + +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.DokkaException +import org.jetbrains.dokka.gradle.AbstractDokkaTask +import org.jetbrains.dokka.gradle.DokkaCollectorTask +import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.dokka.gradle.utils.all_ +import org.jetbrains.dokka.gradle.utils.allprojects_ +import org.jetbrains.dokka.gradle.utils.configureEach_ +import org.jetbrains.dokka.gradle.utils.withDependencies_ +import java.io.File +import kotlin.test.* + +class DokkaCollectorTaskTest { + + @Test + fun buildDokkaConfiguration() { + val rootProject = ProjectBuilder.builder().build() + val childProject = ProjectBuilder.builder().withParent(rootProject).build() + childProject.plugins.apply("org.jetbrains.kotlin.jvm") + + rootProject.allprojects_ { + plugins.apply("org.jetbrains.dokka") + tasks.withType<AbstractDokkaTask>().configureEach_ { + plugins.withDependencies_ { clear() } + } + tasks.withType<DokkaTask>().configureEach_ { + dokkaSourceSets.configureEach_ { + classpath.setFrom(emptyList<Any>()) + } + } + } + + val collectorTasks = rootProject.tasks.withType<DokkaCollectorTask>() + collectorTasks.configureEach_ { + moduleName.set("custom Module Name") + outputDirectory.set(File("customOutputDirectory")) + cacheRoot.set(File("customCacheRoot")) + failOnWarning.set(true) + offlineMode.set(true) + } + + assertTrue(collectorTasks.isNotEmpty(), "Expected at least one collector task") + + collectorTasks.forEach { task -> + val dokkaConfiguration = task.buildDokkaConfiguration() + assertEquals( + DokkaConfigurationImpl( + moduleName = "custom Module Name", + outputDir = rootProject.projectDir.resolve("customOutputDirectory"), + cacheRoot = rootProject.projectDir.resolve("customCacheRoot"), + failOnWarning = true, + offlineMode = true, + sourceSets = task.childDokkaTasks + .map { it.buildDokkaConfiguration() } + .map { it.sourceSets } + .reduce { acc, list -> acc + list }, + pluginsClasspath = task.childDokkaTasks + .map { it.plugins.resolve().toList() } + .reduce { acc, mutableSet -> acc + mutableSet } + ), + dokkaConfiguration, + ) + } + } + + @Test + fun `verify that cacheRoot is optional, and not required to build DokkaConfiguration`() { + val rootProject = ProjectBuilder.builder().build() + val childProject = ProjectBuilder.builder().withParent(rootProject).build() + childProject.plugins.apply("org.jetbrains.kotlin.jvm") + + rootProject.allprojects_ { + plugins.apply("org.jetbrains.dokka") + tasks.withType<AbstractDokkaTask>().configureEach_ { + plugins.withDependencies_ { clear() } + } + tasks.withType<DokkaTask>().configureEach_ { + dokkaSourceSets.configureEach_ { + classpath.setFrom(emptyList<Any>()) + } + } + } + + val collectorTasks = rootProject.tasks.withType<DokkaCollectorTask>() + collectorTasks.configureEach_ { + cacheRoot.set(null as File?) + } + + assertTrue(collectorTasks.isNotEmpty(), "Expected at least one collector task") + + collectorTasks.forEach { task -> + val dokkaConfiguration = task.buildDokkaConfiguration() + assertNull(dokkaConfiguration.cacheRoot, "Expect that cacheRoot is null") + } + } + + @Test + fun `with no child tasks throws DokkaException`() { + val project = ProjectBuilder.builder().build() + val collectorTask = project.tasks.create<DokkaCollectorTask>("collector") + project.configurations.all_ { withDependencies_ { clear() } } + assertFailsWith<DokkaException> { collectorTask.generateDocumentation() } + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaMultiModuleTaskTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaMultiModuleTaskTest.kt new file mode 100644 index 00000000..a95af583 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaMultiModuleTaskTest.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UnstableApiUsage", "DEPRECATION", "PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.withType +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.* +import org.jetbrains.dokka.gradle.utils.allprojects_ +import org.jetbrains.dokka.gradle.utils.configureEach_ +import org.jetbrains.dokka.gradle.utils.create_ +import org.jetbrains.dokka.gradle.utils.withDependencies_ +import java.io.File +import kotlin.test.* + +class DokkaMultiModuleTaskTest { + + private val rootProject = ProjectBuilder.builder() + .withName("root") + .build() + + private val childProject = ProjectBuilder.builder() + .withName("child") + .withProjectDir(rootProject.projectDir.resolve("child")) + .withParent(rootProject).build() + + private val childDokkaTask = childProject.tasks.create<DokkaTaskPartial>("childDokkaTask") + + private val multiModuleTask = rootProject.tasks.create<DokkaMultiModuleTask>("multiModuleTask").apply { + addChildTask(childDokkaTask) + } + + init { + rootProject.plugins.apply("org.jetbrains.dokka") + childProject.plugins.apply("org.jetbrains.dokka") + rootProject.allprojects_ { + tasks.withType<AbstractDokkaTask>().configureEach_ { + plugins.withDependencies_ { clear() } + } + } + } + + @Test + fun `child project is withing root project`() { + assertEquals( + rootProject.projectDir, childProject.projectDir.parentFile, + "Expected child project being inside the root project" + ) + + assertEquals( + childProject.projectDir.name, "child", + "Expected folder of child project to be called 'child'" + ) + } + + @Test + fun buildDokkaConfiguration() { + val include1 = childDokkaTask.project.file("include1.md") + val include2 = childDokkaTask.project.file("include2.md") + val topLevelInclude = multiModuleTask.project.file("README.md") + + childDokkaTask.apply { + dokkaSourceSets.create("main") + dokkaSourceSets.create("test") + dokkaSourceSets.configureEach_ { + includes.from(include1, include2) + } + } + + multiModuleTask.apply { + moduleVersion.set("1.5.0") + moduleName.set("custom Module Name") + outputDirectory.set(project.buildDir.resolve("customOutputDirectory")) + cacheRoot.set(File("customCacheRoot")) + pluginsConfiguration.add( + PluginConfigurationImpl( + "pluginA", + DokkaConfiguration.SerializationFormat.JSON, + """ { "key" : "value2" } """ + ) + ) + failOnWarning.set(true) + offlineMode.set(true) + includes.from(listOf(topLevelInclude)) + } + + val dokkaConfiguration = multiModuleTask.buildDokkaConfiguration() + assertEquals( + DokkaConfigurationImpl( + moduleName = "custom Module Name", + moduleVersion = "1.5.0", + outputDir = multiModuleTask.project.buildDir.resolve("customOutputDirectory"), + cacheRoot = multiModuleTask.project.projectDir.resolve("customCacheRoot"), + pluginsConfiguration = mutableListOf( + PluginConfigurationImpl( + "pluginA", + DokkaConfiguration.SerializationFormat.JSON, + """ { "key" : "value2" } """ + ) + ), + pluginsClasspath = emptyList(), + failOnWarning = true, + offlineMode = true, + includes = setOf(topLevelInclude), + modules = listOf( + DokkaModuleDescriptionImpl( + name = "child", + relativePathToOutputDirectory = File("child"), + includes = setOf(include1, include2), + sourceOutputDirectory = childDokkaTask.outputDirectory.get().asFile + ) + ) + ), + dokkaConfiguration + ) + } + + @Test + fun `multimodule task should not include unspecified version`() { + childDokkaTask.apply { + dokkaSourceSets.create("main") + dokkaSourceSets.create("test") + } + + multiModuleTask.apply { + moduleVersion.set("unspecified") + } + + val dokkaConfiguration = multiModuleTask.buildDokkaConfiguration() + assertNull(dokkaConfiguration.moduleVersion) + } + + @Test + fun `setting dokkaTaskNames declares proper task dependencies`() { + val dependenciesInitial = multiModuleTask.taskDependencies.getDependencies(multiModuleTask).toSet() + assertEquals(1, dependenciesInitial.size, "Expected one dependency") + val dependency = dependenciesInitial.single() + + assertTrue( + dependency is DokkaTaskPartial, + "Expected dependency to be of Type ${DokkaTaskPartial::class.simpleName}" + ) + assertEquals(childProject, dependency.project, "Expected dependency from child project") + + + val customDokkaTask = childProject.tasks.create<DokkaTask>("customDokkaTask") + + multiModuleTask.addSubprojectChildTasks("customDokkaTask") + val dependenciesAfter = multiModuleTask.taskDependencies.getDependencies(multiModuleTask).toSet() + + assertEquals(2, dependenciesAfter.size, "Expected two dependencies") + assertTrue(customDokkaTask in dependenciesAfter, "Expected 'customDokkaTask' in dependencies") + + } + + @Test + fun `multimodule task with no child tasks throws DokkaException`() { + val project = ProjectBuilder.builder().build() + val multimodule = project.tasks.create<DokkaMultiModuleTask>("multimodule") + project.configurations.configureEach_ { withDependencies_ { clear() } } + assertFailsWith<DokkaException> { multimodule.generateDocumentation() } + } + + @Test + fun childDokkaTaskIncludes() { + val childDokkaTaskInclude1 = childProject.file("include1") + val childDokkaTaskInclude2 = childProject.file("include2") + val childDokkaTaskInclude3 = childProject.file("include3") + + childDokkaTask.apply { + dokkaSourceSets.create_("main") { + includes.from(childDokkaTaskInclude1, childDokkaTaskInclude2) + } + dokkaSourceSets.create_("main2") { + includes.from(childDokkaTaskInclude3) + } + } + + val secondChildDokkaTaskInclude = childProject.file("include4") + val secondChildDokkaTask = childProject.tasks.create<DokkaTaskPartial>("secondChildDokkaTask") { + dokkaSourceSets.create_("main") { + includes.from(secondChildDokkaTaskInclude) + } + } + multiModuleTask.addChildTask(secondChildDokkaTask) + + assertEquals( + mapOf( + ":child:childDokkaTask" to setOf( + childDokkaTaskInclude1, + childDokkaTaskInclude2, + childDokkaTaskInclude3 + ), + ":child:secondChildDokkaTask" to setOf(secondChildDokkaTaskInclude) + ), + multiModuleTask.childDokkaTaskIncludes + ) + } + + @Test + fun sourceChildOutputDirectories() { + val parent = ProjectBuilder.builder().build() + parent.plugins.apply("org.jetbrains.dokka") + val child = ProjectBuilder.builder().withName("child").withParent(parent).build() + child.plugins.apply("org.jetbrains.dokka") + + val parentTask = parent.tasks.create<DokkaMultiModuleTask>("parent") + val childTask = child.tasks.create<DokkaTask>("child") + + parentTask.addChildTask(childTask) + childTask.outputDirectory.set(child.file("custom/output")) + + assertEquals( + listOf(parent.file("child/custom/output")), + parentTask.sourceChildOutputDirectories.files.toList(), + "Expected child output directory being present" + ) + } + + @Test + fun targetChildOutputDirectories() { + val parent = ProjectBuilder.builder().build() + val child = ProjectBuilder.builder().withName("child").withParent(parent).build() + + val parentTask = parent.tasks.create<DokkaMultiModuleTask>("parent") + val childTask = child.tasks.create<DokkaTask>("child") + + parentTask.addChildTask(childTask) + + parentTask.fileLayout.set(DokkaMultiModuleFileLayout { taskParent, taskChild -> + taskParent.project.layout.buildDirectory.dir(taskChild.name) + }) + + assertEquals( + listOf(parent.project.buildDir.resolve("child")), + parentTask.targetChildOutputDirectories.get().map { it.asFile }, + "Expected child target output directory being present" + ) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTaskTest.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTaskTest.kt new file mode 100644 index 00000000..d86fa268 --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/tasks/DokkaTaskTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("PackageDirectoryMismatch") + +package org.jetbrains.dokka.gradle + +import org.gradle.kotlin.dsl.create +import org.gradle.testfixtures.ProjectBuilder +import org.jetbrains.dokka.gradle.utils.all_ +import org.jetbrains.dokka.gradle.utils.register_ +import org.jetbrains.dokka.gradle.utils.withDependencies_ +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class DokkaTaskTest { + @Test + fun `no suppressed source sets are present after in built configuration`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + val task = project.tasks.create<DokkaTask>("dokkaTask") + project.configurations.all_ { withDependencies_ { clear() } } + + task.dokkaSourceSets.register("main") + task.dokkaSourceSets.register("jvm") + task.dokkaSourceSets.register_("test") { + suppress.set(true) + } + + assertEquals( + listOf("main", "jvm").sorted(), + task.buildDokkaConfiguration().sourceSets.map { it.sourceSetID.sourceSetName }.sorted(), + "Expected only unsuppressed source sets `main` and `test` to be present in built configuration" + ) + } + + @Test + fun `module version is not present if not specified`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("org.jetbrains.dokka") + + val task = project.tasks.create<DokkaTask>("dokkaTask") + project.configurations.all_ { withDependencies_ { clear() } } + + task.dokkaSourceSets.register("main") + assertNull(task.buildDokkaConfiguration().moduleVersion) + } +} diff --git a/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/utils/samWithReceiverWorkarounds.kt b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/utils/samWithReceiverWorkarounds.kt new file mode 100644 index 00000000..198f189e --- /dev/null +++ b/dokka-runners/runner-gradle-plugin-classic/src/test/kotlin/org/jetbrains/dokka/gradle/utils/samWithReceiverWorkarounds.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.gradle.utils + +import org.gradle.api.* +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.DependencySet +import org.jetbrains.dokka.gradle.GradleDokkaSourceSetBuilder +import org.jetbrains.dokka.gradle.GradleExternalDocumentationLinkBuilder + + +/** + * Workarounds because `SamWithReceiver` not working in test sources + * https://youtrack.jetbrains.com/issue/KTIJ-14684 + * + * The `SamWithReceiver` plugin is automatically applied by the `kotlin-dsl` plugin. + * It converts all [org.gradle.api.Action] so the parameter is the receiver: + * + * ``` + * // with SamWithReceiver ✅ + * tasks.configureEach { + * val task: Task = this + * } + * + * // without SamWithReceiver + * tasks.configureEach { it -> + * val task: Task = it + * } + * ``` + * + * This is nice because it means that the Dokka Gradle Plugin more closely matches `build.gradle.kts` files. + * + * However, [IntelliJ is bugged](https://youtrack.jetbrains.com/issue/KTIJ-14684) and doesn't + * acknowledge that `SamWithReceiver` has been applied in test sources. The code works and compiles, + * but IntelliJ shows red errors. + * + * These functions are workarounds, and should be removed ASAP. + */ +@Suppress("unused") +private object Explain + +fun Project.subprojects_(configure: Project.() -> Unit) = + subprojects(configure) + +@Suppress("SpellCheckingInspection") +fun Project.allprojects_(configure: Project.() -> Unit) = + allprojects(configure) + +fun <T> DomainObjectCollection<T>.configureEach_(configure: T.() -> Unit) = + configureEach(configure) + +fun <T> DomainObjectCollection<T>.all_(configure: T.() -> Unit) = + all(configure) + +fun Configuration.withDependencies_(action: DependencySet.() -> Unit): Configuration = + withDependencies(action) + + +fun <T> NamedDomainObjectContainer<T>.create_(name: String, configure: T.() -> Unit): T = + create(name, configure) + +fun <T> NamedDomainObjectContainer<T>.register_( + name: String, + configure: T.() -> Unit +): NamedDomainObjectProvider<T> = + register(name, configure) + + +fun GradleDokkaSourceSetBuilder.externalDocumentationLink_( + action: GradleExternalDocumentationLinkBuilder.() -> Unit +) = externalDocumentationLink(action) diff --git a/dokka-runners/runner-maven-plugin/api/runner-maven-plugin.api b/dokka-runners/runner-maven-plugin/api/runner-maven-plugin.api new file mode 100644 index 00000000..069e7744 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/api/runner-maven-plugin.api @@ -0,0 +1,141 @@ +public abstract class org/jetbrains/dokka/maven/AbstractDokkaMojo : org/apache/maven/plugin/AbstractMojo { + public fun <init> (Ljava/util/List;)V + public fun execute ()V + public final fun getApiVersion ()Ljava/lang/String; + public final fun getCacheRoot ()Ljava/lang/String; + public final fun getClasspath ()Ljava/util/List; + public final fun getDisplayName ()Ljava/lang/String; + public final fun getDocumentedVisibilities ()Ljava/util/Set; + public final fun getDokkaPlugins ()Ljava/util/List; + public final fun getExternalDocumentationLinks ()Ljava/util/List; + public final fun getFailOnWarning ()Z + public final fun getIncludeNonPublic ()Z + public final fun getIncludes ()Ljava/util/List; + public final fun getJdkVersion ()I + public final fun getLanguageVersion ()Ljava/lang/String; + protected final fun getMavenProject ()Lorg/apache/maven/project/MavenProject; + public final fun getModuleName ()Ljava/lang/String; + public final fun getNoJdkLink ()Z + public final fun getNoStdlibLink ()Z + public final fun getOfflineMode ()Z + protected abstract fun getOutDir ()Ljava/lang/String; + public final fun getPerPackageOptions ()Ljava/util/List; + public final fun getPlatform ()Ljava/lang/String; + public final fun getReportUndocumented ()Z + public final fun getSamples ()Ljava/util/List; + protected final fun getSession ()Lorg/apache/maven/execution/MavenSession; + public final fun getSkip ()Z + public final fun getSkipDeprecated ()Z + public final fun getSkipEmptyPackages ()Z + public final fun getSourceDirectories ()Ljava/util/List; + public final fun getSourceLinks ()Ljava/util/List; + public final fun getSourceSetName ()Ljava/lang/String; + public final fun getSuppressInheritedMembers ()Z + public final fun getSuppressObviousFunctions ()Z + public final fun getSuppressedFiles ()Ljava/util/List; + public final fun setApiVersion (Ljava/lang/String;)V + public final fun setCacheRoot (Ljava/lang/String;)V + public final fun setClasspath (Ljava/util/List;)V + public final fun setDisplayName (Ljava/lang/String;)V + public final fun setDocumentedVisibilities (Ljava/util/Set;)V + public final fun setDokkaPlugins (Ljava/util/List;)V + public final fun setExternalDocumentationLinks (Ljava/util/List;)V + public final fun setFailOnWarning (Z)V + public final fun setIncludeNonPublic (Z)V + public final fun setIncludes (Ljava/util/List;)V + public final fun setJdkVersion (I)V + public final fun setLanguageVersion (Ljava/lang/String;)V + protected final fun setMavenProject (Lorg/apache/maven/project/MavenProject;)V + public final fun setModuleName (Ljava/lang/String;)V + public final fun setNoJdkLink (Z)V + public final fun setNoStdlibLink (Z)V + public final fun setOfflineMode (Z)V + public final fun setPerPackageOptions (Ljava/util/List;)V + public final fun setPlatform (Ljava/lang/String;)V + public final fun setReportUndocumented (Z)V + public final fun setSamples (Ljava/util/List;)V + protected final fun setSession (Lorg/apache/maven/execution/MavenSession;)V + public final fun setSkip (Z)V + public final fun setSkipDeprecated (Z)V + public final fun setSkipEmptyPackages (Z)V + public final fun setSourceDirectories (Ljava/util/List;)V + public final fun setSourceLinks (Ljava/util/List;)V + public final fun setSourceSetName (Ljava/lang/String;)V + public final fun setSuppressInheritedMembers (Z)V + public final fun setSuppressObviousFunctions (Z)V + public final fun setSuppressedFiles (Ljava/util/List;)V +} + +public final class org/jetbrains/dokka/maven/DokkaJavadocJarMojo : org/jetbrains/dokka/maven/AbstractDokkaMojo { + public fun <init> ()V + public fun execute ()V + public final fun getOutputDir ()Ljava/lang/String; + public final fun setOutputDir (Ljava/lang/String;)V +} + +public final class org/jetbrains/dokka/maven/DokkaJavadocMojo : org/jetbrains/dokka/maven/AbstractDokkaMojo { + public fun <init> ()V + public final fun getOutputDir ()Ljava/lang/String; + public final fun setOutputDir (Ljava/lang/String;)V +} + +public final class org/jetbrains/dokka/maven/DokkaMojo : org/jetbrains/dokka/maven/AbstractDokkaMojo { + public fun <init> ()V + public final fun getOutputDir ()Ljava/lang/String; + public final fun setOutputDir (Ljava/lang/String;)V +} + +public final class org/jetbrains/dokka/maven/ExternalDocumentationLinkBuilder { + public fun <init> ()V + public final fun build ()Lorg/jetbrains/dokka/ExternalDocumentationLinkImpl; + public final fun getPackageListUrl ()Ljava/net/URL; + public final fun getUrl ()Ljava/net/URL; + public final fun setPackageListUrl (Ljava/net/URL;)V + public final fun setUrl (Ljava/net/URL;)V +} + +public class org/jetbrains/dokka/maven/HelpMojo : org/apache/maven/plugin/AbstractMojo { + public fun <init> ()V + public fun execute ()V +} + +public final class org/jetbrains/dokka/maven/MavenDokkaLogger : org/jetbrains/dokka/utilities/DokkaLogger { + public fun <init> (Lorg/apache/maven/plugin/logging/Log;)V + public fun debug (Ljava/lang/String;)V + public fun error (Ljava/lang/String;)V + public fun getErrorsCount ()I + public final fun getLog ()Lorg/apache/maven/plugin/logging/Log; + public fun getWarningsCount ()I + public fun info (Ljava/lang/String;)V + public fun progress (Ljava/lang/String;)V + public fun setErrorsCount (I)V + public fun setWarningsCount (I)V + public fun warn (Ljava/lang/String;)V +} + +public final class org/jetbrains/dokka/maven/PackageOptions : org/jetbrains/dokka/DokkaConfiguration$PackageOptions { + public fun <init> ()V + public fun getDocumentedVisibilities ()Ljava/util/Set; + public fun getIncludeNonPublic ()Z + public fun getMatchingRegex ()Ljava/lang/String; + public fun getReportUndocumented ()Ljava/lang/Boolean; + public fun getSkipDeprecated ()Z + public fun getSuppress ()Z + public fun setDocumentedVisibilities (Ljava/util/Set;)V + public fun setIncludeNonPublic (Z)V + public fun setMatchingRegex (Ljava/lang/String;)V + public fun setReportUndocumented (Z)V + public fun setSkipDeprecated (Z)V + public fun setSuppress (Z)V +} + +public final class org/jetbrains/dokka/maven/SourceLinkMapItem { + public fun <init> ()V + public final fun getLineSuffix ()Ljava/lang/String; + public final fun getPath ()Ljava/lang/String; + public final fun getUrl ()Ljava/lang/String; + public final fun setLineSuffix (Ljava/lang/String;)V + public final fun setPath (Ljava/lang/String;)V + public final fun setUrl (Ljava/lang/String;)V +} + diff --git a/dokka-runners/runner-maven-plugin/build.gradle.kts b/dokka-runners/runner-maven-plugin/build.gradle.kts new file mode 100644 index 00000000..f0de8dd5 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/build.gradle.kts @@ -0,0 +1,137 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import dokkabuild.overridePublicationArtifactId + +plugins { + id("dokkabuild.kotlin-jvm") + id("dokkabuild.publish-jvm") + id("dokkabuild.setup-maven-cli") + alias(libs.plugins.kotlinx.binaryCompatibilityValidator) +} + +overridePublicationArtifactId("dokka-maven-plugin") + +dependencies { + // this version is required, so that it will be available in the POM of plugin + implementation("org.jetbrains.dokka:dokka-core:$version") + + implementation(libs.apacheMaven.core) + implementation(libs.apacheMaven.pluginApi) + implementation(libs.apacheMaven.pluginAnnotations) + implementation(libs.apacheMaven.archiver) +} + +val mavenPluginTaskGroup = "maven plugin" + +val generatePom by tasks.registering(Sync::class) { + description = "Generate pom.xml for Maven Plugin Plugin" + group = mavenPluginTaskGroup + + inputs.property("dokka_version", project.version) + + val pomTemplateFile = layout.projectDirectory.file("pom.template.xml") + + val mavenVersion = mavenCliSetup.mavenVersion.orNull + val mavenPluginToolsVersion = mavenCliSetup.mavenPluginToolsVersion.orNull + + from(pomTemplateFile) { + rename { it.replace(".template.xml", ".xml") } + + expand( + "mavenVersion" to mavenVersion, + "dokka_version" to project.version, + "mavenPluginToolsVersion" to mavenPluginToolsVersion, + ) + } + + into(temporaryDir) +} + +val prepareHelpMojoDir by tasks.registering(Sync::class) { + description = "Prepare files for generating the Maven Plugin HelpMojo" + group = mavenPluginTaskGroup + + into(layout.buildDirectory.dir("maven-help-mojo")) + from(generatePom) +} + +val helpMojo by tasks.registering(Exec::class) { + description = "Generate the Maven Plugin HelpMojo" + group = mavenPluginTaskGroup + + dependsOn(tasks.installMavenBinary, prepareHelpMojoDir) + + workingDir(prepareHelpMojoDir.map { it.destinationDir }) + executable(mavenCliSetup.mvn.get()) + args("-e", "-B", "org.apache.maven.plugins:maven-plugin-plugin:helpmojo") + + outputs.dir(workingDir) +} + +val helpMojoSources by tasks.registering(Sync::class) { + description = "Sync the HelpMojo source files into a SourceSet SrcDir" + group = mavenPluginTaskGroup + from(helpMojo) { + eachFile { + // drop 2 leading directories + relativePath = RelativePath(true, *relativePath.segments.drop(2).toTypedArray()) + } + } + includeEmptyDirs = false + into(temporaryDir) + include("**/*.java") +} + +val helpMojoResources by tasks.registering(Sync::class) { + description = "Sync the HelpMojo resource files into a SourceSet SrcDir" + group = mavenPluginTaskGroup + from(helpMojo) + into(temporaryDir) + include("**/**") + exclude("**/*.java") +} + +sourceSets.main { + // use the generated HelpMojo as compilation input, so Gradle will automatically generate the mojo + java.srcDirs(helpMojoSources) + resources.srcDirs(helpMojoResources) +} + +val preparePluginDescriptorDir by tasks.registering(Sync::class) { + description = "Prepare files for generating the Maven Plugin descriptor" + group = mavenPluginTaskGroup + + into(layout.buildDirectory.dir("maven-plugin-descriptor")) + + from(tasks.compileKotlin) { into("classes/java/main") } + from(tasks.compileJava) { into("classes/java/main") } + from(helpMojoResources) +} + +val pluginDescriptor by tasks.registering(Exec::class) { + description = "Generate the Maven Plugin descriptor" + group = mavenPluginTaskGroup + + dependsOn(tasks.installMavenBinary, preparePluginDescriptorDir) + + workingDir(preparePluginDescriptorDir.map { it.destinationDir }) + executable(mavenCliSetup.mvn.get()) + args("-e", "-B", "org.apache.maven.plugins:maven-plugin-plugin:descriptor") + + outputs.dir("$workingDir/classes/java/main/META-INF/maven") +} + +tasks.jar { + metaInf { + from(pluginDescriptor) { + into("maven") + } + } + manifest { + attributes("Class-Path" to configurations.runtimeClasspath.map { configuration -> + configuration.resolve().joinToString(" ") { it.name } + }) + } +} diff --git a/dokka-runners/runner-maven-plugin/gradle.properties b/dokka-runners/runner-maven-plugin/gradle.properties new file mode 100644 index 00000000..5715ee14 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/gradle.properties @@ -0,0 +1,9 @@ +# +# Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# +group=org.jetbrains.dokka +version=1.9.20-SNAPSHOT + +org.jetbrains.dokka.javaToolchain.mainCompiler=8 +org.jetbrains.dokka.javaToolchain.testLauncher=8 +org.jetbrains.dokka.kotlinLanguageLevel=1.4 diff --git a/dokka-runners/runner-maven-plugin/pom.template.xml b/dokka-runners/runner-maven-plugin/pom.template.xml new file mode 100644 index 00000000..b4795112 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/pom.template.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>org.jetbrains.dokka</groupId> + <artifactId>dokka-maven-plugin</artifactId> + <version>${dokka_version}</version> + <packaging>maven-plugin</packaging> + <properties> + <maven.version>${mavenVersion}</maven.version> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-plugin-plugin</artifactId> + <version>${mavenPluginToolsVersion}</version> + <configuration> + <helpPackageName>org.jetbrains.dokka.maven</helpPackageName> + <skipErrorNoDescriptorsFound>true</skipErrorNoDescriptorsFound> + </configuration> + <executions> + <execution> + <id>default-descriptor</id> + <goals> + <goal>descriptor</goal> + </goals> + <phase>process-classes</phase> + </execution> + <execution> + <id>help-descriptor</id> + <goals> + <goal>helpmojo</goal> + </goals> + <phase>process-classes</phase> + </execution> + </executions> + </plugin> + </plugins> + <directory>./</directory> + <outputDirectory>./classes/java/main</outputDirectory> + </build> + <dependencies> + <dependency> + <groupId>org.apache.maven.plugin-tools</groupId> + <artifactId>maven-plugin-annotations</artifactId> + <version>${mavenPluginToolsVersion}</version> + <scope>provided</scope> + </dependency> + </dependencies> +</project> diff --git a/dokka-runners/runner-maven-plugin/settings.gradle.kts b/dokka-runners/runner-maven-plugin/settings.gradle.kts new file mode 100644 index 00000000..63bf3743 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/settings.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("UnstableApiUsage") + +rootProject.name = "runner-maven-plugin" + +pluginManagement { + includeBuild("../../build-logic") + + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/DokkaMojo.kt b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/DokkaMojo.kt new file mode 100644 index 00000000..d14fea9c --- /dev/null +++ b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/DokkaMojo.kt @@ -0,0 +1,615 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.maven + +import org.apache.maven.archiver.MavenArchiveConfiguration +import org.apache.maven.archiver.MavenArchiver +import org.apache.maven.artifact.DefaultArtifact +import org.apache.maven.artifact.handler.DefaultArtifactHandler +import org.apache.maven.artifact.resolver.ArtifactResolutionRequest +import org.apache.maven.artifact.resolver.ArtifactResolutionResult +import org.apache.maven.artifact.resolver.ResolutionErrorHandler +import org.apache.maven.execution.MavenSession +import org.apache.maven.model.Dependency +import org.apache.maven.plugin.AbstractMojo +import org.apache.maven.plugin.MojoExecutionException +import org.apache.maven.plugins.annotations.* +import org.apache.maven.project.MavenProject +import org.apache.maven.project.MavenProjectHelper +import org.apache.maven.repository.RepositorySystem +import org.codehaus.plexus.archiver.Archiver +import org.codehaus.plexus.archiver.jar.JarArchiver +import org.codehaus.plexus.archiver.util.DefaultFileSet +import org.codehaus.plexus.util.xml.Xpp3Dom +import org.jetbrains.dokka.* +import org.jetbrains.dokka.DokkaConfiguration.ExternalDocumentationLink +import java.io.File +import java.net.URL + +public abstract class AbstractDokkaMojo( + private val defaultDokkaPlugins: List<Dependency> +) : AbstractMojo() { + + @Parameter(defaultValue = "\${project}", readonly = true, required = true) + protected var mavenProject: MavenProject? = null + + /** + * The current build session instance. This is used for + * dependency resolver API calls via repositorySystem. + */ + @Parameter(defaultValue = "\${session}", required = true, readonly = true) + protected var session: MavenSession? = null + + @Component + private var repositorySystem: RepositorySystem? = null + + @Component + private var resolutionErrorHandler: ResolutionErrorHandler? = null + + @Parameter(defaultValue = "JVM") + public var displayName: String = "JVM" + + @Parameter + public var sourceSetName: String = "JVM" + + /** + * Source code roots to be analyzed and documented. + * Accepts directories and individual `.kt` / `.java` files. + * + * Default is `{project.compileSourceRoots}`. + */ + @Parameter(required = true, defaultValue = "\${project.compileSourceRoots}") + public var sourceDirectories: List<String> = emptyList() + + /** + * List of directories or files that contain sample functions which are referenced via + * [@sample](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) KDoc tag. + */ + @Parameter + public var samples: List<String> = emptyList() + + /** + * List of Markdown files that contain + * [module and package documentation](https://kotlinlang.org/docs/dokka-module-and-package-docs.html). + * + * Contents of specified files will be parsed and embedded into documentation as module and package descriptions. + * + * Example of such a file: + * + * ```markdown + * # Module kotlin-demo + * + * The module shows the Dokka usage. + * + * # Package org.jetbrains.kotlin.demo + * + * Contains assorted useful stuff. + * + * ## Level 2 heading + * + * Text after this heading is also part of documentation for `org.jetbrains.kotlin.demo` + * + * # Package org.jetbrains.kotlin.demo2 + * + * Useful stuff in another package. + * ``` + */ + @Parameter + public var includes: List<String> = emptyList() + + /** + * Classpath for analysis and interactive samples. + * + * Useful if some types that come from dependencies are not resolved/picked up automatically. + * Property accepts both `.jar` and `.klib` files. + * + * Default is `{project.compileClasspathElements}`. + */ + @Parameter(required = true, defaultValue = "\${project.compileClasspathElements}") + public var classpath: List<String> = emptyList() + + /** + * Specifies the location of the project source code on the Web. If provided, Dokka generates + * "source" links for each declaration. See [SourceLinkMapItem] for more details. + */ + @Parameter + public var sourceLinks: List<SourceLinkMapItem> = emptyList() + + /** + * Display name used to refer to the project/module. Used for ToC, navigation, logging, etc. + * + * Default is `{project.artifactId}`. + */ + @Parameter(required = true, defaultValue = "\${project.artifactId}") + public var moduleName: String = "" + + /** + * Whether to skip documentation generation. + * + * Default is `false`. + */ + @Parameter(required = false, defaultValue = "false") + public var skip: Boolean = false + + /** + * JDK version to use when generating external documentation links for Java types. + * + * For instance, if you use [java.util.UUID] from JDK in some public declaration signature, + * and this property is set to `8`, Dokka will generate an external documentation link + * to [JDK 8 Javadocs](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) for it. + * + * Default is JDK 8. + */ + @Parameter(required = false, defaultValue = "${DokkaDefaults.jdkVersion}") + public var jdkVersion: Int = DokkaDefaults.jdkVersion + + /** + * Whether to document declarations annotated with [Deprecated]. + * + * Can be overridden on package level by setting [PackageOptions.skipDeprecated]. + * + * Default is `false`. + */ + @Parameter + public var skipDeprecated: Boolean = DokkaDefaults.skipDeprecated + + /** + * Whether to skip packages that contain no visible declarations after + * various filters have been applied. + * + * For instance, if [skipDeprecated] is set to `true` and your package contains only + * deprecated declarations, it will be considered to be empty. + * + * Default is `true`. + */ + @Parameter + public var skipEmptyPackages: Boolean = DokkaDefaults.skipEmptyPackages + + /** + * Whether to emit warnings about visible undocumented declarations, that is declarations without KDocs + * after they have been filtered by [documentedVisibilities]. + * + * This setting works well with [failOnWarning]. + * + * Can be overridden for a specific package by setting [PackageOptions.reportUndocumented]. + * + * Default is `false`. + */ + @Parameter + public var reportUndocumented: Boolean = DokkaDefaults.reportUndocumented + + /** + * Allows to customize documentation generation options on a per-package basis. + * + * @see PackageOptions for details + */ + @Parameter + public var perPackageOptions: List<PackageOptions> = emptyList() + + /** + * Allows linking to Dokka/Javadoc documentation of the project's dependencies. + * + * @see ExternalDocumentationLinkBuilder for details + */ + @Parameter + public var externalDocumentationLinks: List<ExternalDocumentationLinkBuilder> = emptyList() + + /** + * Whether to generate external documentation links that lead to API reference + * documentation for Kotlin's standard library when declarations from it are used. + * + * Default is `false`, meaning links will be generated. + */ + @Parameter(defaultValue = "${DokkaDefaults.noStdlibLink}") + public var noStdlibLink: Boolean = DokkaDefaults.noStdlibLink + + /** + * Whether to generate external documentation links to JDK's Javadocs + * when declarations from it are used. + * + * The version of JDK Javadocs is determined by [jdkVersion] property. + * + * Default is `false`, meaning links will be generated. + */ + @Parameter(defaultValue = "${DokkaDefaults.noJdkLink}") + public var noJdkLink: Boolean = DokkaDefaults.noJdkLink + + /** + * Whether to resolve remote files/links over network. + * + * This includes package-lists used for generating external documentation links: + * for instance, to make classes from standard library clickable. + * + * Setting this to `true` can significantly speed up build times in certain cases, + * but can also worsen documentation quality and user experience, for instance by + * not resolving some dependency's class/member links. + * + * When using offline mode, you can cache fetched files locally and provide them to + * Dokka as local paths. For instance, see [ExternalDocumentationLinkBuilder]. + * + * Default is `false`. + */ + @Parameter(defaultValue = "${DokkaDefaults.offlineMode}") + public var offlineMode: Boolean = DokkaDefaults.offlineMode + + /** + * [Kotlin language version](https://kotlinlang.org/docs/compatibility-modes.html) + * used for setting up analysis and [@sample](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) + * environment. + * + * By default, the latest language version available to Dokka's embedded compiler will be used. + */ + @Parameter + public var languageVersion: String? = null + + /** + * [Kotlin API version](https://kotlinlang.org/docs/compatibility-modes.html) + * used for setting up analysis and [@sample](https://kotlinlang.org/docs/kotlin-doc.html#sample-identifier) + * environment. + * + * By default, it will be deduced from [languageVersion]. + */ + @Parameter + public var apiVersion: String? = null + + /** + * Directories or individual files that should be suppressed, meaning declarations from them + * will be not documented. + */ + @Parameter + public var suppressedFiles: List<String> = emptyList() + + /** + * Set of visibility modifiers that should be documented. + * + * This can be used if you want to document protected/internal/private declarations, + * as well as if you want to exclude public declarations and only document internal API. + * + * Can be configured on per-package basis, see [PackageOptions.documentedVisibilities]. + * + * Default is [DokkaConfiguration.Visibility.PUBLIC]. + */ + @Parameter(property = "visibility") + public var documentedVisibilities: Set<DokkaConfiguration.Visibility> = DokkaDefaults.documentedVisibilities + // hack to set the default value for lists, didn't find any other safe way + // maven seems to overwrite Kotlin's default initialization value, so it doesn't matter what you put there + get() = field.ifEmpty { DokkaDefaults.documentedVisibilities } + + /** + * Whether to fail documentation generation if Dokka has emitted a warning or an error. + * Will wait until all errors and warnings have been emitted first. + * + * This setting works well with [reportUndocumented] + * + * Default is `false`. + */ + @Parameter + public var failOnWarning: Boolean = DokkaDefaults.failOnWarning + + /** + * Whether to suppress obvious functions. + * + * A function is considered to be obvious if it is: + * - Inherited from `kotlin.Any`, `Kotlin.Enum`, `java.lang.Object` or `java.lang.Enum`, + * such as `equals`, `hashCode`, `toString`. + * - Synthetic (generated by the compiler) and does not have any documentation, such as + * `dataClass.componentN` or `dataClass.copy`. + * + * Default is `true` + */ + @Parameter(defaultValue = "${DokkaDefaults.suppressObviousFunctions}") + public var suppressObviousFunctions: Boolean = DokkaDefaults.suppressObviousFunctions + + /** + * Whether to suppress inherited members that aren't explicitly overridden in a given class. + * + * Note: this can suppress functions such as `equals`/`hashCode`/`toString`, but cannot suppress + * synthetic functions such as `dataClass.componentN` and `dataClass.copy`. Use [suppressObviousFunctions] + * for that. + * + * Default is `false`. + */ + @Parameter(defaultValue = "${DokkaDefaults.suppressInheritedMembers}") + public var suppressInheritedMembers: Boolean = DokkaDefaults.suppressInheritedMembers + + /** + * Dokka plugins to be using during documentation generation. + * + * Example: + * + * ```xml + * <dokkaPlugins> + * <plugin> + * <groupId>org.jetbrains.dokka</groupId> + * <artifactId>gfm-plugin</artifactId> + * <version>1.7.20</version> + * </plugin> + * </dokkaPlugins> + * ``` + */ + @Parameter + public var dokkaPlugins: List<Dependency> = emptyList() + get() = field + defaultDokkaPlugins + + @Parameter + public var cacheRoot: String? = null + + @Parameter + public var platform: String = "" + + /** + * Deprecated. Use [documentedVisibilities] instead. + */ + @Parameter + public var includeNonPublic: Boolean = DokkaDefaults.includeNonPublic + + protected abstract fun getOutDir(): String + + override fun execute() { + if (skip) { + log.info("Dokka skip parameter is true so no dokka output will be produced") + return + } + + sourceLinks.forEach { + if (it.path.contains("\\")) { + throw MojoExecutionException("Incorrect path property, only Unix based path allowed.") + } + } + + if (moduleName.contains(',')) { + // To figure out why this is needed and if it is still relevant, see the comment here: + // https://github.com/Kotlin/dokka/issues/3011#issuecomment-1568620493 + throw IllegalArgumentException("Module name cannot contain commas as it is used internally as a delimiter.") + } + + fun defaultLinks(config: DokkaSourceSetImpl): Set<ExternalDocumentationLinkImpl> { + val links = mutableSetOf<ExternalDocumentationLinkImpl>() + if (!config.noJdkLink) + links += ExternalDocumentationLink.jdk(jdkVersion) + + if (!config.noStdlibLink) + links += ExternalDocumentationLink.kotlinStdlib() + return links + } + + val sourceSet = DokkaSourceSetImpl( + displayName = displayName, + sourceSetID = DokkaSourceSetID(moduleName, sourceSetName), + classpath = classpath.map(::File), + sourceRoots = sourceDirectories.map(::File).toSet(), + dependentSourceSets = emptySet(), + samples = samples.map(::File).toSet(), + includes = includes.map(::File).toSet(), + includeNonPublic = includeNonPublic, + documentedVisibilities = documentedVisibilities, + reportUndocumented = reportUndocumented, + skipEmptyPackages = skipEmptyPackages, + skipDeprecated = skipDeprecated, + jdkVersion = jdkVersion, + sourceLinks = sourceLinks.map { SourceLinkDefinitionImpl(File(it.path).canonicalPath, URL(it.url), it.lineSuffix) }.toSet(), + perPackageOptions = perPackageOptions.map { + @Suppress("DEPRECATION") // for includeNonPublic, preserve backwards compatibility + PackageOptionsImpl( + matchingRegex = it.matchingRegex, + includeNonPublic = it.includeNonPublic, + documentedVisibilities = it.documentedVisibilities, + reportUndocumented = it.reportUndocumented, + skipDeprecated = it.skipDeprecated, + suppress = it.suppress + ) + }, + externalDocumentationLinks = externalDocumentationLinks.map { it.build() }.toSet(), + languageVersion = languageVersion, + apiVersion = apiVersion, + noStdlibLink = noStdlibLink, + noJdkLink = noJdkLink, + suppressedFiles = suppressedFiles.map(::File).toSet(), + analysisPlatform = if (platform.isNotEmpty()) Platform.fromString(platform) else Platform.DEFAULT, + ).let { + it.copy( + externalDocumentationLinks = defaultLinks(it) + it.externalDocumentationLinks + ) + } + + val logger = MavenDokkaLogger(log) + + val pluginsConfiguration = + (mavenProject?.getPlugin("org.jetbrains.dokka:dokka-maven-plugin")?.configuration as? Xpp3Dom) + ?.getChild("pluginsConfiguration")?.children?.map { + PluginConfigurationImpl( + it.name, + DokkaConfiguration.SerializationFormat.XML, + it.toString() + ) + }.orEmpty() + + val configuration = DokkaConfigurationImpl( + moduleName = moduleName, + outputDir = File(getOutDir()), + offlineMode = offlineMode, + cacheRoot = cacheRoot?.let(::File), + sourceSets = listOf(sourceSet), + // TODO [beresnev] analysis switcher + pluginsClasspath = getArtifactByMaven("org.jetbrains.dokka", "analysis-kotlin-descriptors", dokkaVersion) + + getArtifactByMaven("org.jetbrains.dokka", "dokka-base", dokkaVersion) + + dokkaPlugins.map { getArtifactByMaven(it.groupId, it.artifactId, it.version ?: dokkaVersion) } + .flatten(), + pluginsConfiguration = pluginsConfiguration.toMutableList(), + modules = emptyList(), + failOnWarning = failOnWarning, + suppressObviousFunctions = suppressObviousFunctions, + suppressInheritedMembers = suppressInheritedMembers, + // looks like maven has different life cycle compared to gradle, + // so finalizing coroutines after each module pass causes an error. + // see https://github.com/Kotlin/dokka/issues/2457 + finalizeCoroutines = false, + ) + + val gen = DokkaGenerator(configuration, logger) + + gen.generate() + } + + private fun getArtifactByMaven( + groupId: String, + artifactId: String, + version: String + ): List<File> { + + val request = ArtifactResolutionRequest().apply { + isResolveRoot = true + isResolveTransitively = true + localRepository = session!!.localRepository + remoteRepositories = mavenProject!!.pluginArtifactRepositories + isOffline = session!!.isOffline + isForceUpdate = session!!.request.isUpdateSnapshots + servers = session!!.request.servers + mirrors = session!!.request.mirrors + proxies = session!!.request.proxies + artifact = DefaultArtifact( + groupId, artifactId, version, "compile", "jar", null, + DefaultArtifactHandler("jar") + ) + } + + log.debug("Resolving $groupId:$artifactId:$version ...") + + val result: ArtifactResolutionResult = repositorySystem!!.resolve(request) + resolutionErrorHandler!!.throwErrors(request, result) + return result.artifacts.map { it.file } + } + + private val dokkaVersion: String by lazy { + mavenProject?.pluginArtifacts?.firstOrNull { it.groupId == "org.jetbrains.dokka" && it.artifactId == "dokka-maven-plugin" }?.version + ?: throw IllegalStateException("Not found dokka plugin") + } +} + +@Mojo( + name = "dokka", + defaultPhase = LifecyclePhase.PRE_SITE, + threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE, + requiresProject = true +) +public class DokkaMojo : AbstractDokkaMojo(emptyList()) { + + /** + * Directory to which documentation will be generated. + * + * Default is `{project.basedir}/target/dokka`. + */ + @Parameter(required = true, defaultValue = "\${project.basedir}/target/dokka") + public var outputDir: String = "" + + override fun getOutDir(): String = outputDir +} + +@Mojo( + name = "javadoc", + defaultPhase = LifecyclePhase.PRE_SITE, + threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE, + requiresProject = true +) +public class DokkaJavadocMojo : AbstractDokkaMojo(listOf(javadocDependency)) { + + /** + * Directory to which documentation will be generated. + * + * Default is `{project.basedir}/target/dokkaJavadoc`. + */ + @Parameter(required = true, defaultValue = "\${project.basedir}/target/dokkaJavadoc") + public var outputDir: String = "" + + override fun getOutDir(): String = outputDir +} + +@Mojo( + name = "javadocJar", + defaultPhase = LifecyclePhase.PRE_SITE, + threadSafe = true, + requiresDependencyResolution = ResolutionScope.COMPILE, + requiresProject = true +) +public class DokkaJavadocJarMojo : AbstractDokkaMojo(listOf(javadocDependency)) { + + /** + * Directory to which documentation jar will be generated. + * + * Default is `{project.basedir}/target/dokkaJavadocJar`. + */ + @Parameter(required = true, defaultValue = "\${project.basedir}/target/dokkaJavadocJar") + public var outputDir: String = "" + + /** + * Specifies the directory where the generated jar file will be put. + */ + @Parameter(property = "project.build.directory") + private var jarOutputDirectory: String? = null + + /** + * Specifies the filename that will be used for the generated jar file. Please note that `-javadoc` + * or `-test-javadoc` will be appended to the file name. + */ + @Parameter(property = "project.build.finalName") + private var finalName: String? = null + + /** + * Specifies whether to attach the generated artifact to the project helper. + */ + @Parameter(property = "attach", defaultValue = "true") + private val attach: Boolean = false + + /** + * The archive configuration to use. + * See [Maven Archiver Reference](https://maven.apache.org/shared/maven-archiver/index.html) + */ + @Parameter + private val archive = MavenArchiveConfiguration() + + @Parameter(property = "maven.javadoc.classifier", defaultValue = "javadoc", required = true) + private var classifier: String? = null + + @Component + private var projectHelper: MavenProjectHelper? = null + + @Component(role = Archiver::class, hint = "jar") + private var jarArchiver: JarArchiver? = null + + override fun getOutDir(): String = outputDir + + override fun execute() { + super.execute() + if (!File(outputDir).exists()) { + log.warn("No javadoc generated so no javadoc jar will be generated") + return + } + val outputFile = generateArchive("$finalName-$classifier.jar") + if (attach) { + projectHelper?.attachArtifact(mavenProject, "javadoc", classifier, outputFile) + } + } + + private fun generateArchive(jarFileName: String): File { + val javadocJar = File(jarOutputDirectory, jarFileName) + + val archiver = MavenArchiver() + archiver.archiver = jarArchiver + archiver.setOutputFile(javadocJar) + archiver.archiver.addFileSet(DefaultFileSet().apply { directory = File(outputDir) }) + + archive.isAddMavenDescriptor = false + archiver.createArchive(session, mavenProject, archive) + + return javadocJar + } +} + +private val javadocDependency = Dependency().apply { + groupId = "org.jetbrains.dokka" + artifactId = "javadoc-plugin" +} diff --git a/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/ExternalDocumentationLinkBuilder.kt b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/ExternalDocumentationLinkBuilder.kt new file mode 100644 index 00000000..5eb4f269 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/ExternalDocumentationLinkBuilder.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.maven + +import org.apache.maven.plugins.annotations.Parameter +import org.jetbrains.dokka.ExternalDocumentationLink +import org.jetbrains.dokka.ExternalDocumentationLinkImpl +import java.net.URL + +/** + * Configuration block that allows creating links leading to externally hosted + * documentation of your dependencies. + * + * For instance, if you are using types from `kotlinx.serialization`, by default + * they will be unclickable in your documentation, as if unresolved. However, + * since API reference for `kotlinx.serialization` is also built by Dokka and is + * [published on kotlinlang.org](https://kotlinlang.org/api/kotlinx.serialization/), + * you can configure external documentation links for it, allowing Dokka to generate + * documentation links for used types, making them clickable and appear resolved. + * + * Example: + * + * ```xml + * <externalDocumentationLinks> + * <link> + * <url>https://kotlinlang.org/api/latest/jvm/stdlib/</url> + * <packageListUrl>file:/${project.basedir}/stdlib.package.list</packageListUrl> + * </link> + * </externalDocumentationLinks> + * ``` + */ +public class ExternalDocumentationLinkBuilder { + + /** + * Root URL of documentation to link with. **Must** contain a trailing slash. + * + * Dokka will do its best to automatically find `package-list` for the given URL, and link + * declarations together. + * + * It automatic resolution fails or if you want to use locally cached files instead, + * consider providing [packageListUrl]. + * + * Example: + * + * ```xml + * <url>https://kotlinlang.org/api/latest/jvm/stdlib/</url> + * ``` + */ + @Parameter(name = "url", required = true) + public var url: URL? = null + + /** + * Specifies the exact location of a `package-list` instead of relying on Dokka + * automatically resolving it. Can also be a locally cached file to avoid network calls. + * + * Example: + * + * ```xml + * <packageListUrl>file:/${project.basedir}/stdlib.package.list</packageListUrl> + * ``` + */ + @Parameter(name = "packageListUrl", required = true) + public var packageListUrl: URL? = null + + public fun build(): ExternalDocumentationLinkImpl = ExternalDocumentationLink(url, packageListUrl) +} diff --git a/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/MavenDokkaLogger.kt b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/MavenDokkaLogger.kt new file mode 100644 index 00000000..1bc39d10 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/MavenDokkaLogger.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.maven + +import org.apache.maven.plugin.logging.Log +import org.jetbrains.dokka.utilities.DokkaLogger +import java.util.concurrent.atomic.AtomicInteger + +public class MavenDokkaLogger( + public val log: Log +) : DokkaLogger { + private val warningsCounter = AtomicInteger() + private val errorsCounter = AtomicInteger() + + override var warningsCount: Int + get() = warningsCounter.get() + set(value) = warningsCounter.set(value) + + override var errorsCount: Int + get() = errorsCounter.get() + set(value) = errorsCounter.set(value) + + override fun debug(message: String) { + log.debug(message) + } + + override fun info(message: String) { + log.info(message) + } + + override fun progress(message: String) { + log.info(message) + } + + override fun warn(message: String) { + this.log.warn(message).also { warningsCounter.incrementAndGet() } + } + + override fun error(message: String) { + log.error(message).also { errorsCounter.incrementAndGet() } + } +} diff --git a/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/PackageOptions.kt b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/PackageOptions.kt new file mode 100644 index 00000000..875fb047 --- /dev/null +++ b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/PackageOptions.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.maven + +import org.apache.maven.plugins.annotations.Parameter +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaDefaults + +/** + * Configuration block that allows setting some options for specific packages + * matched by [matchingRegex]. + * + * Example: + * + * ```xml + * <configuration> + * <perPackageOptions> + * <packageOptions> + * <matchingRegex>.*api.*</matchingRegex> + * <suppress>false</suppress> + * <reportUndocumented>false</reportUndocumented> + * <skipDeprecated>false</skipDeprecated> + * <documentedVisibilities> + * <visibility>PUBLIC</visibility> + * <visibility>PROTECTED</visibility> + * </documentedVisibilities> + * </packageOptions> + * </perPackageOptions> + * </configuration> + * ``` + */ +public class PackageOptions : DokkaConfiguration.PackageOptions { + + /** + * Regular expression that is used to match the package. + * + * If multiple packages match the same `matchingRegex`, the longest `matchingRegex` will be used. + * + * Default is any string: `.*`. + */ + @Parameter + override var matchingRegex: String = ".*" + + /** + * Whether this package should be skipped when generating documentation. + * + * Default is `false`. + */ + @Parameter + override var suppress: Boolean = DokkaDefaults.suppress + + /** + * List of visibility modifiers that should be documented. + * + * This can be used if you want to document protected/internal/private declarations within a + * specific package, as well as if you want to exclude public declarations and only document internal API. + * + * Default is [DokkaConfiguration.Visibility.PUBLIC]. + */ + @Parameter(property = "visibility") + override var documentedVisibilities: Set<DokkaConfiguration.Visibility> = DokkaDefaults.documentedVisibilities + + /** + * Whether to document declarations annotated with [Deprecated]. + * + * Can be set on project level with [AbstractDokkaMojo.skipDeprecated]. + * + * Default is `false`. + */ + @Parameter + override var skipDeprecated: Boolean = DokkaDefaults.skipDeprecated + + /** + * Whether to emit warnings about visible undocumented declarations, that is declarations from + * this package and without KDocs, after they have been filtered by [documentedVisibilities]. + * + * This setting works well with [AbstractDokkaMojo.failOnWarning]. + * + * Default is `false`. + */ + @Parameter + override var reportUndocumented: Boolean = DokkaDefaults.reportUndocumented + + @Parameter + @Deprecated("Use [documentedVisibilities] property for a more flexible control over documented visibilities") + override var includeNonPublic: Boolean = DokkaDefaults.includeNonPublic +} diff --git a/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/SourceLinkMapItem.kt b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/SourceLinkMapItem.kt new file mode 100644 index 00000000..ee2354aa --- /dev/null +++ b/dokka-runners/runner-maven-plugin/src/main/kotlin/org/jetbrains/dokka/maven/SourceLinkMapItem.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.maven + +import org.apache.maven.plugins.annotations.Parameter + +/** + * Configuration block that allows adding a `source` link to each signature + * which leads to [path] with a specific line number (configurable by setting [lineSuffix]), + * letting documentation readers find source code for each declaration. + * + * Example: + * + * ```xml + * <sourceLinks> + * <link> + * <path>${project.basedir}/src</path> + * <url>https://github.com/kotlin/dokka/tree/master/src</url> + * <lineSuffix>#L</lineSuffix> + * </link> + * </sourceLinks> + * ``` + */ +public class SourceLinkMapItem { + + /** + * Path to the local source directory. The path must be relative to the root of current project. + * + * Example: + * + * ```xml + * <path>${project.basedir}/src</path> + * ``` + */ + @Parameter(name = "path", required = true) + public var path: String = "" + + /** + * URL of source code hosting service that can be accessed by documentation readers, + * like GitHub, GitLab, Bitbucket, etc. This URL will be used to generate + * source code links of declarations. + * + * Example: + * + * ```xml + * <url>https://github.com/username/projectname/tree/master/src</url> + * ``` + */ + @Parameter(name = "url", required = true) + public var url: String = "" + + /** + * Suffix used to append source code line number to the URL. This will help readers navigate + * not only to the file, but to the specific line number of the declaration. + * + * The number itself will be appended to the specified suffix. For instance, + * if this property is set to `#L` and the line number is 10, resulting URL suffix + * will be `#L10` + * + * Suffixes used by popular services: + * - GitHub: `#L` + * - GitLab: `#L` + * - Bitbucket: `#lines-` + */ + @Parameter(name = "lineSuffix") + public var lineSuffix: String? = null +} |