Skip to content

Commit

Permalink
[K2] Add a check that source roots do not intersect (#3465)
Browse files Browse the repository at this point in the history
* [K2] Add a check that source roots do not intersect

* [K1] Log a warning if source roots are intersected
  • Loading branch information
vmishenev committed Feb 26, 2024
1 parent 2a94f8b commit 722d082
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<String>())
) { 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<String>())
) { 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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<File>.normalize() = mapTo(mutableSetOf()) { it.normalize() }
private fun intersect(normalizedPaths: Set<File>, normalizedPaths2: Set<File>): Set<File> {
val result = mutableSetOf<File>()
for (p1 in normalizedPaths) {
for (p2 in normalizedPaths2) {
if (p1.startsWith(p2) || p2.startsWith(p1)) {
result.add(p1)
result.add(p2)
}
}
}
return result
}
}
Original file line number Diff line number Diff line change
@@ -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<String>()
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<File>.normalize() = mapTo(mutableSetOf()) { it.normalize() }
private fun intersect(normalizedPaths: Set<File>, normalizedPaths2: Set<File>): Set<File> {
val result = mutableSetOf<File>()
for (p1 in normalizedPaths) {
for (p2 in normalizedPaths2) {
if (p1.startsWith(p2) || p2.startsWith(p1)) {
result.add(p1)
result.add(p2)
}
}
}
return result
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ public class SymbolsAnalysisPlugin : DokkaPlugin() {
plugin<KotlinAnalysisPlugin>().sampleAnalysisEnvironmentCreator providing ::SymbolSampleAnalysisEnvironmentCreator
}

internal val sourceRootIndependentChecker by extending {
CoreExtensions.preGenerationCheck providing ::SourceRootIndependentChecker
}

@OptIn(DokkaPluginApiPreview::class)
override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement
}

0 comments on commit 722d082

Please sign in to comment.