From edcd1fb24d01e11b5a8185328255f2005aadf037 Mon Sep 17 00:00:00 2001 From: Ignat Beresnev Date: Fri, 27 Oct 2023 13:11:41 +0200 Subject: Implement analysis test API (#3184) --- core/api/core.api | 26 ++ .../documentation/DefaultDocumentableMerger.kt | 307 +++++++++++++++++++++ .../main/kotlin/testApi/testRunner/TestRunner.kt | 2 +- plugins/base/api/base.api | 18 -- plugins/base/src/main/kotlin/DokkaBase.kt | 5 +- .../documentables/ClashingDriIdentifier.kt | 12 + .../documentables/DefaultDocumentableMerger.kt | 287 ------------------- .../documentables/DefaultPageCreator.kt | 2 +- subprojects/analysis-kotlin-api/build.gradle.kts | 20 ++ .../test/jvm/java/SampleJavaAnalysisTest.kt | 49 ++++ .../test/jvm/kotlin/SampleKotlinJvmAnalysisTest.kt | 43 +++ .../test/jvm/mixed/SampleMixedJvmAnalysisTest.kt | 81 ++++++ .../moduledocs/PackageDocumentationAnalysisTest.kt | 66 +++++ .../analysis/test/sample/SampleAnalysisTest.kt | 55 ++++ .../jetbrains/dokka/analysis/test/api/TestData.kt | 21 ++ .../dokka/analysis/test/api/TestDataFile.kt | 37 +++ .../dokka/analysis/test/api/TestProject.kt | 97 +++++++ .../dokka/analysis/test/api/TestProjectFactory.kt | 67 +++++ .../test/api/analysis/TestAnalysisContext.kt | 36 +++ .../test/api/analysis/TestAnalysisServices.kt | 20 ++ .../test/api/analysis/TestProjectAnalyzer.kt | 223 +++++++++++++++ .../api/configuration/TestDokkaConfiguration.kt | 171 ++++++++++++ .../configuration/TestDokkaConfigurationBuilder.kt | 145 ++++++++++ .../configuration/TestDokkaConfigurationMapper.kt | 177 ++++++++++++ .../test/api/jvm/java/JavaConfigurationBuilder.kt | 61 ++++ .../analysis/test/api/jvm/java/JavaFileCreator.kt | 25 ++ .../analysis/test/api/jvm/java/JavaTestData.kt | 54 ++++ .../analysis/test/api/jvm/java/JavaTestDataFile.kt | 27 ++ .../analysis/test/api/jvm/java/JavaTestProject.kt | 73 +++++ .../jvm/kotlin/KotlinJvmConfigurationBuilder.kt | 56 ++++ .../api/jvm/kotlin/KotlinJvmDependencyUtils.kt | 22 ++ .../test/api/jvm/kotlin/KotlinJvmTestProject.kt | 93 +++++++ .../api/jvm/mixed/MixedJvmConfigurationBuilder.kt | 69 +++++ .../test/api/jvm/mixed/MixedJvmTestData.kt | 47 ++++ .../test/api/jvm/mixed/MixedJvmTestProject.kt | 80 ++++++ .../analysis/test/api/kotlin/KotlinTestData.kt | 48 ++++ .../analysis/test/api/kotlin/KotlinTestDataFile.kt | 27 ++ .../analysis/test/api/kotlin/KtFileCreator.kt | 32 +++ .../api/kotlin/sample/KotlinSampleFileCreator.kt | 32 +++ .../test/api/kotlin/sample/KotlinSampleTestData.kt | 44 +++ .../api/kotlin/sample/KotlinSampleTestDataFile.kt | 27 ++ .../analysis/test/api/markdown/MarkdownTestData.kt | 40 +++ .../test/api/markdown/MarkdownTestDataFile.kt | 26 ++ .../analysis/test/api/markdown/MdFileCreator.kt | 28 ++ .../analysis/test/api/util/CollectionUtils.kt | 18 ++ .../dokka/analysis/test/api/util/DslApiUtils.kt | 9 + .../dokka/analysis/test/api/util/FileUtils.kt | 41 +++ 47 files changed, 2635 insertions(+), 311 deletions(-) create mode 100644 core/src/main/kotlin/transformers/documentation/DefaultDocumentableMerger.kt create mode 100644 plugins/base/src/main/kotlin/transformers/documentables/ClashingDriIdentifier.kt delete mode 100644 plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt create mode 100644 subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/SampleJavaAnalysisTest.kt create mode 100644 subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/kotlin/SampleKotlinJvmAnalysisTest.kt create mode 100644 subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/mixed/SampleMixedJvmAnalysisTest.kt create mode 100644 subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/moduledocs/PackageDocumentationAnalysisTest.kt create mode 100644 subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestData.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestDataFile.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProjectFactory.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisContext.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfiguration.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationBuilder.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationMapper.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaFileCreator.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestData.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestDataFile.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmDependencyUtils.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmConfigurationBuilder.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestData.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestData.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestDataFile.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KtFileCreator.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleFileCreator.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestData.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestDataFile.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestData.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestDataFile.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MdFileCreator.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/CollectionUtils.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/DslApiUtils.kt create mode 100644 subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/FileUtils.kt diff --git a/core/api/core.api b/core/api/core.api index 2399880b..fffcb877 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -4513,6 +4513,32 @@ public abstract interface class org/jetbrains/dokka/renderers/Renderer { public abstract fun render (Lorg/jetbrains/dokka/pages/RootPageNode;)V } +public final class org/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier : org/jetbrains/dokka/model/properties/ExtraProperty { + public static final field Companion Lorg/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier$Companion; + public fun (Ljava/util/Set;)V + public final fun component1 ()Ljava/util/Set; + public final fun copy (Ljava/util/Set;)Lorg/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier; + public static synthetic fun copy$default (Lorg/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier;Ljava/util/Set;ILjava/lang/Object;)Lorg/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier; + public fun equals (Ljava/lang/Object;)Z + public fun getKey ()Lorg/jetbrains/dokka/model/properties/ExtraProperty$Key; + public final fun getValue ()Ljava/util/Set; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier$Companion : org/jetbrains/dokka/model/properties/ExtraProperty$Key { + public synthetic fun mergeStrategyFor (Ljava/lang/Object;Ljava/lang/Object;)Lorg/jetbrains/dokka/model/properties/MergeStrategy; + public fun mergeStrategyFor (Lorg/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier;Lorg/jetbrains/dokka/transformers/documentation/ClashingDriIdentifier;)Lorg/jetbrains/dokka/model/properties/MergeStrategy; +} + +public final class org/jetbrains/dokka/transformers/documentation/DefaultDocumentableMerger : org/jetbrains/dokka/transformers/documentation/DocumentableMerger { + public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V + public fun invoke (Ljava/util/Collection;)Lorg/jetbrains/dokka/model/DModule; + public final fun mergeWith (Lorg/jetbrains/dokka/model/DFunction;Lorg/jetbrains/dokka/model/DFunction;)Lorg/jetbrains/dokka/model/DFunction; + public final fun mergeWith (Lorg/jetbrains/dokka/model/DPackage;Lorg/jetbrains/dokka/model/DPackage;)Lorg/jetbrains/dokka/model/DPackage; + public final fun mergeWith (Lorg/jetbrains/dokka/model/DProperty;Lorg/jetbrains/dokka/model/DProperty;)Lorg/jetbrains/dokka/model/DProperty; +} + public abstract interface class org/jetbrains/dokka/transformers/documentation/DocumentableMerger { public abstract fun invoke (Ljava/util/Collection;)Lorg/jetbrains/dokka/model/DModule; } diff --git a/core/src/main/kotlin/transformers/documentation/DefaultDocumentableMerger.kt b/core/src/main/kotlin/transformers/documentation/DefaultDocumentableMerger.kt new file mode 100644 index 00000000..fe1e5d64 --- /dev/null +++ b/core/src/main/kotlin/transformers/documentation/DefaultDocumentableMerger.kt @@ -0,0 +1,307 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.transformers.documentation + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.InternalDokkaApi +import org.jetbrains.dokka.model.* +import org.jetbrains.dokka.model.properties.ExtraProperty +import org.jetbrains.dokka.model.properties.MergeStrategy +import org.jetbrains.dokka.model.properties.mergeExtras +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.CoreExtensions + +/** + * Should NOT be used outside of Dokka itself, there are no guarantees + * this class will continue to exist in future releases. + * + * This class resides in core because it is a non-trivial implementation + * for a core extension [CoreExtensions.documentableMerger], which is needed + * in modules that only have access to `dokka-core`. + */ +@InternalDokkaApi +public class DefaultDocumentableMerger(context: DokkaContext) : DocumentableMerger { + private val dependencyInfo = context.getDependencyInfo() + + override fun invoke(modules: Collection): DModule? = + modules.reduceOrNull { left, right -> + val list = listOf(left, right) + DModule( + name = modules.map { it.name }.distinct().joinToString("|"), + packages = merge( + list.flatMap { it.packages } + ) { pck1, pck2 -> pck1.mergeWith(pck2) }, + documentation = list.map { it.documentation }.flatMap { it.entries }.associate { (k, v) -> k to v }, + expectPresentInSet = list.firstNotNullOfOrNull { it.expectPresentInSet }, + sourceSets = list.flatMap { it.sourceSets }.toSet() + ).mergeExtras(left, right) + } + + private fun DokkaContext.getDependencyInfo() + : Map> { + + fun getDependencies(sourceSet: DokkaConfiguration.DokkaSourceSet): List = + listOf(sourceSet) + configuration.sourceSets.filter { + it.sourceSetID in sourceSet.dependentSourceSets + }.flatMap { getDependencies(it) } + + return configuration.sourceSets.associateWith { getDependencies(it) } + } + + private fun merge(elements: List, reducer: (T, T) -> T): List = + elements.groupingBy { it.dri } + .reduce { _, left, right -> reducer(left, right) } + .values.toList() + + private fun mergeExpectActual( + elements: List, + reducer: (T, T) -> T + ): List where T : Documentable, T : WithSources { + + fun mergeClashingElements(elements: List>>): List = + elements.groupBy { it.first.name }.values.flatMap { listOfDocumentableToSSIds -> + val merged = listOfDocumentableToSSIds.map { (documentable, sourceSets) -> + when (documentable) { + is DClass -> documentable.copy( + extra = documentable.extra + ClashingDriIdentifier( + sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) + ) + ) + is DObject -> documentable.copy( + extra = documentable.extra + ClashingDriIdentifier( + sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) + ) + ) + is DAnnotation -> documentable.copy( + extra = documentable.extra + ClashingDriIdentifier( + sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) + ) + ) + is DInterface -> documentable.copy( + extra = documentable.extra + ClashingDriIdentifier( + sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) + ) + ) + is DEnum -> documentable.copy( + extra = documentable.extra + ClashingDriIdentifier( + sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) + ) + ) + is DFunction -> documentable.copy( + extra = documentable.extra + ClashingDriIdentifier( + sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) + ) + ) + is DProperty -> documentable.copy( + extra = documentable.extra + ClashingDriIdentifier( + sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) + ) + ) + else -> documentable + } + } + @Suppress("UNCHECKED_CAST") + merged as List + } + + + fun analyzeExpectActual(sameDriElements: List): List { + val (expects, actuals) = sameDriElements.partition { it.expectPresentInSet != null } + val groupedByOwnExpectWithActualSourceSetIds = expects.map { expect -> + val actualsForGivenExpect = actuals.filter { actual -> + dependencyInfo[actual.sourceSets.single()] + ?.contains(expect.expectPresentInSet!!) + ?: throw IllegalStateException("Cannot resolve expect/actual relation for ${actual.name}") + } + (listOf(expect) + actualsForGivenExpect) to actualsForGivenExpect.flatMap { it.sourceSets }.toSet() + } + val reducedToOneDocumentableWithActualSourceSetIds = + groupedByOwnExpectWithActualSourceSetIds.map { it.first.reduce(reducer) to it.second } + return reducedToOneDocumentableWithActualSourceSetIds.let(::mergeClashingElements) + } + + + return elements.partition { + (it as? WithIsExpectActual)?.isExpectActual ?: false + }.let { (expectActuals, notExpectActuals) -> + notExpectActuals.map { it to it.sourceSets } + .groupBy { it.first.dri }.values.flatMap(::mergeClashingElements) + + expectActuals.groupBy { it.dri }.values.flatMap(::analyzeExpectActual) + } + } + + public fun DPackage.mergeWith(other: DPackage): DPackage = copy( + functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, + properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, + classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + typealiases = merge(typealiases + other.typealiases) { ta1, ta2 -> ta1.mergeWith(ta2) }, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + public fun DFunction.mergeWith(other: DFunction): DFunction = copy( + parameters = merge(this.parameters + other.parameters) { p1, p2 -> p1.mergeWith(p2) }, + receiver = receiver?.let { r -> other.receiver?.let { r.mergeWith(it) } ?: r } ?: other.receiver, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources + other.sources, + visibility = visibility + other.visibility, + modifier = modifier + other.modifier, + sourceSets = sourceSets + other.sourceSets, + generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, + ).mergeExtras(this, other) + + public fun DProperty.mergeWith(other: DProperty): DProperty = copy( + receiver = receiver?.let { r -> other.receiver?.let { r.mergeWith(it) } ?: r } ?: other.receiver, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources + other.sources, + visibility = visibility + other.visibility, + modifier = modifier + other.modifier, + sourceSets = sourceSets + other.sourceSets, + getter = getter?.let { g -> other.getter?.let { g.mergeWith(it) } ?: g } ?: other.getter, + setter = setter?.let { s -> other.setter?.let { s.mergeWith(it) } ?: s } ?: other.setter, + generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, + ).mergeExtras(this, other) + + private fun DClasslike.mergeWith(other: DClasslike): DClasslike = when { + this is DClass && other is DClass -> mergeWith(other) + this is DEnum && other is DEnum -> mergeWith(other) + this is DInterface && other is DInterface -> mergeWith(other) + this is DObject && other is DObject -> mergeWith(other) + this is DAnnotation && other is DAnnotation -> mergeWith(other) + else -> throw IllegalStateException("${this::class.qualifiedName} ${this.name} cannot be merged with ${other::class.qualifiedName} ${other.name}") + } + + private fun DClass.mergeWith(other: DClass): DClass = copy( + constructors = mergeExpectActual( + constructors + other.constructors + ) { f1, f2 -> f1.mergeWith(f2) }, + functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, + properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, + classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, + modifier = modifier + other.modifier, + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources + other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + private fun DEnum.mergeWith(other: DEnum): DEnum = copy( + entries = merge(entries + other.entries) { ee1, ee2 -> ee1.mergeWith(ee2) }, + constructors = mergeExpectActual( + constructors + other.constructors + ) { f1, f2 -> f1.mergeWith(f2) }, + functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, + properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, + classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources + other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + private fun DEnumEntry.mergeWith(other: DEnumEntry): DEnumEntry = copy( + functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, + properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, + classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + private fun DObject.mergeWith(other: DObject): DObject = copy( + functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, + properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, + classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources + other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + private fun DInterface.mergeWith(other: DInterface): DInterface = copy( + functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, + properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, + classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, + supertypes = supertypes + other.supertypes, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources + other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + private fun DAnnotation.mergeWith(other: DAnnotation): DAnnotation = copy( + constructors = mergeExpectActual( + constructors + other.constructors + ) { f1, f2 -> f1.mergeWith(f2) }, + functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, + properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, + classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, + companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sources = sources + other.sources, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets, + generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) } + ).mergeExtras(this, other) + + private fun DParameter.mergeWith(other: DParameter): DParameter = copy( + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + private fun DTypeParameter.mergeWith(other: DTypeParameter): DTypeParameter = copy( + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) + + private fun DTypeAlias.mergeWith(other: DTypeAlias): DTypeAlias = copy( + documentation = documentation + other.documentation, + expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, + underlyingType = underlyingType + other.underlyingType, + visibility = visibility + other.visibility, + sourceSets = sourceSets + other.sourceSets + ).mergeExtras(this, other) +} + +public data class ClashingDriIdentifier(val value: Set) : ExtraProperty { + public companion object : ExtraProperty.Key { + override fun mergeStrategyFor( + left: ClashingDriIdentifier, + right: ClashingDriIdentifier + ): MergeStrategy = + MergeStrategy.Replace(ClashingDriIdentifier(left.value + right.value)) + } + + override val key: ExtraProperty.Key = ClashingDriIdentifier +} + +// TODO [beresnev] remove this copy-paste and use the same method from stdlib instead after updating to 1.5 +private inline fun Iterable.firstNotNullOfOrNull(transform: (T) -> R?): R? { + for (element in this) { + val result = transform(element) + if (result != null) { + return result + } + } + return null +} diff --git a/core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt b/core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt index 23300190..0958ea14 100644 --- a/core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt +++ b/core/test-api/src/main/kotlin/testApi/testRunner/TestRunner.kt @@ -113,7 +113,7 @@ public abstract class AbstractTest, D : Dokk block(tempDir) } finally { if (cleanUpAfterUse) { - tempDir.delete() + tempDir.deleteRecursively() } } } diff --git a/plugins/base/api/base.api b/plugins/base/api/base.api index 788444b9..ae872558 100644 --- a/plugins/base/api/base.api +++ b/plugins/base/api/base.api @@ -1102,24 +1102,6 @@ public final class org/jetbrains/dokka/base/transformers/documentables/CallableE public fun mergeStrategyFor (Lorg/jetbrains/dokka/base/transformers/documentables/CallableExtensions;Lorg/jetbrains/dokka/base/transformers/documentables/CallableExtensions;)Lorg/jetbrains/dokka/model/properties/MergeStrategy; } -public final class org/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier : org/jetbrains/dokka/model/properties/ExtraProperty { - public static final field Companion Lorg/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier$Companion; - public fun (Ljava/util/Set;)V - public final fun component1 ()Ljava/util/Set; - public final fun copy (Ljava/util/Set;)Lorg/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier; - public static synthetic fun copy$default (Lorg/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier;Ljava/util/Set;ILjava/lang/Object;)Lorg/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier; - public fun equals (Ljava/lang/Object;)Z - public fun getKey ()Lorg/jetbrains/dokka/model/properties/ExtraProperty$Key; - public final fun getValue ()Ljava/util/Set; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class org/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier$Companion : org/jetbrains/dokka/model/properties/ExtraProperty$Key { - public synthetic fun mergeStrategyFor (Ljava/lang/Object;Ljava/lang/Object;)Lorg/jetbrains/dokka/model/properties/MergeStrategy; - public fun mergeStrategyFor (Lorg/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier;Lorg/jetbrains/dokka/base/transformers/documentables/ClashingDriIdentifier;)Lorg/jetbrains/dokka/model/properties/MergeStrategy; -} - public final class org/jetbrains/dokka/base/transformers/documentables/DeprecatedDocumentableFilterTransformer : org/jetbrains/dokka/base/transformers/documentables/SuppressedByConditionDocumentableFilterTransformer { public fun (Lorg/jetbrains/dokka/plugability/DokkaContext;)V public fun shouldBeSuppressed (Lorg/jetbrains/dokka/model/Documentable;)Z diff --git a/plugins/base/src/main/kotlin/DokkaBase.kt b/plugins/base/src/main/kotlin/DokkaBase.kt index dfec2c15..ca86d4d5 100644 --- a/plugins/base/src/main/kotlin/DokkaBase.kt +++ b/plugins/base/src/main/kotlin/DokkaBase.kt @@ -33,10 +33,7 @@ import org.jetbrains.dokka.base.translators.documentables.DefaultDocumentableToP import org.jetbrains.dokka.generation.Generation import org.jetbrains.dokka.plugability.* import org.jetbrains.dokka.renderers.Renderer -import org.jetbrains.dokka.transformers.documentation.DocumentableMerger -import org.jetbrains.dokka.transformers.documentation.DocumentableToPageTranslator -import org.jetbrains.dokka.transformers.documentation.DocumentableTransformer -import org.jetbrains.dokka.transformers.documentation.PreMergeDocumentableTransformer +import org.jetbrains.dokka.transformers.documentation.* import org.jetbrains.dokka.transformers.pages.PageTransformer @Suppress("unused") diff --git a/plugins/base/src/main/kotlin/transformers/documentables/ClashingDriIdentifier.kt b/plugins/base/src/main/kotlin/transformers/documentables/ClashingDriIdentifier.kt new file mode 100644 index 00000000..e9c7342e --- /dev/null +++ b/plugins/base/src/main/kotlin/transformers/documentables/ClashingDriIdentifier.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.base.transformers.documentables + +@Deprecated( + message = "Declaration was moved to dokka-core", + replaceWith = ReplaceWith("org.jetbrains.dokka.transformers.documentation.ClashingDriIdentifier"), + level = DeprecationLevel.WARNING // TODO change to error after Kotlin 1.9.20 +) +public typealias ClashingDriIdentifier = org.jetbrains.dokka.transformers.documentation.ClashingDriIdentifier diff --git a/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt b/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt deleted file mode 100644 index ec53df78..00000000 --- a/plugins/base/src/main/kotlin/transformers/documentables/DefaultDocumentableMerger.kt +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package org.jetbrains.dokka.base.transformers.documentables - -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.base.utils.firstNotNullOfOrNull -import org.jetbrains.dokka.model.* -import org.jetbrains.dokka.model.properties.ExtraProperty -import org.jetbrains.dokka.model.properties.MergeStrategy -import org.jetbrains.dokka.model.properties.mergeExtras -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.transformers.documentation.DocumentableMerger - -internal class DefaultDocumentableMerger(val context: DokkaContext) : DocumentableMerger { - private val dependencyInfo = context.getDependencyInfo() - - override fun invoke(modules: Collection): DModule? = - modules.reduceOrNull { left, right -> - val list = listOf(left, right) - DModule( - name = modules.map { it.name }.distinct().joinToString("|"), - packages = merge( - list.flatMap { it.packages } - ) { pck1, pck2 -> pck1.mergeWith(pck2) }, - documentation = list.map { it.documentation }.flatMap { it.entries }.associate { (k, v) -> k to v }, - expectPresentInSet = list.firstNotNullOfOrNull { it.expectPresentInSet }, - sourceSets = list.flatMap { it.sourceSets }.toSet() - ).mergeExtras(left, right) - } - - private fun DokkaContext.getDependencyInfo() - : Map> { - - fun getDependencies(sourceSet: DokkaConfiguration.DokkaSourceSet): List = - listOf(sourceSet) + configuration.sourceSets.filter { - it.sourceSetID in sourceSet.dependentSourceSets - }.flatMap { getDependencies(it) } - - return configuration.sourceSets.associateWith { getDependencies(it) } - } - - private fun merge(elements: List, reducer: (T, T) -> T): List = - elements.groupingBy { it.dri } - .reduce { _, left, right -> reducer(left, right) } - .values.toList() - - private fun mergeExpectActual( - elements: List, - reducer: (T, T) -> T - ): List where T : Documentable, T : WithSources { - - fun mergeClashingElements(elements: List>>): List = - elements.groupBy { it.first.name }.values.flatMap { listOfDocumentableToSSIds -> - val merged = listOfDocumentableToSSIds.map { (documentable, sourceSets) -> - when (documentable) { - is DClass -> documentable.copy( - extra = documentable.extra + ClashingDriIdentifier( - sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) - ) - ) - is DObject -> documentable.copy( - extra = documentable.extra + ClashingDriIdentifier( - sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) - ) - ) - is DAnnotation -> documentable.copy( - extra = documentable.extra + ClashingDriIdentifier( - sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) - ) - ) - is DInterface -> documentable.copy( - extra = documentable.extra + ClashingDriIdentifier( - sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) - ) - ) - is DEnum -> documentable.copy( - extra = documentable.extra + ClashingDriIdentifier( - sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) - ) - ) - is DFunction -> documentable.copy( - extra = documentable.extra + ClashingDriIdentifier( - sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) - ) - ) - is DProperty -> documentable.copy( - extra = documentable.extra + ClashingDriIdentifier( - sourceSets + (documentable.extra[ClashingDriIdentifier]?.value ?: emptySet()) - ) - ) - else -> documentable - } - } - @Suppress("UNCHECKED_CAST") - merged as List - } - - - fun analyzeExpectActual(sameDriElements: List): List { - val (expects, actuals) = sameDriElements.partition { it.expectPresentInSet != null } - val groupedByOwnExpectWithActualSourceSetIds = expects.map { expect -> - val actualsForGivenExpect = actuals.filter { actual -> - dependencyInfo[actual.sourceSets.single()] - ?.contains(expect.expectPresentInSet!!) - ?: throw IllegalStateException("Cannot resolve expect/actual relation for ${actual.name}") - } - (listOf(expect) + actualsForGivenExpect) to actualsForGivenExpect.flatMap { it.sourceSets }.toSet() - } - val reducedToOneDocumentableWithActualSourceSetIds = - groupedByOwnExpectWithActualSourceSetIds.map { it.first.reduce(reducer) to it.second } - return reducedToOneDocumentableWithActualSourceSetIds.let(::mergeClashingElements) - } - - - return elements.partition { - (it as? WithIsExpectActual)?.isExpectActual ?: false - }.let { (expectActuals, notExpectActuals) -> - notExpectActuals.map { it to it.sourceSets } - .groupBy { it.first.dri }.values.flatMap(::mergeClashingElements) + - expectActuals.groupBy { it.dri }.values.flatMap(::analyzeExpectActual) - } - } - - fun DPackage.mergeWith(other: DPackage): DPackage = copy( - functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, - properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, - classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - typealiases = merge(typealiases + other.typealiases) { ta1, ta2 -> ta1.mergeWith(ta2) }, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DFunction.mergeWith(other: DFunction): DFunction = copy( - parameters = merge(this.parameters + other.parameters) { p1, p2 -> p1.mergeWith(p2) }, - receiver = receiver?.let { r -> other.receiver?.let { r.mergeWith(it) } ?: r } ?: other.receiver, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sources = sources + other.sources, - visibility = visibility + other.visibility, - modifier = modifier + other.modifier, - sourceSets = sourceSets + other.sourceSets, - generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, - ).mergeExtras(this, other) - - fun DProperty.mergeWith(other: DProperty): DProperty = copy( - receiver = receiver?.let { r -> other.receiver?.let { r.mergeWith(it) } ?: r } ?: other.receiver, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sources = sources + other.sources, - visibility = visibility + other.visibility, - modifier = modifier + other.modifier, - sourceSets = sourceSets + other.sourceSets, - getter = getter?.let { g -> other.getter?.let { g.mergeWith(it) } ?: g } ?: other.getter, - setter = setter?.let { s -> other.setter?.let { s.mergeWith(it) } ?: s } ?: other.setter, - generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, - ).mergeExtras(this, other) - - fun DClasslike.mergeWith(other: DClasslike): DClasslike = when { - this is DClass && other is DClass -> mergeWith(other) - this is DEnum && other is DEnum -> mergeWith(other) - this is DInterface && other is DInterface -> mergeWith(other) - this is DObject && other is DObject -> mergeWith(other) - this is DAnnotation && other is DAnnotation -> mergeWith(other) - else -> throw IllegalStateException("${this::class.qualifiedName} ${this.name} cannot be merged with ${other::class.qualifiedName} ${other.name}") - } - - fun DClass.mergeWith(other: DClass): DClass = copy( - constructors = mergeExpectActual( - constructors + other.constructors - ) { f1, f2 -> f1.mergeWith(f2) }, - functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, - properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, - classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, - companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, - generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, - modifier = modifier + other.modifier, - supertypes = supertypes + other.supertypes, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sources = sources + other.sources, - visibility = visibility + other.visibility, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DEnum.mergeWith(other: DEnum): DEnum = copy( - entries = merge(entries + other.entries) { ee1, ee2 -> ee1.mergeWith(ee2) }, - constructors = mergeExpectActual( - constructors + other.constructors - ) { f1, f2 -> f1.mergeWith(f2) }, - functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, - properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, - classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, - companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, - supertypes = supertypes + other.supertypes, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sources = sources + other.sources, - visibility = visibility + other.visibility, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DEnumEntry.mergeWith(other: DEnumEntry): DEnumEntry = copy( - functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, - properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, - classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DObject.mergeWith(other: DObject): DObject = copy( - functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, - properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, - classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, - supertypes = supertypes + other.supertypes, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sources = sources + other.sources, - visibility = visibility + other.visibility, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DInterface.mergeWith(other: DInterface): DInterface = copy( - functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, - properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, - classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, - companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, - generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) }, - supertypes = supertypes + other.supertypes, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sources = sources + other.sources, - visibility = visibility + other.visibility, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DAnnotation.mergeWith(other: DAnnotation): DAnnotation = copy( - constructors = mergeExpectActual( - constructors + other.constructors - ) { f1, f2 -> f1.mergeWith(f2) }, - functions = mergeExpectActual(functions + other.functions) { f1, f2 -> f1.mergeWith(f2) }, - properties = mergeExpectActual(properties + other.properties) { p1, p2 -> p1.mergeWith(p2) }, - classlikes = mergeExpectActual(classlikes + other.classlikes) { c1, c2 -> c1.mergeWith(c2) }, - companion = companion?.let { c -> other.companion?.let { c.mergeWith(it) } ?: c } ?: other.companion, - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sources = sources + other.sources, - visibility = visibility + other.visibility, - sourceSets = sourceSets + other.sourceSets, - generics = merge(generics + other.generics) { tp1, tp2 -> tp1.mergeWith(tp2) } - ).mergeExtras(this, other) - - fun DParameter.mergeWith(other: DParameter): DParameter = copy( - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DTypeParameter.mergeWith(other: DTypeParameter): DTypeParameter = copy( - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) - - fun DTypeAlias.mergeWith(other: DTypeAlias): DTypeAlias = copy( - documentation = documentation + other.documentation, - expectPresentInSet = expectPresentInSet ?: other.expectPresentInSet, - underlyingType = underlyingType + other.underlyingType, - visibility = visibility + other.visibility, - sourceSets = sourceSets + other.sourceSets - ).mergeExtras(this, other) -} - -public data class ClashingDriIdentifier(val value: Set) : ExtraProperty { - public companion object : ExtraProperty.Key { - override fun mergeStrategyFor( - left: ClashingDriIdentifier, - right: ClashingDriIdentifier - ): MergeStrategy = - MergeStrategy.Replace(ClashingDriIdentifier(left.value + right.value)) - } - - override val key: ExtraProperty.Key = ClashingDriIdentifier -} diff --git a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt index 58abee56..5c8ac512 100644 --- a/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt +++ b/plugins/base/src/main/kotlin/translators/documentables/DefaultPageCreator.kt @@ -9,7 +9,7 @@ import org.jetbrains.dokka.base.DokkaBaseConfiguration import org.jetbrains.dokka.base.resolvers.anchors.SymbolAnchorHint import org.jetbrains.dokka.base.signatures.SignatureProvider import org.jetbrains.dokka.base.transformers.documentables.CallableExtensions -import org.jetbrains.dokka.base.transformers.documentables.ClashingDriIdentifier +import org.jetbrains.dokka.transformers.documentation.ClashingDriIdentifier import org.jetbrains.dokka.base.transformers.pages.comments.CommentsToContentConverter import org.jetbrains.dokka.base.transformers.pages.tags.CustomTagContentProvider import org.jetbrains.dokka.base.translators.documentables.PageContentBuilder.DocumentableContentBuilder diff --git a/subprojects/analysis-kotlin-api/build.gradle.kts b/subprojects/analysis-kotlin-api/build.gradle.kts index 58247479..bf3b5b3c 100644 --- a/subprojects/analysis-kotlin-api/build.gradle.kts +++ b/subprojects/analysis-kotlin-api/build.gradle.kts @@ -7,10 +7,30 @@ import org.jetbrains.registerDokkaArtifactPublication plugins { id("org.jetbrains.conventions.kotlin-jvm") id("org.jetbrains.conventions.maven-publish") + `java-test-fixtures` } dependencies { compileOnly(projects.core) + + testFixturesApi(projects.core) + + testImplementation(kotlin("test")) + testImplementation(projects.subprojects.analysisKotlinDescriptors) +} + +disableTestFixturesPublishing() + +/** + * Test fixtures are automatically published by default, which at this moment in time is unwanted + * as the test api is unstable and is internal to the Dokka project, so it shouldn't be used outside of it. + * + * @see https://docs.gradle.org/current/userguide/java_testing.html#ex-disable-publishing-of-test-fixtures-variants + */ +fun disableTestFixturesPublishing() { + val javaComponent = components["java"] as AdhocComponentWithVariants + javaComponent.withVariantsFromConfiguration(configurations["testFixturesApiElements"]) { skip() } + javaComponent.withVariantsFromConfiguration(configurations["testFixturesRuntimeElements"]) { skip() } } registerDokkaArtifactPublication("analysisKotlinApi") { diff --git a/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/SampleJavaAnalysisTest.kt b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/SampleJavaAnalysisTest.kt new file mode 100644 index 00000000..f6632f60 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/java/SampleJavaAnalysisTest.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.analysis.test.jvm.java + +import org.jetbrains.dokka.analysis.test.api.javaTestProject +import org.jetbrains.dokka.analysis.test.api.parse +import kotlin.test.Test +import kotlin.test.assertEquals + +class SampleJavaAnalysisTest { + + /** + * Used as a sample for [javaTestProject] + */ + @Test + fun sample() { + val testProject = javaTestProject { + dokkaConfiguration { + moduleName = "java-module-name-for-unit-test" + + javaSourceSet { + // source-set specific configuration + } + } + javaFile(pathFromSrc = "org/jetbrains/dokka/test/java/Bar.java") { + +""" + public class Bar { + public static void bar() { + System.out.println("Bar"); + } + } + """ + } + } + + val module = testProject.parse() + assertEquals("java-module-name-for-unit-test", module.name) + assertEquals(1, module.packages.size) + + val pckg = module.packages[0] + assertEquals("org.jetbrains.dokka.test.java", pckg.name) + assertEquals(1, pckg.classlikes.size) + + val fooClass = pckg.classlikes[0] + assertEquals("Bar", fooClass.name) + } +} diff --git a/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/kotlin/SampleKotlinJvmAnalysisTest.kt b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/kotlin/SampleKotlinJvmAnalysisTest.kt new file mode 100644 index 00000000..6c73af1f --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/kotlin/SampleKotlinJvmAnalysisTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.jvm.kotlin + +import org.jetbrains.dokka.analysis.test.api.kotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.parse +import kotlin.test.Test +import kotlin.test.assertEquals + +class SampleKotlinJvmAnalysisTest { + + /** + * Used as a sample for [kotlinJvmTestProject] + */ + @Test + fun sample() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + moduleName = "kotlin-jvm-module-name-for-unit-test" + + kotlinSourceSet { + // source-set specific configuration + } + } + ktFile(pathFromSrc = "org/jetbrains/dokka/test/kotlin/MyFile.kt") { + +"public class Foo {}" + } + } + + val module = testProject.parse() + assertEquals("kotlin-jvm-module-name-for-unit-test", module.name) + assertEquals(1, module.packages.size) + + val pckg = module.packages[0] + assertEquals("org.jetbrains.dokka.test.kotlin", pckg.name) + assertEquals(1, pckg.classlikes.size) + + val fooClass = pckg.classlikes[0] + assertEquals("Foo", fooClass.name) + } +} diff --git a/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/mixed/SampleMixedJvmAnalysisTest.kt b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/mixed/SampleMixedJvmAnalysisTest.kt new file mode 100644 index 00000000..fec2ceb8 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/jvm/mixed/SampleMixedJvmAnalysisTest.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.analysis.test.jvm.mixed + +import org.jetbrains.dokka.analysis.test.api.mixedJvmTestProject +import org.jetbrains.dokka.analysis.test.api.parse +import kotlin.test.Test +import kotlin.test.assertEquals + +class SampleMixedJvmAnalysisTest { + + /** + * Used as a sample for [mixedJvmTestProject] + */ + @Test + fun sample() { + val testProject = mixedJvmTestProject { + dokkaConfiguration { + moduleName = "mixed-project-module-name-for-unit-test" + + jvmSourceSet { + // source-set specific configuration + } + } + + kotlinSourceDirectory { + ktFile(pathFromSrc = "test/MyFile.kt") { + +"fun foo(): String = \"Foo\"" + } + javaFile(pathFromSrc = "test/MyJavaFileInKotlin.java") { + +""" + public class MyJavaFileInKotlin { + public static void bar() { + System.out.println("Bar"); + } + } + """ + } + } + + javaSourceDirectory { + ktFile(pathFromSrc = "test/MyFile.kt") { + +"fun bar(): String = \"Bar\"" + } + javaFile(pathFromSrc = "test/MyJavaFileInJava.java") { + +""" + public class MyJavaFileInJava { + public static void bar() { + System.out.println("Bar"); + } + } + """ + } + } + } + + val module = testProject.parse() + assertEquals("mixed-project-module-name-for-unit-test", module.name) + assertEquals(1, module.packages.size) + + val pckg = module.packages[0] + assertEquals("test", pckg.name) + + assertEquals(2, pckg.classlikes.size) + assertEquals(2, pckg.functions.size) + + val firstClasslike = pckg.classlikes[0] + assertEquals("MyJavaFileInKotlin", firstClasslike.name) + + val secondClasslike = pckg.classlikes[1] + assertEquals("MyJavaFileInJava", secondClasslike.name) + + val firstFunction = pckg.functions[0] + assertEquals("bar", firstFunction.name) + + val secondFunction = pckg.functions[1] + assertEquals("foo", secondFunction.name) + } +} diff --git a/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/moduledocs/PackageDocumentationAnalysisTest.kt b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/moduledocs/PackageDocumentationAnalysisTest.kt new file mode 100644 index 00000000..55507023 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/moduledocs/PackageDocumentationAnalysisTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.moduledocs + +import org.jetbrains.dokka.analysis.test.api.kotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.useServices +import org.jetbrains.dokka.model.doc.CodeInline +import org.jetbrains.dokka.model.doc.Description +import org.jetbrains.dokka.model.doc.P +import org.jetbrains.dokka.model.doc.Text +import kotlin.test.Test +import kotlin.test.assertEquals + +class PackageDocumentationAnalysisTest { + + @Test + fun `should parse include description for a nested package in kotlin-jvm`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + includes = setOf("/documentation/cool-package-description.md") + } + } + + ktFile(pathFromSrc = "org/jetbrains/dokka/pckg/docs/test/TestFile.kt") { + +"class TestFile {}" + } + + mdFile(pathFromProjectRoot = "/documentation/cool-package-description.md") { + +""" + # Package org.jetbrains.dokka.pckg.docs.test + + This is my test description for the package `org.jetbrains.dokka.pckg.docs.test`, + which contains only one file named TestFile.kt. It has one empty class. + """ + } + } + + testProject.useServices { context -> + val pckg = context.module.packages.single { it.name == "org.jetbrains.dokka.pckg.docs.test" } + + val allPackageDocs = moduleAndPackageDocumentationReader.read(pckg) + assertEquals(1, allPackageDocs.size) + + val sourceSetPackageDocs = allPackageDocs.entries.single().value + assertEquals(1, sourceSetPackageDocs.children.size) + + val descriptionTag = sourceSetPackageDocs.children.single() as Description + assertEquals(1, descriptionTag.children.size) + + val paragraphTag = descriptionTag.children.single() as P + assertEquals(3, paragraphTag.children.size) + + val expectedParagraphChildren = listOf( + Text("This is my test description for the package "), + CodeInline(children = listOf(Text( + "org.jetbrains.dokka.pckg.docs.test" + ))), + Text(", which contains only one file named TestFile.kt. It has one empty class.") + ) + assertEquals(expectedParagraphChildren, paragraphTag.children) + } + } +} diff --git a/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt new file mode 100644 index 00000000..618e28a8 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.sample + +import org.jetbrains.dokka.analysis.test.api.kotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.useServices +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class SampleAnalysisTest { + + @Test + fun `should return sources of a kotlin sample`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + additionalSourceRoots = setOf("/samples") + } + } + sampleFile("/samples/stringListOf-sample.kt", fqPackageName = "org.jetbrains.dokka.sample.generator") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + import org.jetbrains.dokka.DokkaGenerator + import org.jetbrains.dokka.utilities.DokkaLogger + + fun runGenerator(configuration: DokkaConfiguration, logger: DokkaLogger) { + DokkaGenerator(configuration, logger).generate() + } + """ + } + } + + testProject.useServices { context -> + val sampleSourceSet = context.configuration.sourceSets.single() + + val sampleProvider = sampleProviderFactory.build() + val sample = sampleProvider.getSample(sampleSourceSet, "org.jetbrains.dokka.sample.generator.runGenerator") + assertNotNull(sample) + + val expectedImports = listOf( + "import org.jetbrains.dokka.DokkaConfiguration", + "import org.jetbrains.dokka.DokkaGenerator", + "import org.jetbrains.dokka.utilities.DokkaLogger" + ).joinToString(separator = "\n") + + val expectedBody = "DokkaGenerator(configuration, logger).generate()" + + assertEquals(expectedImports, sample.imports) + assertEquals(expectedBody, sample.body) + } + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestData.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestData.kt new file mode 100644 index 00000000..64bfd7a3 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestData.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api + +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * Represents some sort of data of a [TestProject], which normally consists of a number of [TestDataFile]. + * + * This can be anything that can usually be found in a user-defined project: + * programming language source code, markdown files with documentation, samples, etc. + * + * This virtual test data will be materialized and created physically before running Dokka, + * and then passed as input files into it. + */ +@AnalysisTestDslMarker +interface TestData { + fun getFiles(): List +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestDataFile.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestDataFile.kt new file mode 100644 index 00000000..5b2233ba --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestDataFile.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api + +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * Represents a single file of a project's [TestData]. + * + * This file will be materialized and created physically before running Dokka, + * and then passed as one of the input files into it. + * + * @property pathFromProjectRoot this file's path from the root of the project. Must begin + * with `/` to not confuse it with relative paths. + */ +@AnalysisTestDslMarker +abstract class TestDataFile(val pathFromProjectRoot: String) { + + init { + require(pathFromProjectRoot.startsWith("/")) { + "File path going from the project's root must begin with \"/\" to not confuse it with relative paths." + } + } + + /** + * Returns the string contents of this file. + * + * The contents must be complete, as if the user themselves wrote it. For Kotlin files, + * it should return Kotlin source code (including the package and all import statements). + * For `.md` files, it should return valid Markdown documentation. + * + * These contents will be used to populate the real input file to be used by Dokka. + */ + abstract fun getContents(): String +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt new file mode 100644 index 00000000..9c0fa936 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.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.analysis.test.api + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.analysis.TestAnalysisContext +import org.jetbrains.dokka.analysis.test.api.analysis.TestAnalysisServices +import org.jetbrains.dokka.analysis.test.api.analysis.TestProjectAnalyzer +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaConfigurationBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.util.withTempDirectory +import org.jetbrains.dokka.model.DModule + +/** + * Represents a virtual test project (as if it's user-defined) that will be used to run Dokka. + * + * A test project consists of some Dokka configuration (represented as [TestDokkaConfiguration]) + * and some project-specific data like source code and markdown files (represented as [TestData]). + * + * See [kotlinJvmTestProject], [javaTestProject] and [mixedJvmTestProject] for convenient ways + * of bootstrapping test projects. + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + */ +interface TestProject { + + /** + * Verifies that this project is valid from the user's and Dokka's perspectives. + * Exists to save time with debugging difficult to catch mistakes, such as copy-pasted + * test data that is not applicable to this project. + * + * Must throw an exception if there's misconfiguration, incorrect / corrupted test data + * or API misuse. + * + * Verification is performed before running Dokka on this project. + */ + fun verify() + + /** + * Returns the configuration of this project, which will then be mapped to [DokkaConfiguration]. + * + * This is typically constructed using [BaseTestDokkaConfigurationBuilder]. + */ + fun getConfiguration(): TestDokkaConfiguration + + /** + * Returns this project's test data - a collection of source code files, markdown files + * and whatever else that can be usually found in a user-defined project. + */ + fun getTestData(): TestData +} + +/** + * Runs Dokka on the given [TestProject] and returns the generated documentable model. + * + * Can be used to verify the resulting documentable model, to check that + * everything was parsed and converted correctly. + * + * Usage example: + * ```kotlin + * val testProject = kotlinJvmTestProject { + * ... + * } + * + * val module: DModule = testProject.parse() + * ``` + */ +fun TestProject.parse(): DModule = TestProjectAnalyzer.parse(this) + +/** + * Runs Dokka on the given [TestProject] and provides not only the resulting documentable model, + * but analysis context and configuration as well, which gives you the ability to call public + * analysis services. + * + * Usage example: + * + * ```kotlin + * val testProject = kotlinJvmTestProject { + * ... + * } + * + * testProject.useServices { context -> + * val pckg: DPackage = context.module.packages.single() + * + * // use `moduleAndPackageDocumentationReader` service to get documentation of a package + * val allPackageDocs: SourceSetDependent = moduleAndPackageDocumentationReader.read(pckg) + * } + * ``` + */ +fun TestProject.useServices(block: TestAnalysisServices.(context: TestAnalysisContext) -> Unit) { + withTempDirectory { tempDirectory -> + val (services, context) = TestProjectAnalyzer.analyze(this, tempDirectory) + services.block(context) + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProjectFactory.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProjectFactory.kt new file mode 100644 index 00000000..81a20243 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProjectFactory.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api + +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.kotlin.KotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.mixed.MixedJvmTestProject +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * Creates a single-target Kotlin/JVM test project that only has Kotlin source code. + * + * See [javaTestProject] and [mixedJvmTestProject] if you want to check interoperability + * with other JVM languages. + * + * By default, the sources are put in `/src/main/kotlin`, and the JVM version of Kotlin's + * standard library is available on classpath. + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + * + * @sample org.jetbrains.dokka.analysis.test.jvm.kotlin.SampleKotlinJvmAnalysisTest.sample + */ +fun kotlinJvmTestProject(init: (@AnalysisTestDslMarker KotlinJvmTestProject).() -> Unit): TestProject { + val testData = KotlinJvmTestProject() + testData.init() + return testData +} + +/** + * Creates a Java-only test project. + * + * This can be used to test Dokka's Java support or specific + * corner cases related to parsing Java sources. + * + * By default, the sources are put in `/src/main/java`. No Kotlin source code is allowed. + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + * + * @sample org.jetbrains.dokka.analysis.test.jvm.java.SampleJavaAnalysisTest.sample + */ +fun javaTestProject(init: (@AnalysisTestDslMarker JavaTestProject).() -> Unit): TestProject { + val testData = JavaTestProject() + testData.init() + return testData +} + +/** + * Creates a project where a number of JVM language sources are allowed, + * like Java and Kotlin sources co-existing in the same source directory. + * + * This can be used to test interoperability between JVM languages. + * + * By default, this project consists of a single "jvm" source set, which has two source root directories: + * * `/src/main/kotlin` + * * `/src/main/java` + * + * See [parse] and [useServices] functions to learn how to run Dokka with this project as input. + * + * @sample org.jetbrains.dokka.analysis.test.jvm.mixed.SampleMixedJvmAnalysisTest.sample + */ +fun mixedJvmTestProject(init: (@AnalysisTestDslMarker MixedJvmTestProject).() -> Unit): TestProject { + val testProject = MixedJvmTestProject() + testProject.init() + return testProject +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisContext.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisContext.kt new file mode 100644 index 00000000..de6efb1b --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisContext.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.analysis.test.api.analysis + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext + +/** + * Context and data gathered during the analysis of a [TestProject]. + */ +class TestAnalysisContext( + + /** + * The actual [DokkaContext] that was used to run Dokka. + * + * Includes all plugins and classes available on classpath during the analysis. + */ + val context: DokkaContext, + + /** + * The actual [DokkaConfiguration] that was used to run Dokka. + * + * It was initially mapped from [TestDokkaConfiguration], and then added to by Dokka itself. + */ + val configuration: DokkaConfiguration, + + /** + * The entry point to the documentable model of the analyzed [TestProject]. + */ + val module: DModule +) diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt new file mode 100644 index 00000000..ab70bbd4 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.analysis + +import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.internal.ModuleAndPackageDocumentationReader +import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory + +/** + * Services exposed in [KotlinAnalysisPlugin] that are ready to be used. + * + * This class exists purely for convenience and to reduce boilerplate in tests. + * It is analogous to calling `context.plugin().querySingle { serviceName }`. + */ +class TestAnalysisServices( + val sampleProviderFactory: SampleProviderFactory, + val moduleAndPackageDocumentationReader: ModuleAndPackageDocumentationReader +) diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt new file mode 100644 index 00000000..1668b53f --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt @@ -0,0 +1,223 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.analysis + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin +import org.jetbrains.dokka.analysis.test.api.TestDataFile +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.configuration.toDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.parse +import org.jetbrains.dokka.analysis.test.api.useServices +import org.jetbrains.dokka.analysis.test.api.util.withTempDirectory +import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.transformers.documentation.DefaultDocumentableMerger +import org.jetbrains.dokka.transformers.documentation.DocumentableMerger +import org.jetbrains.dokka.transformers.sources.SourceToDocumentableTranslator +import org.jetbrains.dokka.utilities.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.LoggingLevel +import java.io.File + +/** + * The main logger used for running Dokka and analyzing projects. + * + * Changing the level to [LoggingLevel.DEBUG] can help with debugging faulty tests + * or tricky corner cases. + */ +val analysisLogger = DokkaConsoleLogger(minLevel = LoggingLevel.INFO) + +/** + * Analyzer of the test projects, it is essentially a very simple Dokka runner. + * + * Takes all virtual files of the given [TestProject], creates the real files for + * them in a temporary directory, and then runs Dokka with this temporary directory + * as the input user project. This allows us to simulate Dokka's behavior and results + * on a made-up project as if it were real and run via the CLI runner. + * + * Executes only a limited number of steps and uses a small subset of [CoreExtensions] + * that are necessary to test the analysis logic. + * + * Works only with single-module projects, where the source code of this project + * resides in the root `src` directory. Works with multiple source sets and targets, + * so both simple Kotlin/JVM and more complicated Kotlin Multiplatform project must work. + */ +object TestProjectAnalyzer { + + /** + * A quick way to analyze a [TestProject], for cases when only the documentable + * model is needed to verify the result. + * + * Creates the input test files, runs Dokka and then deletes them right after the documentable + * model has been created, leaving no trailing files or any other garbage behind. + * + * @see [TestProject.parse] for a user-friendly way to call it + */ + fun parse(testProject: TestProject): DModule { + // since we only need documentables, we can delete the input test files right away + return withTempDirectory(analysisLogger) { tempDirectory -> + val (_, context) = testProject.initialize(outputDirectory = tempDirectory) + generateDocumentableModel(context) + } + } + + /** + * Works in the same way as [parse], but it returns the context and configuration used for + * running Dokka, and does not delete the input test files at the end of the execution - it + * must be taken care of on call site. + * + * @param persistentDirectory a directory that will be used to generate the input test files into. + * It must be available during the test run, especially if services are used, + * otherwise parts of Dokka might not work as expected. Can be safely deleted + * at the end of the test after all asserts have been run. + * + * @see [TestProject.useServices] for a user-friendly way to call it + */ + fun analyze( + testProject: TestProject, + persistentDirectory: File + ): Pair { + val (dokkaConfiguration, dokkaContext) = testProject.initialize(outputDirectory = persistentDirectory) + val analysisServices = createTestAnalysisServices(dokkaContext) + val testAnalysisContext = TestAnalysisContext( + context = dokkaContext, + configuration = dokkaConfiguration, + module = generateDocumentableModel(dokkaContext) + ) + return analysisServices to testAnalysisContext + } + + /** + * Prepares this [TestProject] for analysis by creating + * the test files, setting up context and configuration. + */ + private fun TestProject.initialize(outputDirectory: File): Pair { + analysisLogger.progress("Initializing and verifying project $this") + this.verify() + require(outputDirectory.isDirectory) { + "outputDirectory has to exist and be a directory: $outputDirectory" + } + this.initializeTestFiles(relativeToDir = outputDirectory) + + analysisLogger.progress("Creating configuration and context") + val testDokkaConfiguration = this.getConfiguration() + val dokkaConfiguration = testDokkaConfiguration.toDokkaConfiguration(projectDir = outputDirectory).also { + it.verify() + } + return dokkaConfiguration to createContext(dokkaConfiguration) + } + + /** + * Takes the virtual [TestDataFile] of this [TestProject] and creates + * the real files relative to the [relativeToDir] param. + */ + private fun TestProject.initializeTestFiles(relativeToDir: File) { + analysisLogger.progress("Initializing test files relative to the \"$relativeToDir\" directory") + + this.getTestData().getFiles().forEach { + val testDataFile = relativeToDir.resolve(it.pathFromProjectRoot.removePrefix("/")) + try { + testDataFile.parentFile.mkdirs() + } catch (e: Exception) { + // the IOException thrown from `mkdirs()` has no details and thus is more difficult to debug. + throw IllegalStateException("Unable to create dirs \"${testDataFile.parentFile}\"", e) + } + + analysisLogger.debug("Creating \"${testDataFile.absolutePath}\"") + check(testDataFile.createNewFile()) { + "Unable to create a test file: ${testDataFile.absolutePath}" + } + testDataFile.writeText(it.getContents(), Charsets.UTF_8) + } + } + + /** + * Verifies this [DokkaConfiguration] to make sure there are no unexpected + * parameter option values, such as non-existing classpath entries. + * + * If this method fails, it's likely there's a configuration error in the test, + * or an exception must be made in one of the checks. + */ + private fun DokkaConfiguration.verify() { + this.includes.forEach { verifyFileExists(it) } + this.sourceSets.forEach { sourceSet -> + sourceSet.classpath.forEach { verifyFileExists(it) } + sourceSet.includes.forEach { verifyFileExists(it) } + sourceSet.samples.forEach { verifyFileExists(it) } + // we do not verify sourceRoots since the source directory + // is not guaranteed to exist even if it was configured. + } + } + + private fun verifyFileExists(file: File) { + if (!file.exists() && !file.absolutePath.contains("non-existing")) { + throw IllegalArgumentException( + "The provided file does not exist. Bad test data or configuration? " + + "If it is done intentionally, add \"non-existing\" to the path or the name. File: \"$file\"" + ) + } + } + + private fun createContext(dokkaConfiguration: DokkaConfiguration): DokkaContext { + analysisLogger.progress("Creating DokkaContext from test configuration") + return DokkaContext.create( + configuration = dokkaConfiguration, + logger = analysisLogger, + pluginOverrides = listOf() + ) + } + + /** + * Generates the documentable model by using all available [SourceToDocumentableTranslator] extensions, + * and then merging all the results into a single [DModule] by calling [DocumentableMerger]. + */ + private fun generateDocumentableModel(context: DokkaContext): DModule { + analysisLogger.progress("Generating the documentable model") + val sourceSetModules = context + .configuration + .sourceSets + .map { sourceSet -> translateSources(sourceSet, context) } + .flatten() + + if (sourceSetModules.isEmpty()) { + throw IllegalStateException("Got no modules after translating sources. Is the test data set up?") + } + + return DefaultDocumentableMerger(context).invoke(sourceSetModules) + ?: error("Unable to merge documentables for some reason") + } + + /** + * Translates input source files to the documentable model by using + * all registered [SourceToDocumentableTranslator] core extensions. + */ + private fun translateSources(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): List { + val translators = context[CoreExtensions.sourceToDocumentableTranslator] + require(translators.isNotEmpty()) { + "Need at least one source to documentable translator to run tests, otherwise no data will be generated." + } + analysisLogger.debug("Translating sources for ${sourceSet.sourceSetID}") + return translators.map { it.invoke(sourceSet, context) } + } + + /** + * A helper function to query analysis services, to avoid + * boilerplate and misconfiguration in the tests. + * + * The idea is to provide the users with ready-to-use services, + * without them having to know how to query or configure them. + */ + private fun createTestAnalysisServices(context: DokkaContext): TestAnalysisServices { + analysisLogger.progress("Creating analysis services") + val internalPlugin = context.plugin() + return TestAnalysisServices( + sampleProviderFactory = internalPlugin.querySingle { sampleProviderFactory }, + moduleAndPackageDocumentationReader = internalPlugin.querySingle { moduleAndPackageDocumentationReader } + ) + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfiguration.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfiguration.kt new file mode 100644 index 00000000..5c5a0daf --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfiguration.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.configuration + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.test.api.TestProject + +/** + * Configuration options for [TestProject]. + * + * Represents a trimmed-down version of [DokkaConfiguration] that only + * exposes properties that can be used by Dokka's analysis implementations. + */ +data class TestDokkaConfiguration( + + /** + * Name of this [TestProject]. + * + * @see DokkaConfiguration.moduleName + */ + val moduleName: String, + + /** + * References Markdown files that contain documentation for this module and packages. + * + * Contains paths relative to the root of [TestProject], so it must begin with `/`. + * + * Example: `/docs/module.md` + * + * @see DokkaConfiguration.includes + * @see https://kotlinlang.org/docs/dokka-module-and-package-docs.html + */ + val includes: Set = emptySet(), + + /** + * A number of source directories and their configuration options that + * make up this project. + * + * A multiplatform Kotlin project will typically have multiple source sets + * for the supported platforms, whereas a single Kotlin/JVM project will have only one. + * + * @see TestDokkaSourceSet + */ + val sourceSets: Set + + +) { + override fun toString(): String { + return "TestDokkaConfiguration(moduleName='$moduleName', includes=$includes, sourceSets=$sourceSets)" + } +} + +/** + * Configuration options for a collection of source files for a specific platform. + * + * Represents a trimmed-down version of [DokkaConfiguration.DokkaSourceSet] that only + * exposes properties that can be used by Dokka's analysis implementations. + * + * @see https://kotlinlang.org/docs/multiplatform-discover-project.html#source-sets + */ +data class TestDokkaSourceSet ( + + /** + * @see DokkaConfiguration.DokkaSourceSet.analysisPlatform + */ + val analysisPlatform: Platform, + + /** + * A unique identifier of this source set in the scope of the project. + * + * It must be unique even if two source sets have the same name, + * the same platform or the same configuration. + * + * @see DokkaConfiguration.DokkaSourceSet.sourceSetID + */ + val sourceSetID: DokkaSourceSetID, + + /** + * A set of source set ids that this source set depends on. + * + * @see DokkaConfiguration.DokkaSourceSet.dependentSourceSets + */ + val dependentSourceSets: Set, + + /** + * A set of directories that contain the source files for this source set. + * + * A source set typically has only one source root directory, but there can be additional + * ones if the project has generated sources that reside separately, or if the `@sample` + * tag is used, and the samples reside in a different directory. + * + * Contains paths relative to the root of [TestProject], so it must begin with `/`. + * + * @see DokkaConfiguration.DokkaSourceSet.sourceRoots + */ + val sourceRoots: Set, + + /** + * A set of JARs to be used for analyzing sources. + * + * If this [TestProject] exposes types from external libraries, Dokka needs to know + * about these libraries so that it can generate correct signatures and documentation. + * + * Contains absolute paths, can be any file, even if it has nothing to do with unit + * tests or test project. + * + * @see DokkaConfiguration.DokkaSourceSet.classpath + */ + val classpath: Set, + + /** + * References Markdown files that contain documentation for this module and packages. + * + * Contains paths relative to the root of [TestProject], so it must begin with `/`. + * + * Example: `/docs/module.md` + * + * @see DokkaConfiguration.DokkaSourceSet.includes + * @see https://kotlinlang.org/docs/dokka-module-and-package-docs.html + */ + val includes: Set = emptySet(), + + /** + * A set of Kotlin files with functions that show how to use certain API. + * + * Contains paths relative to the root of [TestProject], so it must begin with `/`. + * + * Example: `/samples/collectionSamples.kt` + * + * @see DokkaConfiguration.DokkaSourceSet.samples + */ + val samples: Set = emptySet(), + + /** + * Compatibility mode for Kotlin language version X.Y. + * + * Example: `1.9` + * + * @see https://kotlinlang.org/docs/compatibility-modes.html + */ + val languageVersion: String? = null, + + /** + * Compatibility mode for Kotlin API version X.Y. + * + * Example: `1.9` + * + * @see https://kotlinlang.org/docs/compatibility-modes.html + */ + val apiVersion: String? = null, + + +) { + override fun toString(): String { + return "TestDokkaSourceSet(" + + "analysisPlatform=$analysisPlatform, " + + "sourceSetID=$sourceSetID, " + + "dependentSourceSets=$dependentSourceSets, " + + "sourceRoots=$sourceRoots, " + + "classpath=$classpath, " + + "includes=$includes, " + + "samples=$samples, " + + "languageVersion=$languageVersion, " + + "apiVersion=$apiVersion" + + ")" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationBuilder.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationBuilder.kt new file mode 100644 index 00000000..b6563fb7 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationBuilder.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.configuration + +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * A builder for [TestDokkaConfiguration] that contains base options. + * + * Implementations can override the properties to modify the resulting test + * data or prohibit setting certain properties. + */ +@AnalysisTestDslMarker +abstract class BaseTestDokkaConfigurationBuilder { + + /** + * @see TestDokkaConfiguration.moduleName + */ + abstract var moduleName: String + + + /** + * @see TestDokkaConfiguration.includes + */ + open var includes: Set = emptySet() + + /** + * Verifies that this source set configuration is valid. For example, + * it should check that all file paths are set in the expected format. + * + * Must be invoked manually as part of [TestProject.verify]. + */ + open fun verify() { + includes.forEach { + verifyFilePathStartsWithSlash("includes", it) + verifyFileExtension("includes", it, ".md") + } + } + + abstract fun build(): TestDokkaConfiguration + + override fun toString(): String { + return "BaseTestDokkaConfigurationBuilder(moduleName='$moduleName', includes=$includes)" + } +} + +/** + * A builder for [TestDokkaSourceSet] that contains base options. + * + * Implementations can override the properties to modify the resulting test + * data or prohibit setting certain properties. + */ +@AnalysisTestDslMarker +abstract class BaseTestDokkaSourceSetBuilder { + + /** + * Directories **additional** to the default source roots. + * + * @see TestDokkaSourceSet.sourceRoots + */ + open var additionalSourceRoots: Set = emptySet() + + /** + * JARs **additional** to the default classpath. + * + * @see TestDokkaSourceSet.classpath + */ + open var additionalClasspath: Set = emptySet() + + /** + * @see TestDokkaSourceSet.includes + */ + open var includes: Set = emptySet() + + /** + * @see TestDokkaSourceSet.samples + */ + open var samples: Set = emptySet() + + /** + * @see TestDokkaSourceSet.languageVersion + */ + open var languageVersion: String? = null + + /** + * @see TestDokkaSourceSet.apiVersion + */ + open var apiVersion: String? = null + + /** + * Verifies that this source set configuration is valid. For example, + * it should check that all file paths are set in the expected format. + * + * Must be invoked manually during the verification of the + * higher-level [BaseTestDokkaConfigurationBuilder.verify]. + */ + open fun verify() { + additionalSourceRoots.forEach { + verifyFilePathStartsWithSlash("additionalSourceRoots", it) + } + additionalClasspath.forEach { + // this check can be extended to accept .klib, .class or other files + // as the need for it arises, as long as Dokka supports it + verifyFileExtension("additionalClasspath", it, ".jar") + } + includes.forEach { + verifyFilePathStartsWithSlash("includes", it) + verifyFileExtension("includes", it, ".md") + } + samples.forEach { + verifyFilePathStartsWithSlash("samples", it) + verifyFileExtension("samples", it, ".kt") + } + } + + abstract fun build(): TestDokkaSourceSet + + override fun toString(): String { + return "BaseTestDokkaSourceSetBuilder(" + + "additionalSourceRoots=$additionalSourceRoots, " + + "additionalClasspath=$additionalClasspath, " + + "includes=$includes, " + + "samples=$samples, " + + "languageVersion=$languageVersion, " + + "apiVersion=$apiVersion" + + ")" + } +} + +internal fun verifyFilePathStartsWithSlash(propertyName: String, path: String) { + require(path.startsWith("/")) { + "Property $propertyName must contain paths relative to the root of the project. " + + "Please, prefix it with \"/\" for readability and consistency." + } +} + +internal fun verifyFileExtension(propertyName: String, filePath: String, expectedExtension: String) { + require(filePath.endsWith(expectedExtension)) { + "Property $propertyName only accepts files with \"$expectedExtension\" extension. " + + "Got: \"${filePath.substringAfterLast("/")}\"." + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationMapper.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationMapper.kt new file mode 100644 index 00000000..f6356ab2 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/configuration/TestDokkaConfigurationMapper.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.configuration + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.test.api.util.mapToSet +import java.io.File + +/** + * Maps [TestDokkaConfiguration] to the actual [DokkaConfiguration] that will + * be used to run Dokka; The resulting configuration must be valid from Dokka's perspective. + * + * @receiver test configuration to map to the real one; all file paths must be relative to the + * root of the project. + * @param projectDir the actual project directory that will be used to create test files in; + * the path must be absolute and must exist. + */ +fun TestDokkaConfiguration.toDokkaConfiguration(projectDir: File): DokkaConfiguration { + require(projectDir.exists() && projectDir.isDirectory) { + "Expected the \"projectDir\" File param to exist and be a directory" + } + + val moduleName = this.moduleName + val includes = this.includes.mapToSet { it.relativeTo(projectDir) } + val sourceSets = this.sourceSets.map { it.toDokkaSourceSet(projectDir) } + + return object : DokkaConfiguration { + + /* + * NOTE: The getters need to return data that can be compared by value. + * This means you can't recreate lists of interfaces on every invocation + * as their equals will return false, leading to difficult to trace bugs, + * especially when it comes to `SourceSetDependent` + */ + + override val moduleName: String + get() = moduleName + + override val includes: Set + get() = includes + + override val sourceSets: List + get() = sourceSets + + /* + * The plugin API uses the properties below to initialize plugins found on classpath. + * They are not settable directly in analysis tests, but must not throw any exceptions. + */ + override val pluginsClasspath: List + get() = emptyList() + override val pluginsConfiguration: List + get() = emptyList() + + /* + * The properties below are not used by the analysis modules, + * and thus they don't need to be supported. + * + * If one of the properties below starts being used during + * analysis (i.e starts throwing an exception), a corresponding + * test property should be added along with the mapping. + */ + override val moduleVersion: String + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val outputDir: File + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val cacheRoot: File + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val offlineMode: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val failOnWarning: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val modules: List + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val delayTemplateSubstitution: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val suppressObviousFunctions: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val suppressInheritedMembers: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val finalizeCoroutines: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + } +} + +private fun TestDokkaSourceSet.toDokkaSourceSet(relativeToDir: File): DokkaConfiguration.DokkaSourceSet { + val analysisPlatform = this.analysisPlatform + val sourceSetID = this.sourceSetID + val dependentSourceSets = this.dependentSourceSets + val sourceRoots = this.sourceRoots.mapToSet { it.relativeTo(relativeToDir) } + val classpath = this.classpath.map { File(it) } + val includes = this.includes.mapToSet { it.relativeTo(relativeToDir) } + val samples = this.samples.mapToSet { it.relativeTo(relativeToDir) } + val languageVersion = this.languageVersion + val apiVersion = this.apiVersion + + return object : DokkaConfiguration.DokkaSourceSet { + + /* + * NOTE: The getters need to return data that can be compared by value. + * This means you can't recreate lists of interfaces on every invocation + * as their equals will return false, leading to difficult to trace bugs, + * especially when it comes to `SourceSetDependent` + */ + + override val analysisPlatform: Platform + get() = analysisPlatform + + override val sourceSetID: DokkaSourceSetID + get() = sourceSetID + + override val dependentSourceSets: Set + get() = dependentSourceSets + + override val sourceRoots: Set + get() = sourceRoots + + override val classpath: List + get() = classpath + + override val includes: Set + get() = includes + + override val samples: Set + get() = samples + + override val languageVersion: String? + get() = languageVersion + + override val apiVersion: String? + get() = apiVersion + + + /* + * The properties below are not used by the analysis modules, + * and thus they don't need to be supported. + * + * If one of the properties below starts being used during + * analysis (i.e starts throwing an exception), a corresponding + * test property should be added along with the mapping. + */ + override val displayName: String + get() = throw NotImplementedError("Not expected to be used by analysis modules") + @Suppress("OVERRIDE_DEPRECATION") + override val includeNonPublic: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val reportUndocumented: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val skipEmptyPackages: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val skipDeprecated: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val jdkVersion: Int + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val sourceLinks: Set + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val perPackageOptions: List + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val externalDocumentationLinks: Set + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val noStdlibLink: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val noJdkLink: Boolean + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val suppressedFiles: Set + get() = throw NotImplementedError("Not expected to be used by analysis modules") + override val documentedVisibilities: Set + get() = throw NotImplementedError("Not expected to be used by analysis modules") + } +} + +private fun String.relativeTo(dir: File): File { + return dir.resolve(this.removePrefix("/")) +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt new file mode 100644 index 00000000..6775fa21 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.jvm.java + +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaConfigurationBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaSourceSetBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaSourceSet +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * An implementation of [BaseTestDokkaConfigurationBuilder] specific to [JavaTestProject]. + */ +class JavaTestConfigurationBuilder : BaseTestDokkaConfigurationBuilder() { + override var moduleName: String = "javaTestProject" + + private val javaSourceSetBuilder = JavaTestSourceSetBuilder() + + @AnalysisTestDslMarker + fun javaSourceSet(fillSourceSet: JavaTestSourceSetBuilder.() -> Unit) { + fillSourceSet(javaSourceSetBuilder) + } + + override fun verify() { + super.verify() + javaSourceSetBuilder.verify() + } + + override fun build(): TestDokkaConfiguration { + return TestDokkaConfiguration( + moduleName = moduleName, + includes = includes, + sourceSets = setOf(javaSourceSetBuilder.build()) + ) + } +} + +/** + * An implementation of [BaseTestDokkaSourceSetBuilder] specific to [JavaTestProject]. + * + * Defines sensible defaults that should cover the majority of simple projects. + */ +class JavaTestSourceSetBuilder : BaseTestDokkaSourceSetBuilder() { + override fun build(): TestDokkaSourceSet { + return TestDokkaSourceSet( + analysisPlatform = Platform.jvm, + sourceSetID = DokkaSourceSetID(scopeId = "project", sourceSetName = "java"), + dependentSourceSets = setOf(), + sourceRoots = additionalSourceRoots + setOf(JavaTestProject.DEFAULT_SOURCE_ROOT), + classpath = additionalClasspath, // TODO [beresnev] is kotlin jvm stdlib needed here? + includes = includes, + samples = samples, + languageVersion = languageVersion, + apiVersion = apiVersion + ) + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaFileCreator.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaFileCreator.kt new file mode 100644 index 00000000..2a79210f --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaFileCreator.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.jvm.java + +/** + * Declares a capability that a `.java` file can be created in the scope of the implementation. + */ +interface JavaFileCreator { + + /** + * Creates a `.java` file. + * + * By default, the package of this file is deduced automatically from the [pathFromSrc] param. + * For example, for a path `org/jetbrains/dokka/test` the package will be `org.jetbrains.dokka.test`. + * It is normally prohibited for Java files to have a mismatch in package and file path, so it + * cannot be overridden. + * + * @param pathFromSrc path relative to the source code directory of the project. + * Must contain packages (if any) and end in `.java`. + * Example: `org/jetbrains/dokka/test/MyClass.java` + */ + fun javaFile(pathFromSrc: String, fillFile: JavaTestDataFile.() -> Unit) +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestData.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestData.kt new file mode 100644 index 00000000..48705e78 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestData.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.jvm.java + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile +import org.jetbrains.dokka.analysis.test.api.util.filePathToPackageName + +/** + * A container for populating and holding Java source code test data. + * + * This container exists so that common creation, population and verification logic + * can be reused, instead of having to implement [JavaFileCreator] multiple times. + * + * @param pathToJavaSources path to the `src` directory in which Java sources must reside. + * Must be relative to the root of the test project. Example: `/src/main/java` + * @see TestData + */ +class JavaTestData( + private val pathToJavaSources: String +) : TestData, JavaFileCreator { + + private val files = mutableListOf() + + override fun javaFile(pathFromSrc: String, fillFile: JavaTestDataFile.() -> Unit) { + val fileName = pathFromSrc.substringAfterLast("/") + check(fileName.endsWith(".java")) { "Java files are expected to have .java extension" } + + val testDataFile = JavaTestDataFile( + fullyQualifiedPackageName = filePathToPackageName(pathFromSrc), + pathFromProjectRoot = "$pathToJavaSources/$pathFromSrc", + ) + fillFile(testDataFile) + testDataFile.checkFileNameIsPresent(fileName) + files.add(testDataFile) + } + + private fun JavaTestDataFile.checkFileNameIsPresent(fileName: String) { + check(this.getContents().contains(fileName.removeSuffix(".java"))) { + "Expected the .java file name to be the same as the top-level declaration name (class/interface)." + } + } + + override fun getFiles(): List { + return files + } + + override fun toString(): String { + return "JavaTestData(pathToJavaSources='$pathToJavaSources', files=$files)" + } +} + diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestDataFile.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestDataFile.kt new file mode 100644 index 00000000..d8cc86a8 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestDataFile.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.jvm.java + +import org.jetbrains.dokka.analysis.test.api.TestDataFile + +class JavaTestDataFile( + fullyQualifiedPackageName: String, + pathFromProjectRoot: String, +) : TestDataFile(pathFromProjectRoot = pathFromProjectRoot) { + + private var fileContents = "package $fullyQualifiedPackageName;" + System.lineSeparator().repeat(2) + + operator fun String.unaryPlus() { + fileContents += (this.trimIndent() + System.lineSeparator()) + } + + override fun getContents(): String { + return fileContents + } + + override fun toString(): String { + return "JavaTestDataFile(pathFromProjectRoot='$pathFromProjectRoot', fileContents='$fileContents')" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.kt new file mode 100644 index 00000000..39f0f0f6 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.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.analysis.test.api.jvm.java + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.javaTestProject +import org.jetbrains.dokka.analysis.test.api.markdown.MarkdownTestData +import org.jetbrains.dokka.analysis.test.api.markdown.MarkdownTestDataFile +import org.jetbrains.dokka.analysis.test.api.markdown.MdFileCreator +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker +import org.jetbrains.dokka.analysis.test.api.util.flatListOf + +/** + * @see javaTestProject for an explanation and a convenient way to construct this project + */ +class JavaTestProject : TestProject, JavaFileCreator, MdFileCreator { + + private val projectConfigurationBuilder = JavaTestConfigurationBuilder() + + private val javaSourceSet = JavaTestData(pathToJavaSources = DEFAULT_SOURCE_ROOT) + private val markdownTestData = MarkdownTestData() + + @AnalysisTestDslMarker + fun dokkaConfiguration(fillConfiguration: JavaTestConfigurationBuilder.() -> Unit) { + fillConfiguration(projectConfigurationBuilder) + } + + @AnalysisTestDslMarker + override fun javaFile(pathFromSrc: String, fillFile: JavaTestDataFile.() -> Unit) { + javaSourceSet.javaFile(pathFromSrc, fillFile) + } + + @AnalysisTestDslMarker + override fun mdFile(pathFromProjectRoot: String, fillFile: MarkdownTestDataFile.() -> Unit) { + markdownTestData.mdFile(pathFromProjectRoot, fillFile) + } + + override fun verify() { + projectConfigurationBuilder.verify() + } + + override fun getConfiguration(): TestDokkaConfiguration { + return projectConfigurationBuilder.build() + } + + override fun getTestData(): TestData { + return object : TestData { + override fun getFiles(): List { + return flatListOf( + this@JavaTestProject.javaSourceSet.getFiles(), + this@JavaTestProject.markdownTestData.getFiles() + ) + } + } + } + + override fun toString(): String { + return "JavaTestProject(" + + "projectConfigurationBuilder=$projectConfigurationBuilder, " + + "javaSourceSet=$javaSourceSet, " + + "markdownTestData=$markdownTestData" + + ")" + } + + internal companion object { + internal const val DEFAULT_SOURCE_ROOT = "/src/main/java" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt new file mode 100644 index 00000000..a0255611 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.jvm.kotlin + +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaConfigurationBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaSourceSetBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaSourceSet +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * An implementation of [BaseTestDokkaConfigurationBuilder] specific to [KotlinJvmTestProject]. + */ +class KotlinJvmTestConfigurationBuilder : BaseTestDokkaConfigurationBuilder() { + override var moduleName: String = "kotlinJvmTestProject" + + private val kotlinSourceSetBuilder = KotlinJvmTestSourceSetBuilder() + + @AnalysisTestDslMarker + fun kotlinSourceSet(fillSourceSet: KotlinJvmTestSourceSetBuilder.() -> Unit) { + fillSourceSet(kotlinSourceSetBuilder) + } + + override fun verify() { + super.verify() + kotlinSourceSetBuilder.verify() + } + + override fun build(): TestDokkaConfiguration { + return TestDokkaConfiguration( + moduleName = moduleName, + includes = includes, + sourceSets = setOf(kotlinSourceSetBuilder.build()) + ) + } +} + +class KotlinJvmTestSourceSetBuilder : BaseTestDokkaSourceSetBuilder() { + override fun build(): TestDokkaSourceSet { + return TestDokkaSourceSet( + analysisPlatform = Platform.jvm, + sourceSetID = DokkaSourceSetID(scopeId = "project", sourceSetName = "kotlin"), + dependentSourceSets = setOf(), + sourceRoots = additionalSourceRoots + setOf(KotlinJvmTestProject.DEFAULT_SOURCE_ROOT), + classpath = additionalClasspath + setOf(getKotlinJvmStdlibJarPath()), + includes = includes, + samples = samples, + languageVersion = languageVersion, + apiVersion = apiVersion + ) + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmDependencyUtils.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmDependencyUtils.kt new file mode 100644 index 00000000..c90e126f --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmDependencyUtils.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.analysis.test.api.jvm.kotlin + +private val lazyKotlinJvmStdlibJar by lazy { + ClassLoader.getSystemResource("kotlin/jvm/Strictfp.class") + ?.file + ?.replace("file:", "") + ?.replaceAfter(".jar", "") + ?: error("Unable to find Kotlin's JVM stdlib jar.") +} + +/** + * Returns an absolute path to the JAR file of Kotlin's standard library for the JVM platform. + * + * Example: `~/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib/1.9.10/xx/kotlin-stdlib-1.9.10.jar` + */ +internal fun getKotlinJvmStdlibJarPath(): String { + return lazyKotlinJvmStdlibJar +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt new file mode 100644 index 00000000..178a1dc3 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.jvm.kotlin + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.kotlin.KotlinTestData +import org.jetbrains.dokka.analysis.test.api.kotlin.KotlinTestDataFile +import org.jetbrains.dokka.analysis.test.api.kotlin.KtFileCreator +import org.jetbrains.dokka.analysis.test.api.kotlin.sample.KotlinSampleFileCreator +import org.jetbrains.dokka.analysis.test.api.kotlin.sample.KotlinSampleTestData +import org.jetbrains.dokka.analysis.test.api.kotlin.sample.KotlinSampleTestDataFile +import org.jetbrains.dokka.analysis.test.api.kotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.markdown.MarkdownTestData +import org.jetbrains.dokka.analysis.test.api.markdown.MarkdownTestDataFile +import org.jetbrains.dokka.analysis.test.api.markdown.MdFileCreator +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker +import org.jetbrains.dokka.analysis.test.api.util.flatListOf + +/** + * @see kotlinJvmTestProject for an explanation and a convenient way to construct this project + */ +class KotlinJvmTestProject : TestProject, KtFileCreator, MdFileCreator, KotlinSampleFileCreator { + + private val projectConfigurationBuilder = KotlinJvmTestConfigurationBuilder() + + private val kotlinSourceSet = KotlinTestData(pathToKotlinSources = DEFAULT_SOURCE_ROOT) + private val markdownTestData = MarkdownTestData() + private val kotlinSampleTestData = KotlinSampleTestData() + + @AnalysisTestDslMarker + fun dokkaConfiguration(fillConfiguration: KotlinJvmTestConfigurationBuilder.() -> Unit) { + fillConfiguration(projectConfigurationBuilder) + } + + @AnalysisTestDslMarker + override fun ktFile(pathFromSrc: String, fqPackageName: String, fillFile: KotlinTestDataFile.() -> Unit) { + kotlinSourceSet.ktFile(pathFromSrc, fqPackageName, fillFile) + } + + @AnalysisTestDslMarker + override fun mdFile(pathFromProjectRoot: String, fillFile: MarkdownTestDataFile.() -> Unit) { + markdownTestData.mdFile(pathFromProjectRoot, fillFile) + } + + @AnalysisTestDslMarker + override fun sampleFile( + pathFromProjectRoot: String, + fqPackageName: String, + fillFile: KotlinSampleTestDataFile.() -> Unit + ) { + kotlinSampleTestData.sampleFile(pathFromProjectRoot, fqPackageName, fillFile) + } + + override fun verify() { + projectConfigurationBuilder.verify() + } + + override fun getConfiguration(): TestDokkaConfiguration { + return projectConfigurationBuilder.build() + } + + override fun getTestData(): TestData { + return object : TestData { + override fun getFiles(): List { + return flatListOf( + this@KotlinJvmTestProject.kotlinSourceSet.getFiles(), + this@KotlinJvmTestProject.markdownTestData.getFiles(), + this@KotlinJvmTestProject.kotlinSampleTestData.getFiles() + ) + } + } + } + + override fun toString(): String { + return "KotlinJvmTestProject(" + + "projectConfigurationBuilder=$projectConfigurationBuilder, " + + "kotlinSourceSet=$kotlinSourceSet, " + + "markdownTestData=$markdownTestData, " + + "kotlinSampleTestData=$kotlinSampleTestData" + + ")" + } + + internal companion object { + internal const val DEFAULT_SOURCE_ROOT = "/src/main/kotlin" + } +} + + diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmConfigurationBuilder.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmConfigurationBuilder.kt new file mode 100644 index 00000000..16af6e7d --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmConfigurationBuilder.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.analysis.test.api.jvm.mixed + +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.Platform +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaConfigurationBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.BaseTestDokkaSourceSetBuilder +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaSourceSet +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.kotlin.KotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.kotlin.getKotlinJvmStdlibJarPath +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * An implementation of [BaseTestDokkaConfigurationBuilder] specific to [MixedJvmTestProject]. + */ +class MixedJvmTestConfigurationBuilder : BaseTestDokkaConfigurationBuilder() { + override var moduleName: String = "mixedJvmTestProject" + + private val mixedJvmSourceSetBuilder = MixedJvmTestSourceSetBuilder() + + @AnalysisTestDslMarker + fun jvmSourceSet(fillSourceDir: MixedJvmTestSourceSetBuilder.() -> Unit) { + fillSourceDir(mixedJvmSourceSetBuilder) + } + + override fun verify() { + super.verify() + mixedJvmSourceSetBuilder.verify() + } + + override fun build(): TestDokkaConfiguration { + return TestDokkaConfiguration( + moduleName = moduleName, + includes = includes, + sourceSets = setOf(mixedJvmSourceSetBuilder.build()) + ) + } + + override fun toString(): String { + return "MixedJvmTestConfigurationBuilder(" + + "moduleName='$moduleName', " + + "jvmSourceSetBuilder=$mixedJvmSourceSetBuilder" + + ")" + } +} + +class MixedJvmTestSourceSetBuilder : BaseTestDokkaSourceSetBuilder() { + override fun build(): TestDokkaSourceSet { + return TestDokkaSourceSet( + analysisPlatform = Platform.jvm, + sourceSetID = DokkaSourceSetID(scopeId = "project", sourceSetName = "jvm"), + dependentSourceSets = setOf(), + sourceRoots = additionalSourceRoots + setOf( + KotlinJvmTestProject.DEFAULT_SOURCE_ROOT, + JavaTestProject.DEFAULT_SOURCE_ROOT + ), + classpath = additionalClasspath + setOf(getKotlinJvmStdlibJarPath()), + includes = includes, + samples = samples, + languageVersion = languageVersion, + apiVersion = apiVersion + ) + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestData.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestData.kt new file mode 100644 index 00000000..24889fe2 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestData.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.analysis.test.api.jvm.mixed + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaFileCreator +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaTestData +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaTestDataFile +import org.jetbrains.dokka.analysis.test.api.kotlin.KotlinTestData +import org.jetbrains.dokka.analysis.test.api.kotlin.KotlinTestDataFile +import org.jetbrains.dokka.analysis.test.api.kotlin.KtFileCreator +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker + +/** + * A container for populating and holding Kotlin and Java source code test data. + * + * This container exists so that common creation, population and verification logic + * can be reused, instead of having to implement [KtFileCreator] and [JavaFileCreator] multiple times. + * + * @see TestData + */ +class MixedJvmTestData(pathToSources: String) : TestData, KtFileCreator, JavaFileCreator { + + private val javaTestData = JavaTestData(pathToJavaSources = pathToSources) + private val kotlinJvmTestData = KotlinTestData(pathToKotlinSources = pathToSources) + + @AnalysisTestDslMarker + override fun javaFile(pathFromSrc: String, fillFile: JavaTestDataFile.() -> Unit) { + javaTestData.javaFile(pathFromSrc, fillFile) + } + + @AnalysisTestDslMarker + override fun ktFile(pathFromSrc: String, fqPackageName: String, fillFile: KotlinTestDataFile.() -> Unit) { + kotlinJvmTestData.ktFile(pathFromSrc, fqPackageName, fillFile) + } + + override fun getFiles(): List { + return javaTestData.getFiles() + kotlinJvmTestData.getFiles() + } + + override fun toString(): String { + return "MixedJvmTestData(javaTestData=$javaTestData, kotlinJvmTestData=$kotlinJvmTestData)" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt new file mode 100644 index 00000000..32b7ce0a --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.jvm.mixed + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile +import org.jetbrains.dokka.analysis.test.api.TestProject +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.kotlin.KotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.markdown.MarkdownTestData +import org.jetbrains.dokka.analysis.test.api.markdown.MarkdownTestDataFile +import org.jetbrains.dokka.analysis.test.api.markdown.MdFileCreator +import org.jetbrains.dokka.analysis.test.api.mixedJvmTestProject +import org.jetbrains.dokka.analysis.test.api.util.AnalysisTestDslMarker +import org.jetbrains.dokka.analysis.test.api.util.flatListOf + +/** + * @see mixedJvmTestProject for an explanation and a convenient way to construct this project + */ +class MixedJvmTestProject : TestProject, MdFileCreator { + + private val projectConfigurationBuilder = MixedJvmTestConfigurationBuilder() + + private val kotlinSourceDirectory = MixedJvmTestData(pathToSources = KotlinJvmTestProject.DEFAULT_SOURCE_ROOT) + private val javaSourceDirectory = MixedJvmTestData(pathToSources = JavaTestProject.DEFAULT_SOURCE_ROOT) + private val markdownTestData = MarkdownTestData() + + @AnalysisTestDslMarker + fun dokkaConfiguration(fillConfiguration: MixedJvmTestConfigurationBuilder.() -> Unit) { + fillConfiguration(projectConfigurationBuilder) + } + + @AnalysisTestDslMarker + fun kotlinSourceDirectory(fillTestData: MixedJvmTestData.() -> Unit) { + fillTestData(kotlinSourceDirectory) + } + + @AnalysisTestDslMarker + fun javaSourceDirectory(fillTestData: MixedJvmTestData.() -> Unit) { + fillTestData(javaSourceDirectory) + } + + @AnalysisTestDslMarker + override fun mdFile(pathFromProjectRoot: String, fillFile: MarkdownTestDataFile.() -> Unit) { + markdownTestData.mdFile(pathFromProjectRoot, fillFile) + } + + override fun verify() { + projectConfigurationBuilder.verify() + } + + override fun getConfiguration(): TestDokkaConfiguration { + return projectConfigurationBuilder.build() + } + + override fun getTestData(): TestData { + return object : TestData { + override fun getFiles(): List { + return flatListOf( + this@MixedJvmTestProject.kotlinSourceDirectory.getFiles(), + this@MixedJvmTestProject.javaSourceDirectory.getFiles(), + this@MixedJvmTestProject.markdownTestData.getFiles() + ) + } + } + } + + override fun toString(): String { + return "MixedJvmTestProject(" + + "projectConfigurationBuilder=$projectConfigurationBuilder, " + + "kotlinSourceDirectory=$kotlinSourceDirectory, " + + "javaSourceDirectory=$javaSourceDirectory, " + + "markdownTestData=$markdownTestData" + + ")" + } +} + diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestData.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestData.kt new file mode 100644 index 00000000..02911235 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestData.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.kotlin + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile + +/** + * A container for populating and holding Kotlin source code test data. + * + * This container exists so that common creation, population and verification logic + * can be reused, instead of having to implement [KtFileCreator] multiple times. + * + * @param pathToKotlinSources path to the `src` directory in which Kotlin sources must reside. + * Must be relative to the root of the test project. Example: `/src/main/kotlin` + * @see TestData + */ +class KotlinTestData( + private val pathToKotlinSources: String +) : TestData, KtFileCreator { + + private val files = mutableListOf() + + override fun ktFile( + pathFromSrc: String, + fqPackageName: String, + fillFile: KotlinTestDataFile.() -> Unit + ) { + check(pathFromSrc.endsWith(".kt")) { "Kotlin files are expected to have .kt extension" } + + val testDataFile = KotlinTestDataFile( + pathFromProjectRoot = "$pathToKotlinSources/$pathFromSrc", + fqPackageName = fqPackageName, + ) + fillFile(testDataFile) + files.add(testDataFile) + } + + override fun getFiles(): List { + return files + } + + override fun toString(): String { + return "KotlinTestData(pathToKotlinSources='$pathToKotlinSources', files=$files)" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestDataFile.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestDataFile.kt new file mode 100644 index 00000000..355440e7 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KotlinTestDataFile.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.kotlin + +import org.jetbrains.dokka.analysis.test.api.TestDataFile + +class KotlinTestDataFile( + pathFromProjectRoot: String, + fqPackageName: String, +) : TestDataFile(pathFromProjectRoot = pathFromProjectRoot) { + + private var fileContents = "package $fqPackageName" + System.lineSeparator().repeat(2) + + operator fun String.unaryPlus() { + fileContents += (this.trimIndent() + System.lineSeparator()) + } + + override fun getContents(): String { + return fileContents + } + + override fun toString(): String { + return "KotlinTestDataFile(pathFromProjectRoot='$pathFromProjectRoot', fileContents='$fileContents')" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KtFileCreator.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KtFileCreator.kt new file mode 100644 index 00000000..15ce31ce --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/KtFileCreator.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.analysis.test.api.kotlin + +import org.jetbrains.dokka.analysis.test.api.util.filePathToPackageName + +/** + * Declares a capability that a `.kt` file can be created in the scope of the implementation. + */ +interface KtFileCreator { + + /** + * Creates a `.kt` file. + * + * By default, the package of this file is deduced automatically from the [pathFromSrc] param. + * For example, for a path `org/jetbrains/dokka/test` the package will be `org.jetbrains.dokka.test`. + * The package can be overridden by setting [fqPackageName]. + * + * @param pathFromSrc path relative to the source code directory of the project. + * Must contain packages (if any) and end in `.kt`. + * Example: `org/jetbrains/dokka/test/File.kt` + * @param fqPackageName package name to be used in the `package` statement of this file. + * This value overrides the automatically deduced package. + */ + fun ktFile( + pathFromSrc: String, + fqPackageName: String = filePathToPackageName(pathFromSrc), + fillFile: KotlinTestDataFile.() -> Unit + ) +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleFileCreator.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleFileCreator.kt new file mode 100644 index 00000000..7fa1d28d --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleFileCreator.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.analysis.test.api.kotlin.sample + +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaSourceSet + +/** + * Declares a capability that a `.kt` file can be created that will be used as a usage sample. + */ +interface KotlinSampleFileCreator { + + /** + * Creates a `.kt` file outside of the source code directory. It should be used as input + * for the `@sample` KDoc tag. + * + * To be picked by Dokka, this file must be included in [TestDokkaSourceSet.samples]. + * + * @param pathFromProjectRoot path relative to the root of the test project. Must begin + * with `/` to not confuse it with relative paths. Example: `/samples/collections.kt` + * @param fqPackageName fully qualified package name to be used in the `package` statement of the file. + * This parameter must be set because the package name cannot be deduced + * from the file path, as samples usually reside outside of the source code directory. + * Example: `org.jetbrains.dokka.sample.collections` + */ + fun sampleFile( + pathFromProjectRoot: String, + fqPackageName: String, + fillFile: KotlinSampleTestDataFile.() -> Unit + ) +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestData.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestData.kt new file mode 100644 index 00000000..9ba4187c --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestData.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.analysis.test.api.kotlin.sample + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile + +/** + * A container for populating and holding Kotlin sample test data. + * + * This container exists so that common creation, population and verification logic + * can be reused, instead of having to implement [KotlinSampleFileCreator] multiple times. + * + * @see TestData + */ +class KotlinSampleTestData : TestData, KotlinSampleFileCreator { + + private val files = mutableListOf() + + override fun sampleFile( + pathFromProjectRoot: String, + fqPackageName: String, + fillFile: KotlinSampleTestDataFile.() -> Unit + ) { + check(pathFromProjectRoot.endsWith(".kt")) { "Kotlin sample files are expected to have .kt extension" } + + val testDataFile = KotlinSampleTestDataFile( + pathFromProjectRoot = pathFromProjectRoot, + fqPackageName = fqPackageName + ) + fillFile(testDataFile) + files.add(testDataFile) + } + + override fun getFiles(): List { + return files + } + + override fun toString(): String { + return "KotlinSampleTestData(files=$files)" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestDataFile.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestDataFile.kt new file mode 100644 index 00000000..6dc8230b --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/kotlin/sample/KotlinSampleTestDataFile.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.kotlin.sample + +import org.jetbrains.dokka.analysis.test.api.TestDataFile + +class KotlinSampleTestDataFile( + pathFromProjectRoot: String, + fqPackageName: String, +) : TestDataFile(pathFromProjectRoot = pathFromProjectRoot) { + + private var fileContents = "package $fqPackageName" + System.lineSeparator().repeat(2) + + operator fun String.unaryPlus() { + fileContents += (this.trimIndent() + System.lineSeparator()) + } + + override fun getContents(): String { + return fileContents + } + + override fun toString(): String { + return "KotlinSampleTestDataFile(pathFromProjectRoot='$pathFromProjectRoot', fileContents='$fileContents')" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestData.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestData.kt new file mode 100644 index 00000000..d0cb64fb --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestData.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.markdown + +import org.jetbrains.dokka.analysis.test.api.TestData +import org.jetbrains.dokka.analysis.test.api.TestDataFile + +/** + * A container for populating and holding Markdown test data. + * + * This container exists so that common creation, population and verification logic + * can be reused, instead of having to implement [MdFileCreator] multiple times. + * + * @see TestData + */ +class MarkdownTestData : TestData, MdFileCreator { + + private val files = mutableListOf() + + override fun mdFile( + pathFromProjectRoot: String, + fillFile: MarkdownTestDataFile.() -> Unit + ) { + check(pathFromProjectRoot.endsWith(".md")) { "Markdown files are expected to have .md extension" } + + val testDataFile = MarkdownTestDataFile(pathFromProjectRoot) + fillFile(testDataFile) + files.add(testDataFile) + } + + override fun getFiles(): List { + return files + } + + override fun toString(): String { + return "MarkdownTestData(files=$files)" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestDataFile.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestDataFile.kt new file mode 100644 index 00000000..9bb8169a --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MarkdownTestDataFile.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.markdown + +import org.jetbrains.dokka.analysis.test.api.TestDataFile + +class MarkdownTestDataFile( + pathFromProjectRoot: String +) : TestDataFile(pathFromProjectRoot = pathFromProjectRoot) { + + private var fileContents = "" + + operator fun String.unaryPlus() { + fileContents += (this.trimIndent() + System.lineSeparator()) + } + + override fun getContents(): String { + return fileContents + } + + override fun toString(): String { + return "MarkdownTestDataFile(pathFromProjectRoot='$pathFromProjectRoot', fileContents='$fileContents')" + } +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MdFileCreator.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MdFileCreator.kt new file mode 100644 index 00000000..8ba97b17 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/markdown/MdFileCreator.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.analysis.test.api.markdown + +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaConfiguration +import org.jetbrains.dokka.analysis.test.api.configuration.TestDokkaSourceSet + +/** + * Declares a capability that an `.md` file can be created in the scope of the implementation. + */ +interface MdFileCreator { + + /** + * Creates an `.md` (Markdown) file. + * + * If you want to use this file for module and package documentation, it must be included + * in [TestDokkaConfiguration.includes] or [TestDokkaSourceSet.includes]. + * + * @param pathFromProjectRoot path relative to the root of the test project. Must begin + * with `/` to not confuse it with relative paths. Example: `/docs/core-package.md` + */ + fun mdFile( + pathFromProjectRoot: String, + fillFile: MarkdownTestDataFile.() -> Unit + ) +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/CollectionUtils.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/CollectionUtils.kt new file mode 100644 index 00000000..6cd41594 --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/CollectionUtils.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.analysis.test.api.util + +internal inline fun flatListOf(vararg lists: List): List { + val overallSize = lists.sumBy { it.size } + val aggregatingList = ArrayList(overallSize) + lists.forEach { + aggregatingList.addAll(it) + } + return aggregatingList +} + +internal inline fun Iterable.mapToSet(transform: (T) -> R): Set { + return this.map(transform).toSet() +} diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/DslApiUtils.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/DslApiUtils.kt new file mode 100644 index 00000000..e855bb4b --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/DslApiUtils.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test.api.util + +@DslMarker +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) +annotation class AnalysisTestDslMarker diff --git a/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/FileUtils.kt b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/FileUtils.kt new file mode 100644 index 00000000..779add8d --- /dev/null +++ b/subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/FileUtils.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.analysis.test.api.util + +import org.jetbrains.dokka.utilities.DokkaLogger +import java.io.File +import java.io.IOException + +/** + * Converts a file path (like `org/jetbrains/dokka/test/File.kt`) to a fully qualified + * package name (like `org.jetbrains.dokka.test`). + * + * @param srcRelativeFilePath path relative to the source code directory. If the full path of the file + * is `src/main/kotlin/org/jetbrains/dokka/test/File.kt`, then only + * `org/jetbrains/dokka/test/File.kt` is expected to be passed here. + * @return fully qualified package name or an empty string in case of the root package. + */ +internal fun filePathToPackageName(srcRelativeFilePath: String): String { + return srcRelativeFilePath + .substringBeforeLast("/", missingDelimiterValue = "") + .replace("/", ".") +} + +/** + * @throws IOException if the requested temporary directory could not be created or deleted once used. + */ +internal fun withTempDirectory(logger: DokkaLogger? = null, block: (tempDirectory: File) -> T): T { + @Suppress("DEPRECATION") // TODO migrate to kotlin.io.path.createTempDirectory with languageVersion >= 1.5 + val tempDir = createTempDir() + try { + logger?.debug("Created temporary directory $tempDir") + return block(tempDir) + } finally { + if (!tempDir.deleteRecursively()) { + throw IOException("Unable to delete temporary directory $tempDir") + } + logger?.debug("Deleted temporary directory $tempDir") + } +} -- cgit