diff --git a/build-logic/src/main/kotlin/dokkabuild/utils/SystemPropertyAdder.kt b/build-logic/src/main/kotlin/dokkabuild/utils/SystemPropertyAdder.kt index 9592fe0584..8e33ae7e5b 100644 --- a/build-logic/src/main/kotlin/dokkabuild/utils/SystemPropertyAdder.kt +++ b/build-logic/src/main/kotlin/dokkabuild/utils/SystemPropertyAdder.kt @@ -4,8 +4,10 @@ package dokkabuild.utils import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection import org.gradle.api.file.RegularFile +import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Provider import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskInputFilePropertyBuilder @@ -14,6 +16,7 @@ import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.findByType import org.gradle.process.CommandLineArgumentProvider +import java.io.File import javax.inject.Inject @@ -34,19 +37,33 @@ val Test.systemProperty: SystemPropertyAdder abstract class SystemPropertyAdder @Inject internal constructor( private val task: Test, ) { + private val objects: ObjectFactory = task.project.objects + fun inputDirectory( key: String, - value: Directory, + value: DirectoryProperty, ): TaskInputFilePropertyBuilder { task.jvmArgumentProviders.add( SystemPropertyArgumentProvider(key, value) { - it.asFile.invariantSeparatorsPath + it.get().asFile.invariantSeparatorsPath } ) return task.inputs.dir(value) .withPropertyName("SystemProperty input directory $key") } + fun inputDirectory( + key: String, + value: Provider, + ): TaskInputFilePropertyBuilder = + inputDirectory(key, objects.directoryProperty().fileProvider(value)) + + fun inputDirectory( + key: String, + value: Directory, + ): TaskInputFilePropertyBuilder = + inputDirectory(key, objects.directoryProperty().apply { set(value) }) + fun inputFile( key: String, file: RegularFile, diff --git a/build-logic/src/main/kotlin/dokkabuild/utils/downloadKotlinStdlibJvmSources.kt b/build-logic/src/main/kotlin/dokkabuild/utils/downloadKotlinStdlibJvmSources.kt new file mode 100644 index 0000000000..ec9df83f26 --- /dev/null +++ b/build-logic/src/main/kotlin/dokkabuild/utils/downloadKotlinStdlibJvmSources.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package dokkabuild.utils + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE +import org.gradle.api.attributes.Category.DOCUMENTATION +import org.gradle.api.attributes.DocsType.DOCS_TYPE_ATTRIBUTE +import org.gradle.api.attributes.DocsType.SOURCES +import org.gradle.api.attributes.Usage.JAVA_RUNTIME +import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE +import org.gradle.api.file.ArchiveOperations +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.Sync +import org.gradle.kotlin.dsl.* +import org.gradle.kotlin.dsl.support.serviceOf +import java.io.File + + +/** + * Download and unpack Kotlin stdlib JVM source code. + * + * @returns the directory containing the unpacked sources. + */ +fun downloadKotlinStdlibJvmSources(project: Project): Provider { + val kotlinStdlibJvmSources: Configuration by project.configurations.creating { + description = "kotlin-stdlib JVM source code." + declarable() + withDependencies { + add(project.dependencies.run { create(kotlin("stdlib")) }) + } + } + + val kotlinStdlibJvmSourcesResolver: Configuration by project.configurations.creating { + description = "Resolver for ${kotlinStdlibJvmSources.name}." + resolvable() + isTransitive = false + extendsFrom(kotlinStdlibJvmSources) + attributes { + attribute(USAGE_ATTRIBUTE, project.objects.named(JAVA_RUNTIME)) + attribute(CATEGORY_ATTRIBUTE, project.objects.named(DOCUMENTATION)) + attribute(DOCS_TYPE_ATTRIBUTE, project.objects.named(SOURCES)) + } + } + + val downloadKotlinStdlibSources by project.tasks.registering(Sync::class) { + description = "Download and unpacks kotlin-stdlib JVM source code." + val archives = project.serviceOf() + val unpackedJvmSources = kotlinStdlibJvmSourcesResolver.incoming.artifacts.resolvedArtifacts.map { artifacts -> + artifacts.map { + archives.zipTree(it.file) + } + } + from(unpackedJvmSources) + into(temporaryDir) + } + + return downloadKotlinStdlibSources.map { it.destinationDir } +} diff --git a/dokka-integration-tests/gradle/src/test/kotlin/StdLibDocumentationIntegrationTest.kt b/dokka-integration-tests/gradle/src/test/kotlin/StdLibDocumentationIntegrationTest.kt deleted file mode 100644 index eb5726bb7c..0000000000 --- a/dokka-integration-tests/gradle/src/test/kotlin/StdLibDocumentationIntegrationTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.it - -import java.net.URL -import kotlin.test.Test - -class StdLibDocumentationIntegrationTest { - - /** - * Documentation for Enum's synthetic values() and valueOf() functions is only present in source code, - * but not present in the descriptors. However, Dokka needs to generate documentation for these functions, - * so it ships with hardcoded kdoc templates. - * - * This test exists to make sure documentation for these hardcoded synthetic functions does not change, - * and fails if it does, indicating that it needs to be updated. - */ - @Test - fun shouldAssertEnumDocumentationHasNotChanged() { - val sourcesLink = "https://raw.githubusercontent.com/JetBrains/kotlin/master/libraries/stdlib/src/kotlin/Enum.kt" - val sources = URL(sourcesLink).readText() - - val expectedValuesDoc = - " /**\n" + - " * Returns an array containing the constants of this enum type, in the order they're declared.\n" + - " * This method may be used to iterate over the constants.\n" + - " * @values\n" + - " */" - check(sources.contains(expectedValuesDoc)) - - val expectedValueOfDoc = - " /**\n" + - " * Returns the enum constant of this type with the specified name. The string must match exactly " + - "an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)\n" + - " * @throws IllegalArgumentException if this enum type has no constant with the specified name\n" + - " * @valueOf\n" + - " */" - check(sources.contains(expectedValueOfDoc)) - } -} diff --git a/dokka-subprojects/plugin-base/build.gradle.kts b/dokka-subprojects/plugin-base/build.gradle.kts index 8246c7594f..136bcbfac8 100644 --- a/dokka-subprojects/plugin-base/build.gradle.kts +++ b/dokka-subprojects/plugin-base/build.gradle.kts @@ -3,6 +3,9 @@ */ import dokkabuild.overridePublicationArtifactId +import dokkabuild.utils.downloadKotlinStdlibJvmSources +import dokkabuild.utils.systemProperty +import org.gradle.api.tasks.PathSensitivity.RELATIVE plugins { id("dokkabuild.kotlin-jvm") @@ -79,3 +82,12 @@ sourceSets.main { tasks.test { maxHeapSize = "4G" } + +//region Download and unpack kotlin-stdlib, so EnumTemplatesTest can test synthetic enum functions. +val kotlinStdlibSourcesDir = downloadKotlinStdlibJvmSources(project) +tasks.withType().configureEach { + systemProperty + .inputDirectory("kotlinStdlibSourcesDir", kotlinStdlibSourcesDir) + .withPathSensitivity(RELATIVE) +} +//endregion diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/EnumTemplatesTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/EnumTemplatesTest.kt new file mode 100644 index 0000000000..eb95cfc2c7 --- /dev/null +++ b/dokka-subprojects/plugin-base/src/test/kotlin/EnumTemplatesTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.assertAll +import utils.assertContains +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Documentation for Enum's synthetic `values()` and `valueOf()` functions is only present in source code, + * but not present in the descriptors. However, Dokka needs to generate documentation for these functions, + * so it ships with hardcoded kdoc templates. + * + * This test exists to make sure the kdoc from Kotlin stdlib for the hardcoded synthetic enum functions + * matches (sometimes approximately) Dokka's templates. + */ +class EnumTemplatesTest { + + @Test + fun enumValueOf() { + listOf( + "Returns the enum constant of this type with the specified name.", + "(Extraneous whitespace characters are not permitted.)", + ).forEach { line -> + assertSubstringIsInFiles(line, enumValueOfTemplate, actualStdlibEnumKt) + } + + "The string must match exactly an identifier used to declare an enum constant in this type.".let { line -> + assertSubstringIsInFiles(line, actualStdlibEnumKt) + assertSubstringIsInFiles( + // The Dokka template has a newline, but otherwise the text is the same. + line.replace("identifier used to declare", "identifier used\nto declare"), + enumValueOfTemplate, + ) + } + + "@throws IllegalArgumentException if this enum type has no constant with the specified name".let { line -> + assertSubstringIsInFiles(line, actualStdlibEnumKt) + assertSubstringIsInFiles( + // The Dokka template uses the FQN of IllegalArgumentException + line.replace("IllegalArgumentException", "kotlin.IllegalArgumentException"), + enumValueOfTemplate, + ) + } + } + + @Test + fun enumValues() { + listOf( + "Returns an array containing the constants of this enum type, in the order they're declared.", + "This method may be used to iterate over the constants.", + ).forEach { line -> + assertSubstringIsInFiles(line, enumValuesTemplate, actualStdlibEnumKt) + } + } + + /** + * This test is disabled because the `Enum.entries` does not have accessible documentation. + * + * See https://youtrack.jetbrains.com/issue/KTIJ-23569/Provide-quick-documentation-for-Enum.entries + */ + @Test + @Disabled("Kotlin stdlib does not have kdoc for Enum.entries") + fun enumEntries() { + listOf( + "Returns a representation of an immutable list of all enum entries, in the order they're declared.", + "This method may be used to iterate over the enum entries.", + ).forEach { line -> + assertSubstringIsInFiles(line, enumEntriesTemplate, actualStdlibEnumKt) + } + } + + companion object { + + /** + * Assert that all [files] exist, are files, and contain [substring]. + */ + private fun assertSubstringIsInFiles(substring: String, vararg files: Path) { + assertAll(files.map { file -> + { + assertTrue(Files.exists(file), "File does not exist: $file") + assertTrue(Files.isRegularFile(file), "File is not a regular file: $file") + assertContains(file.toFile().readText(), substring) + } + }) + } + + private fun loadResource(@Language("file-reference") path: String): Path { + val resource = EnumTemplatesTest::class.java.getResource(path) + ?.toURI() + ?: error("Failed to load resource: $path") + return Paths.get(resource) + } + + private val enumEntriesTemplate: Path = loadResource("/dokka/docs/kdoc/EnumEntries.kt.template") + private val enumValueOfTemplate: Path = loadResource("/dokka/docs/kdoc/EnumValueOf.kt.template") + private val enumValuesTemplate: Path = loadResource("/dokka/docs/kdoc/EnumValues.kt.template") + + /** + * Base directory for the unpacked Kotlin stdlib source code. + * The system property must be set in the Gradle task. + */ + private val kotlinStdlibSourcesDir: Path by lazy { + val sourcesDir = System.getProperty("kotlinStdlibSourcesDir") + ?: error("Missing 'kotlinStdlibSourcesDir' system property") + Paths.get(sourcesDir) + } + + /** Get the actual `Enum.kt` source file from Kotlin stdlib. */ + private val actualStdlibEnumKt: Path by lazy { + kotlinStdlibSourcesDir.resolve("jvmMain/kotlin/Enum.kt") + } + } +} diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt b/dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt index da48f0de44..dcfc0b5dd7 100644 --- a/dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt +++ b/dokka-subprojects/plugin-base/src/test/kotlin/utils/TestUtils.kt @@ -31,9 +31,10 @@ interface AssertDSL { infix fun Any?.equals(other: Any?) = assertEquals(other, this) infix fun Collection?.allEquals(other: Any?) = this?.onEach { it equals other } ?: run { fail("Collection is empty") } + infix fun Collection?.exists(e: T) { assertTrue(this.orEmpty().isNotEmpty(), "Collection cannot be null or empty") - assertTrue(this!!.any{it == e}, "Collection doesn't contain $e") + assertTrue(this!!.any { it == e }, "Collection doesn't contain $e") } infix fun Collection?.counts(n: Int) = this.orEmpty().assertCount(n) @@ -44,16 +45,30 @@ interface AssertDSL { assertEquals(n, count(), "${prefix}Expected $n, got ${count()}") } -/* - * TODO replace with kotlin.test.assertContains after migrating to Kotlin 1.5+ - */ -internal fun assertContains(iterable: Iterable, element: T, ) { +// TODO replace with kotlin.test.assertContains after migrating to Kotlin 1.5+ +internal fun assertContains(iterable: Iterable, element: T) { asserter.assertTrue( { "Expected the collection to contain the element.\nCollection <$iterable>, element <$element>." }, iterable.contains(element) ) } +private fun messagePrefix(message: String?) = if (message == null) "" else "$message. " + +// TODO replace with kotlin.test.assertContains after migrating to Kotlin 1.5+ +// https://github.com/JetBrains/kotlin/blob/c072e7c945fed74805d87ecc89c9a650bad23e12/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt#L334-L345 +internal fun assertContains( + charSequence: CharSequence, + other: CharSequence, + ignoreCase: Boolean = false, + message: String? = null, +) { + asserter.assertTrue( + { messagePrefix(message) + "Expected the char sequence to contain the substring.\nCharSequence <$charSequence>, substring <$other>, ignoreCase <$ignoreCase>." }, + charSequence.contains(other, ignoreCase) + ) +} + inline fun Any?.assertIsInstance(name: String): T = this.let { it as? T } ?: throw AssertionError("$name should not be null")