/* * 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 ) : 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 = 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 = 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 = 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 = 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 = 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 = emptyList() /** * Allows linking to Dokka/Javadoc documentation of the project's dependencies. * * @see ExternalDocumentationLinkBuilder for details */ @Parameter public var externalDocumentationLinks: List = 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 = 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 = 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 * * * org.jetbrains.dokka * gfm-plugin * 1.7.20 * * * ``` */ @Parameter public var dokkaPlugins: List = 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 { val links = mutableSetOf() 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(it.path, 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 { 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" }