diff --git a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/SourceRootChecker.kt b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/SourceRootChecker.kt new file mode 100644 index 0000000000..95c19e666c --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/SourceRootChecker.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.test + +import org.jetbrains.dokka.CoreExtensions +import org.jetbrains.dokka.DokkaConfigurationImpl +import org.jetbrains.dokka.DokkaSourceSetImpl +import org.jetbrains.dokka.Platform + +import org.jetbrains.dokka.analysis.test.api.jvm.kotlin.KotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.util.CollectingDokkaConsoleLogger +import org.jetbrains.dokka.plugability.DokkaContext +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class SourceRootChecker { + + val dokkaConfiguration = DokkaConfigurationImpl( + sourceSets = listOf( + DokkaSourceSetImpl( + analysisPlatform = Platform.jvm, + displayName = "KotlinJvmSourceSet", + sourceSetID = KotlinJvmTestProject.DEFAULT_SOURCE_SET_ID, + sourceRoots = setOf(File("/src/d1/d2")), + ), + DokkaSourceSetImpl( + analysisPlatform = Platform.jvm, + displayName = "KotlinJvmSourceSet2", + sourceSetID = KotlinJvmTestProject.DEFAULT_SOURCE_SET_ID, + sourceRoots = setOf(File("/src/d1"), File("/src/d1/d2/d3/../d4")), + ), + DokkaSourceSetImpl( + analysisPlatform = Platform.jvm, + displayName = "KotlinJvmSourceSet3", + sourceSetID = KotlinJvmTestProject.DEFAULT_SOURCE_SET_ID, + sourceRoots = setOf(File("/src/c")), + ), + ) + ) + + @Test + @OnlySymbols("K1 supports all source roots including intersected ones") + fun `pre-generation check should failed if source roots are intersected`() { + val collectingLogger = CollectingDokkaConsoleLogger() + + val context = DokkaContext.create( + configuration = dokkaConfiguration, + logger = collectingLogger, + pluginOverrides = listOf() + ) + + val (preGenerationCheckResult, checkMessages) = context[CoreExtensions.preGenerationCheck].fold( + Pair(true, emptyList()) + ) { acc, checker -> checker() + acc } + + val expectedMessage = "Source sets 'KotlinJvmSourceSet' and 'KotlinJvmSourceSet2' have the common source roots: /src/d1/d2, /src/d1, /src/d1/d2/d4. Every Kotlin source file should belong to only one source set (module).".replace('/', File.separatorChar) + assertFalse(preGenerationCheckResult) + assertEquals(listOf(expectedMessage), checkMessages) + } + + @Test + @OnlyDescriptors("K1 supports all source roots including intersected ones") + fun `pre-generation check should log warning if source roots are intersected`() { + val collectingLogger = CollectingDokkaConsoleLogger() + + val context = DokkaContext.create( + configuration = dokkaConfiguration, + logger = collectingLogger, + pluginOverrides = listOf() + ) + + val (preGenerationCheckResult, checkMessages) = context[CoreExtensions.preGenerationCheck].fold( + Pair(true, emptyList()) + ) { acc, checker -> checker() + acc } + assertEquals(checkMessages, emptyList()) + assertTrue(collectingLogger.collectedLogMessages.contains("Source sets 'KotlinJvmSourceSet' and 'KotlinJvmSourceSet2' have the common source roots: /src/d1/d2, /src/d1, /src/d1/d2/d4. Every Kotlin source file should belong to only one source set (module).".replace('/', File.separatorChar) + "\nIn Dokka K2 it will be an error. Also, please consider reporting your user case: https://github.com/Kotlin/dokka/issues")) + assertTrue(preGenerationCheckResult) + } +} \ No newline at end of file diff --git a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/TagsAnnotations.kt b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/TagsAnnotations.kt index b66378a3b9..af195ade83 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/TagsAnnotations.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/TagsAnnotations.kt @@ -25,6 +25,24 @@ import org.junit.jupiter.api.Tag @Tag("onlyDescriptors") annotation class OnlyDescriptors(val reason: String = "") + +/** + * Run a test only for symbols (aka K2), not descriptors (K1). + * + * After remove K1 in dokka, this annotation should be also removed without consequences + */ +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +@Tag("onlySymbols") +annotation class OnlySymbols(val reason: String = "") + /** * Run a test only for descriptors, not symbols. * diff --git a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin.kt b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin.kt index 0b7bb3b005..085adc33fb 100644 --- a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin.kt +++ b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/CompilerDescriptorAnalysisPlugin.kt @@ -146,6 +146,10 @@ public class CompilerDescriptorAnalysisPlugin : DokkaPlugin() { CoreExtensions.postActions with PostAction { querySingle { kotlinAnalysis }.close() } } + internal val sourceRootIndependentChecker by extending { + CoreExtensions.preGenerationCheck providing ::K1SourceRootIndependentChecker + } + @OptIn(DokkaPluginApiPreview::class) override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement } diff --git a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/K1SourceRootIndependentChecker.kt b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/K1SourceRootIndependentChecker.kt new file mode 100644 index 0000000000..0b385f928b --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/K1SourceRootIndependentChecker.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.kotlin.descriptors.compiler + +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.validity.PreGenerationChecker +import org.jetbrains.dokka.validity.PreGenerationCheckerOutput +import java.io.File + +/** + * It just logs a warning if different source sets should have disjoint source roots and samples. + * + * K2's analysis API does not support having two different [org.jetbrains.kotlin.analysis.project.structure.KtSourceModule] with the same file system directory or intersecting files, it throws the error "Modules are inconsistent". + * Meanwhile, K1 support it. + */ +internal class K1SourceRootIndependentChecker( + private val context: DokkaContext +) : PreGenerationChecker { + override fun invoke(): PreGenerationCheckerOutput { + val logger = context.logger + val sourceSets = context.configuration.sourceSets + + for (i in sourceSets.indices) { + for (j in i + 1 until sourceSets.size) { + // check source roots + val sourceRoot1 = sourceSets[i].sourceRoots.normalize() + val sourceRoot2 = sourceSets[j].sourceRoots.normalize() + val intersection = intersect(sourceRoot1, sourceRoot2) + if (intersection.isNotEmpty()) { + logger.warn("Source sets '${sourceSets[i].displayName}' and '${sourceSets[j].displayName}' have the common source roots: ${intersection.joinToString()}. Every Kotlin source file should belong to only one source set (module).\n" + + "In Dokka K2 it will be an error. Also, please consider reporting your user case: https://github.com/Kotlin/dokka/issues") + } + } + } + return PreGenerationCheckerOutput(true, emptyList()) + } + + private fun Set.normalize() = mapTo(mutableSetOf()) { it.normalize() } + private fun intersect(normalizedPaths: Set, normalizedPaths2: Set): Set { + val result = mutableSetOf() + for (p1 in normalizedPaths) { + for (p2 in normalizedPaths2) { + if (p1.startsWith(p2) || p2.startsWith(p1)) { + result.add(p1) + result.add(p2) + } + } + } + return result + } +} \ No newline at end of file diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SourceRootIndependentChecker.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SourceRootIndependentChecker.kt new file mode 100644 index 0000000000..82ba0c66d7 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SourceRootIndependentChecker.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka.analysis.kotlin.symbols.plugin + +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.validity.PreGenerationChecker +import org.jetbrains.dokka.validity.PreGenerationCheckerOutput +import java.io.File + +/** + * It checks that different source sets should have disjoint source roots and samples. + * + * + * K2's analysis API does not support having two different [org.jetbrains.kotlin.analysis.project.structure.KtSourceModule] with the same file system directory or intersecting files, it throws the error "Modules are inconsistent". + * + * @see org.jetbrains.kotlin.analysis.project.structure.KtModule.contentScope + * @see org.jetbrains.kotlin.analysis.project.structure.ProjectStructureProvider.getModule + */ +internal class SourceRootIndependentChecker( + private val context: DokkaContext +) : PreGenerationChecker { + override fun invoke(): PreGenerationCheckerOutput { + val messages = mutableListOf() + val sourceSets = context.configuration.sourceSets + + for (i in sourceSets.indices) { + for (j in i + 1 until sourceSets.size) { + // check source roots + val sourceRoot1 = sourceSets[i].sourceRoots.normalize() + val sourceRoot2 = sourceSets[j].sourceRoots.normalize() + val intersection = intersect(sourceRoot1, sourceRoot2) + if (intersection.isNotEmpty()) { + messages += "Source sets '${sourceSets[i].displayName}' and '${sourceSets[j].displayName}' have the common source roots: ${intersection.joinToString()}. Every Kotlin source file should belong to only one source set (module)." + } + } + } + return PreGenerationCheckerOutput(messages.isEmpty(), messages) + } + + + private fun Set.normalize() = mapTo(mutableSetOf()) { it.normalize() } + private fun intersect(normalizedPaths: Set, normalizedPaths2: Set): Set { + val result = mutableSetOf() + for (p1 in normalizedPaths) { + for (p2 in normalizedPaths2) { + if (p1.startsWith(p2) || p2.startsWith(p1)) { + result.add(p1) + result.add(p2) + } + } + } + return result + } + +} \ No newline at end of file diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt index 7804aef601..5c5cf233f2 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAnalysisPlugin.kt @@ -122,6 +122,10 @@ public class SymbolsAnalysisPlugin : DokkaPlugin() { plugin().sampleAnalysisEnvironmentCreator providing ::SymbolSampleAnalysisEnvironmentCreator } + internal val sourceRootIndependentChecker by extending { + CoreExtensions.preGenerationCheck providing ::SourceRootIndependentChecker + } + @OptIn(DokkaPluginApiPreview::class) override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement }