diff options
author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-11-21 11:28:45 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-21 11:28:45 +0100 |
commit | 6fbc2221ff309995c605161b51d4d64cbabddd51 (patch) | |
tree | a6476838442e47b3bbc03d753c74c84f58f67b1e | |
parent | 9ce37affaa2c1199807c08e13485740ea993994e (diff) | |
download | dokka-6fbc2221ff309995c605161b51d4d64cbabddd51.tar.gz dokka-6fbc2221ff309995c605161b51d4d64cbabddd51.tar.bz2 dokka-6fbc2221ff309995c605161b51d4d64cbabddd51.zip |
Stabilize Sample analysis API (#3195)
35 files changed, 1216 insertions, 407 deletions
diff --git a/dokka-subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api b/dokka-subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api index c65dfe5a..3b546932 100644 --- a/dokka-subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api +++ b/dokka-subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api @@ -1,5 +1,6 @@ public final class org/jetbrains/dokka/analysis/kotlin/KotlinAnalysisPlugin : org/jetbrains/dokka/plugability/DokkaPlugin { public fun <init> ()V + public final fun getSampleAnalysisEnvironmentCreator ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; } public final class org/jetbrains/dokka/analysis/kotlin/internal/DocumentableLanguage : java/lang/Enum { @@ -51,7 +52,6 @@ public final class org/jetbrains/dokka/analysis/kotlin/internal/InternalKotlinAn public final fun getInheritanceBuilder ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; public final fun getKotlinToJavaService ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; public final fun getModuleAndPackageDocumentationReader ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; - public final fun getSampleProviderFactory ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; public final fun getSyntheticDocumentableDetector ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; } @@ -65,21 +65,24 @@ public abstract interface class org/jetbrains/dokka/analysis/kotlin/internal/Mod public abstract fun read (Lorg/jetbrains/dokka/model/DPackage;)Ljava/util/Map; } -public abstract interface class org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider : java/lang/AutoCloseable { - public abstract fun getSample (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/lang/String;)Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider$SampleSnippet; +public abstract interface class org/jetbrains/dokka/analysis/kotlin/internal/SyntheticDocumentableDetector { + public abstract fun isSynthetic (Lorg/jetbrains/dokka/model/Documentable;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Z } -public final class org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider$SampleSnippet { - public fun <init> (Ljava/lang/String;Ljava/lang/String;)V - public final fun getBody ()Ljava/lang/String; - public final fun getImports ()Ljava/lang/String; +public abstract interface class org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironment { + public abstract fun resolveSample (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/lang/String;)Lorg/jetbrains/dokka/analysis/kotlin/sample/SampleSnippet; } -public abstract interface class org/jetbrains/dokka/analysis/kotlin/internal/SampleProviderFactory { - public abstract fun build ()Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider; +public abstract interface class org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironmentCreator { + public abstract fun use (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; } -public abstract interface class org/jetbrains/dokka/analysis/kotlin/internal/SyntheticDocumentableDetector { - public abstract fun isSynthetic (Lorg/jetbrains/dokka/model/Documentable;Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Z +public final class org/jetbrains/dokka/analysis/kotlin/sample/SampleSnippet { + public fun <init> (Ljava/util/List;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getBody ()Ljava/lang/String; + public final fun getImports ()Ljava/util/List; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } diff --git a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/KotlinAnalysisPlugin.kt b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/KotlinAnalysisPlugin.kt index 7d434bd5..1df1dfe6 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/KotlinAnalysisPlugin.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/KotlinAnalysisPlugin.kt @@ -4,17 +4,21 @@ package org.jetbrains.dokka.analysis.kotlin +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironmentCreator +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironment import org.jetbrains.dokka.plugability.DokkaPlugin import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.ExtensionPoint import org.jetbrains.dokka.plugability.PluginApiPreviewAcknowledgement public class KotlinAnalysisPlugin : DokkaPlugin() { - /* - * This is where stable public API will go. + /** + * An extension for analyzing Kotlin sample functions used in the `@sample` KDoc tag. * - * No stable public API for now. + * @see SampleAnalysisEnvironment for more details */ + public val sampleAnalysisEnvironmentCreator: ExtensionPoint<SampleAnalysisEnvironmentCreator> by extensionPoint() @OptIn(DokkaPluginApiPreview::class) override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement diff --git a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/internal/InternalKotlinAnalysisPlugin.kt b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/internal/InternalKotlinAnalysisPlugin.kt index 0ef1399a..d032d490 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/internal/InternalKotlinAnalysisPlugin.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/internal/InternalKotlinAnalysisPlugin.kt @@ -31,8 +31,6 @@ public class InternalKotlinAnalysisPlugin : DokkaPlugin() { public val documentableSourceLanguageParser: ExtensionPoint<DocumentableSourceLanguageParser> by extensionPoint() - public val sampleProviderFactory: ExtensionPoint<SampleProviderFactory> by extensionPoint() - @OptIn(DokkaPluginApiPreview::class) override fun pluginApiPreviewAcknowledgement(): PluginApiPreviewAcknowledgement = PluginApiPreviewAcknowledgement } diff --git a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider.kt b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider.kt deleted file mode 100644 index 472d17f0..00000000 --- a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider.kt +++ /dev/null @@ -1,36 +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.analysis.kotlin.internal - -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.InternalDokkaApi - -@InternalDokkaApi -public interface SampleProviderFactory { - /** - * [SampleProvider] is a short-lived closeable instance. - * It assumes that [SampleProvider] scope of use is not big. - * Otherwise, it can lead to high memory consumption / leaks during Dokka running. - */ - public fun build(): SampleProvider -} - -/** - * It is closeable. - * Otherwise, there is a chance of high memory consumption / leak. - * In general case, it creates a separate project to analysis samples directories. - */ -@InternalDokkaApi -public interface SampleProvider: AutoCloseable { - public class SampleSnippet( - public val imports: String, - public val body: String - ) - - /** - * @return [SampleSnippet] or null if it has not found by [fqLink] - */ - public fun getSample(sourceSet: DokkaConfiguration.DokkaSourceSet, fqLink: String): SampleSnippet? -} diff --git a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironment.kt b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironment.kt new file mode 100644 index 00000000..3620808a --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironment.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.kotlin.sample + +import org.jetbrains.dokka.DokkaConfiguration + +/** + * Fully-configured and ready-to-use sample analysis environment. + * + * It's best to limit the scope of use and lifetime of this environment as it takes up + * additional resources which could be freed once the samples have been analyzed. + * Therefore, it's best to use it through the [SampleAnalysisEnvironmentCreator.use] lambda. + * + * For example, if you need to process all samples in an arbitrary project, it's best to do it + * in one iteration and at the same time, so that the environment is created once and lives for + * as little is possible, as opposed to creating it again and again for every individual sample. + */ +public interface SampleAnalysisEnvironment { + + /** + * Resolves a Kotlin sample function by its fully qualified name, and returns its import statements and body. + * + * @param sourceSet must be either the source set in which this sample function resides, or the source set + * for which [DokkaConfiguration#samples] or [DokkaConfiguration#sourceRoots] + * have been configured with the sample's sources. + * @param fullyQualifiedLink fully qualified path to the sample function, including all middle packages + * and the name of the function. Only links to Kotlin functions are valid, + * which can reside within a class. The package must be the same as the package + * declared in the sample file. The function must be resolvable by Dokka, + * meaning it must reside either in the main sources of the project or its + * sources must be included in [DokkaConfiguration#samples] or + * [DokkaConfiguration#sourceRoots]. Example: `com.example.pckg.topLevelKotlinFunction` + * + * @return a sample code snippet which includes import statements and the function body, + * or null if the link could not be resolved (examine the logs to find out the reason). + */ + public fun resolveSample( + sourceSet: DokkaConfiguration.DokkaSourceSet, + fullyQualifiedLink: String + ): SampleSnippet? +} diff --git a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironmentCreator.kt b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironmentCreator.kt new file mode 100644 index 00000000..d64734ef --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironmentCreator.kt @@ -0,0 +1,38 @@ +/* + * 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.kotlin.sample + +import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin + +/** + * Entry point to analyzing Kotlin samples. + * + * Can be acquired via [KotlinAnalysisPlugin.sampleAnalysisEnvironmentCreator]. + */ +public interface SampleAnalysisEnvironmentCreator { + + /** + * Creates and configures the sample analysis environment for a limited-time use. + * + * Configuring sample analysis environment is a rather expensive operation that takes up additional + * resources since Dokka needs to configure and analyze source roots additional to the main ones. + * It's best to limit the scope of use and the lifetime of the created environment + * so that the resources could be freed as soon as possible. + * + * No specific cleanup is required by the caller - everything is taken care of automatically + * as soon as you exit the [block] block. + * + * Usage example: + * ```kotlin + * // create a short-lived environment and resolve all the needed samples + * val sample = sampleAnalysisEnvironmentCreator.use { + * resolveSample(sampleSourceSet, "org.jetbrains.dokka.sample.functionName") + * } + * // process the samples + * // ... + * ``` + */ + public fun <T> use(block: SampleAnalysisEnvironment.() -> T): T +} diff --git a/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleSnippet.kt b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleSnippet.kt new file mode 100644 index 00000000..41b3fa5c --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/sample/SampleSnippet.kt @@ -0,0 +1,45 @@ +/* + * 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.kotlin.sample + +/** + * Represents a sample code snippet of a Kotlin function. The snippet includes both file + * import directives (all, even unused) and the sample function body. + * + * @property imports list of import statement values, without the `import` prefix. + * Contains no blank lines. Example of a single value: `com.example.pckg.MyClass.function`. + * @property body body of the sample function, without the function name or curly braces, only the inner body. + * Common minimal indent of all lines is trimmed. Leading and trailing line breaks are removed. + * Trailing whitespaces are removed. Example: given the sample function `fun foo() { println("foo") }`, + * the sample body will be `println("foo")`. + * + * @see SampleAnalysisEnvironment for how to acquire it + */ +public class SampleSnippet( + public val imports: List<String>, + public val body: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SampleSnippet + + if (imports != other.imports) return false + if (body != other.body) return false + + return true + } + + override fun hashCode(): Int { + var result = imports.hashCode() + result = 31 * result + body.hashCode() + return result + } + + override fun toString(): String { + return "SampleSnippet(imports=$imports, body='$body')" + } +} diff --git a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt index 618e28a8..3b8a2afd 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/test/kotlin/org/jetbrains/dokka/analysis/test/sample/SampleAnalysisTest.kt @@ -4,52 +4,515 @@ package org.jetbrains.dokka.analysis.test.sample +import org.jetbrains.dokka.analysis.kotlin.sample.SampleSnippet import org.jetbrains.dokka.analysis.test.api.kotlinJvmTestProject +import org.jetbrains.dokka.analysis.test.api.mixedJvmTestProject import org.jetbrains.dokka.analysis.test.api.useServices -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import org.jetbrains.dokka.analysis.test.api.util.CollectingDokkaConsoleLogger +import org.jetbrains.dokka.analysis.test.api.util.singleSourceSet +import org.junit.jupiter.api.Tag +import kotlin.test.* class SampleAnalysisTest { @Test - fun `should return sources of a kotlin sample`() { + fun `should resolve a valid sample if set via the samples option`() { val testProject = kotlinJvmTestProject { dokkaConfiguration { kotlinSourceSet { - additionalSourceRoots = setOf("/samples") + samples = setOf("/samples/collections.kt") } } - sampleFile("/samples/stringListOf-sample.kt", fqPackageName = "org.jetbrains.dokka.sample.generator") { + sampleFile("/samples/collections.kt", fqPackageName = "org.jetbrains.dokka.sample.collections") { +""" 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() + fun specificPositionOperations() { + val numbers = mutableListOf(1, 2, 3, 4) + numbers.add(5) + numbers.removeAt(1) + numbers[0] = 0 + numbers.shuffle() + if (numbers.size > 0) { + println(numbers) + } } """ } } testProject.useServices { context -> - val sampleSourceSet = context.configuration.sourceSets.single() + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample( + sourceSet = context.singleSourceSet(), + fullyQualifiedLink = "org.jetbrains.dokka.sample.collections.specificPositionOperations" + ) + } + assertNotNull(sample) + + val expectedImports = listOf( + "org.jetbrains.dokka.DokkaConfiguration", + "org.jetbrains.dokka.DokkaGenerator", + "org.jetbrains.dokka.utilities.DokkaLogger" + ) - val sampleProvider = sampleProviderFactory.build() - val sample = sampleProvider.getSample(sampleSourceSet, "org.jetbrains.dokka.sample.generator.runGenerator") + val expectedBody = """ + val numbers = mutableListOf(1, 2, 3, 4) + numbers.add(5) + numbers.removeAt(1) + numbers[0] = 0 + numbers.shuffle() + if (numbers.size > 0) { + println(numbers) + } + """.trimIndent() + + assertEquals(expectedImports, sample.imports) + assertEquals(expectedBody, sample.body) + } + } + + @Test + @Tag("onlyDescriptors") // TODO #3359 + fun `should resolve a valid sample if set via the additionalSourceRoots option`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + additionalSourceRoots = setOf("/samples") + } + } + sampleFile("/samples/collections.kt", fqPackageName = "org.jetbrains.dokka.sample.collections") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + import org.jetbrains.dokka.DokkaGenerator + import org.jetbrains.dokka.utilities.DokkaLogger + + fun specificPositionOperations() { + val numbers = mutableListOf(1, 2, 3, 4) + numbers.add(5) + numbers.removeAt(1) + numbers[0] = 0 + numbers.shuffle() + if (numbers.size > 0) { + println(numbers) + } + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample( + sourceSet = context.singleSourceSet(), + fullyQualifiedLink = "org.jetbrains.dokka.sample.collections.specificPositionOperations" + ) + } assertNotNull(sample) val expectedImports = listOf( - "import org.jetbrains.dokka.DokkaConfiguration", - "import org.jetbrains.dokka.DokkaGenerator", - "import org.jetbrains.dokka.utilities.DokkaLogger" - ).joinToString(separator = "\n") + "org.jetbrains.dokka.DokkaConfiguration", + "org.jetbrains.dokka.DokkaGenerator", + "org.jetbrains.dokka.utilities.DokkaLogger" + ) + + val expectedBody = """ + val numbers = mutableListOf(1, 2, 3, 4) + numbers.add(5) + numbers.removeAt(1) + numbers[0] = 0 + numbers.shuffle() + if (numbers.size > 0) { + println(numbers) + } + """.trimIndent() + + assertEquals(expectedImports, sample.imports) + assertEquals(expectedBody, sample.body) + } + } + + @Test + @Tag("onlyDescriptors") // TODO #3359 + fun `should resolve a valid sample function that exists in the main source set`() { + val testProject = kotlinJvmTestProject { + ktFile("org/jetbrains/dokka/test/MyKotlinFile.kt") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + + fun myAverageTopLevelFunction() { + println("hello from the average top level function") + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "org.jetbrains.dokka.test.myAverageTopLevelFunction") + } + assertNotNull(sample) + + val expectedImports = listOf("org.jetbrains.dokka.DokkaConfiguration") + val expectedBody = "println(\"hello from the average top level function\")" + + assertEquals(expectedImports, sample.imports) + assertEquals(expectedBody, sample.body) + } + } + + @Test + fun `should resolve a valid sample in the root package`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + samples = setOf("/samples/TopLevelSample.kt") + } + } + + sampleFile("/samples/TopLevelSample.kt", fqPackageName = "") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + + fun foo() { + println("hello from the root") + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "foo") + } + assertNotNull(sample) + + val expectedImports = listOf("org.jetbrains.dokka.DokkaConfiguration") + val expectedBody = "println(\"hello from the root\")" + + assertEquals(expectedImports, sample.imports) + assertEquals(expectedBody, sample.body) + } + } + + @Test + fun `should resolve a valid sample function from a class in the root package`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + samples = setOf("/samples/RootClassSample.kt") + } + } + + sampleFile("/samples/RootClassSample.kt", fqPackageName = "") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + + class RootClass { + fun foo() { + println("hello from within a root class") + } + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "RootClass.foo") + } + assertNotNull(sample) + + val expectedImports = listOf("org.jetbrains.dokka.DokkaConfiguration") + val expectedBody = "println(\"hello from within a root class\")" + + assertEquals(expectedImports, sample.imports) + assertEquals(expectedBody, sample.body) + } + } + + @Test + fun `should resolve a valid sample function from a class`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + samples = setOf("/samples/SampleWithinClass.kt") + } + } + + sampleFile("/samples/SampleWithinClass.kt", fqPackageName = "samples") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + + package samples + + class SampleWithinClass { + fun foo() { + println("hello from within a class") + } + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "samples.SampleWithinClass.foo") + } + assertNotNull(sample) + + val expectedImports = listOf("org.jetbrains.dokka.DokkaConfiguration") + val expectedBody = "println(\"hello from within a class\")" + + assertEquals(expectedImports, sample.imports) + assertEquals(expectedBody, sample.body) + } + } + + @Test + fun `should return null for non-existing sample`() { + val testProject = kotlinJvmTestProject { + // nothing + } + + testProject.useServices { context -> + val nonExistingSample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "com.example.non.existing.sampleFunction") + } + + assertNull(nonExistingSample) + } + } + + @Test + fun `should return null if sample is resolved just by class name`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + samples = setOf("/samples/FooSampleFile.kt") + } + } + sampleFile("/samples/FooSampleFile.kt", fqPackageName = "org.jetbrains.dokka.sample") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + + fun topLevelFunction() {} + + class FooSampleClass { + fun foo() { + println("foo") + } + } + """ + } + } + + val collectingLogger = CollectingDokkaConsoleLogger() + testProject.useServices(collectingLogger) { context -> + val sampleByClassName = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "org.jetbrains.dokka.sample.FooSampleClass") + } + assertNull(sampleByClassName) + } + + val containsNonKotlinSampleLinkLog = collectingLogger.collectedLogMessages.contains( + "Unable to process a @sample link: \"org.jetbrains.dokka.sample.FooSampleClass\". " + + "Only function links allowed." + ) + assertTrue(containsNonKotlinSampleLinkLog) + } + + @Test + @Tag("onlyDescriptors") // TODO #3359 + fun `should return null if trying to resolve a non-kotlin sample link`() { + val testProject = mixedJvmTestProject { + kotlinSourceDirectory { + javaFile("org/jetbrains/test/sample/JavaClass.java") { + +""" + public class JavaClass { + public void foo() { + System.out.println("foo"); + } + } + """ + } + ktFile("org/jetbrains/test/sample/KotlinFile.kt") { + +""" + fun foo() {} + """ + } + } + } + + val collectingLogger = CollectingDokkaConsoleLogger() + testProject.useServices(collectingLogger) { context -> + sampleAnalysisEnvironmentCreator.use { + val kotlinSourceSet = context.singleSourceSet() + + val byClassName = resolveSample(kotlinSourceSet, "org.jetbrains.test.sample.JavaClass") + assertNull(byClassName) + + val byClassFunctionName = resolveSample(kotlinSourceSet, "org.jetbrains.test.sample.JavaClass.foo") + assertNull(byClassFunctionName) + } + } + + val containsNonKotlinSampleLinkLog = collectingLogger.collectedLogMessages.contains( + "Unable to resolve non-Kotlin @sample links: \"org.jetbrains.test.sample.JavaClass\"" + ) + assertTrue(containsNonKotlinSampleLinkLog) + } + + @Test + @Tag("onlyDescriptors") // TODO #3359 + fun `should filter out empty import statement lines`() { + val testProject = kotlinJvmTestProject { + ktFile("org/jetbrains/dokka/test/MyKotlinFile.kt") { + +""" + import org.jetbrains.dokka.DokkaConfiguration + + import org.jetbrains.dokka.DokkaGenerator + + import org.jetbrains.dokka.utilities.DokkaLogger + + fun sample() { + println("hello from sample") + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "org.jetbrains.dokka.test.sample") + } + assertNotNull(sample) - val expectedBody = "DokkaGenerator(configuration, logger).generate()" + val expectedImports = listOf( + "org.jetbrains.dokka.DokkaConfiguration", + "org.jetbrains.dokka.DokkaGenerator", + "org.jetbrains.dokka.utilities.DokkaLogger", + ) + val expectedBody = "println(\"hello from sample\")" assertEquals(expectedImports, sample.imports) assertEquals(expectedBody, sample.body) } } + + @Test + @Tag("onlyDescriptors") // TODO #3359 + fun `should return an empty list of imports if sample file has none`() { + val testProject = kotlinJvmTestProject { + ktFile("org/jetbrains/dokka/test/MyKotlinFile.kt") { + +""" + fun sample() { + println("hello from sample") + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "org.jetbrains.dokka.test.sample") + } + assertNotNull(sample) + + assertTrue(sample.imports.isEmpty()) + + val expectedBody = "println(\"hello from sample\")" + assertEquals(expectedBody, sample.body) + + } + } + + @Test + @Tag("onlyDescriptors") // TODO #3359 + fun `should filter out leading and trailing line breaks`() { + val testProject = kotlinJvmTestProject { + ktFile("org/jetbrains/dokka/test/MyKotlinFile.kt") { + +""" + fun sample() { + + + println("hello from sample") + + + + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "org.jetbrains.dokka.test.sample") + } + assertNotNull(sample) + + val expectedBody = "println(\"hello from sample\")" + assertEquals(expectedBody, sample.body) + } + } + + @Test + @Tag("onlyDescriptors") // TODO #3359 + fun `should filter out trailing whitespace`() { + val testProject = kotlinJvmTestProject { + ktFile("org/jetbrains/dokka/test/MyKotlinFile.kt") { + +""" + fun sample() { + println("hello from sample") + } + """ + } + } + + testProject.useServices { context -> + val sample = sampleAnalysisEnvironmentCreator.use { + resolveSample(context.singleSourceSet(), "org.jetbrains.dokka.test.sample") + } + assertNotNull(sample) + + val expectedBody = "println(\"hello from sample\")" + assertEquals(expectedBody, sample.body) + } + } + + @Test + fun `should see two identical snippets as equal`() { + val firstSnippet = createHardcodedSnippet() + val secondSnippet = createHardcodedSnippet() + + assertEquals(firstSnippet, secondSnippet) + } + + @Test + fun `should return same hashcode for two equal sample snippets`() { + val firstSnippet = createHardcodedSnippet() + val secondSnippet = createHardcodedSnippet() + + assertEquals(firstSnippet.hashCode(), secondSnippet.hashCode()) + } + + private fun createHardcodedSnippet(): SampleSnippet { + return SampleSnippet( + imports = listOf( + "org.jetbrains.dokka.DokkaConfiguration", + "org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet", + ), + body = """ + class Foo { + fun bar(): String = TODO() + } + """.trimIndent() + ) + } + + @Test + @Ignore // TODO [beresnev] should be implemented when there's api for KMP projects + fun `should return null for existing sample when resolving with the wrong source set`() {} } diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt index 9c0fa936..e000bb69 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/TestProject.kt @@ -8,10 +8,13 @@ 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.analysis.defaultAnalysisLogger 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.CollectingDokkaConsoleLogger import org.jetbrains.dokka.analysis.test.api.util.withTempDirectory import org.jetbrains.dokka.model.DModule +import org.jetbrains.dokka.utilities.DokkaLogger /** * Represents a virtual test project (as if it's user-defined) that will be used to run Dokka. @@ -66,8 +69,11 @@ interface TestProject { * * val module: DModule = testProject.parse() * ``` + * + * @param logger logger to be used for running Dokka and tests. Custom loggers like [CollectingDokkaConsoleLogger] + * can be useful in verifying the behavior. */ -fun TestProject.parse(): DModule = TestProjectAnalyzer.parse(this) +fun TestProject.parse(logger: DokkaLogger = defaultAnalysisLogger): DModule = TestProjectAnalyzer.parse(this, logger) /** * Runs Dokka on the given [TestProject] and provides not only the resulting documentable model, @@ -88,10 +94,16 @@ fun TestProject.parse(): DModule = TestProjectAnalyzer.parse(this) * val allPackageDocs: SourceSetDependent<DocumentationNode> = moduleAndPackageDocumentationReader.read(pckg) * } * ``` + * + * @param logger logger to be used for running Dokka and tests. Custom loggers like [CollectingDokkaConsoleLogger] + * can be useful in verifying the behavior. */ -fun TestProject.useServices(block: TestAnalysisServices.(context: TestAnalysisContext) -> Unit) { +fun TestProject.useServices( + logger: DokkaLogger = defaultAnalysisLogger, + block: TestAnalysisServices.(context: TestAnalysisContext) -> Unit +) { withTempDirectory { tempDirectory -> - val (services, context) = TestProjectAnalyzer.analyze(this, tempDirectory) + val (services, context) = TestProjectAnalyzer.analyze(this, tempDirectory, logger) services.block(context) } } diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt index ab70bbd4..f729838d 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestAnalysisServices.kt @@ -6,7 +6,7 @@ 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 +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironmentCreator /** * Services exposed in [KotlinAnalysisPlugin] that are ready to be used. @@ -15,6 +15,6 @@ import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory * It is analogous to calling `context.plugin<KotlinAnalysisPlugin>().querySingle { serviceName }`. */ class TestAnalysisServices( - val sampleProviderFactory: SampleProviderFactory, + val sampleAnalysisEnvironmentCreator: SampleAnalysisEnvironmentCreator, val moduleAndPackageDocumentationReader: ModuleAndPackageDocumentationReader ) diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt index 1668b53f..674c6d47 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/analysis/TestProjectAnalyzer.kt @@ -6,6 +6,7 @@ package org.jetbrains.dokka.analysis.test.api.analysis import org.jetbrains.dokka.CoreExtensions import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin import org.jetbrains.dokka.analysis.test.api.TestDataFile import org.jetbrains.dokka.analysis.test.api.TestProject @@ -21,16 +22,14 @@ 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.DokkaLogger 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. + * The default logger used for running Dokka and analyzing projects. */ -val analysisLogger = DokkaConsoleLogger(minLevel = LoggingLevel.INFO) +val defaultAnalysisLogger = DokkaConsoleLogger(minLevel = LoggingLevel.DEBUG) /** * Analyzer of the test projects, it is essentially a very simple Dokka runner. @@ -47,7 +46,7 @@ val analysisLogger = DokkaConsoleLogger(minLevel = LoggingLevel.INFO) * 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 { +internal object TestProjectAnalyzer { /** * A quick way to analyze a [TestProject], for cases when only the documentable @@ -58,11 +57,11 @@ object TestProjectAnalyzer { * * @see [TestProject.parse] for a user-friendly way to call it */ - fun parse(testProject: TestProject): DModule { + fun parse(testProject: TestProject, logger: DokkaLogger): 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) + return withTempDirectory(logger) { tempDirectory -> + val (_, context) = testProject.initialize(outputDirectory = tempDirectory, logger) + generateDocumentableModel(context, logger) } } @@ -80,14 +79,15 @@ object TestProjectAnalyzer { */ fun analyze( testProject: TestProject, - persistentDirectory: File + persistentDirectory: File, + logger: DokkaLogger ): Pair<TestAnalysisServices, TestAnalysisContext> { - val (dokkaConfiguration, dokkaContext) = testProject.initialize(outputDirectory = persistentDirectory) - val analysisServices = createTestAnalysisServices(dokkaContext) + val (dokkaConfiguration, dokkaContext) = testProject.initialize(outputDirectory = persistentDirectory, logger) + val analysisServices = createTestAnalysisServices(dokkaContext, logger) val testAnalysisContext = TestAnalysisContext( context = dokkaContext, configuration = dokkaConfiguration, - module = generateDocumentableModel(dokkaContext) + module = generateDocumentableModel(dokkaContext, logger) ) return analysisServices to testAnalysisContext } @@ -96,28 +96,31 @@ object TestProjectAnalyzer { * Prepares this [TestProject] for analysis by creating * the test files, setting up context and configuration. */ - private fun TestProject.initialize(outputDirectory: File): Pair<DokkaConfiguration, DokkaContext> { - analysisLogger.progress("Initializing and verifying project $this") + private fun TestProject.initialize( + outputDirectory: File, + logger: DokkaLogger + ): Pair<DokkaConfiguration, DokkaContext> { + logger.progress("Initializing and verifying project $this") this.verify() require(outputDirectory.isDirectory) { "outputDirectory has to exist and be a directory: $outputDirectory" } - this.initializeTestFiles(relativeToDir = outputDirectory) + this.initializeTestFiles(relativeToDir = outputDirectory, logger) - analysisLogger.progress("Creating configuration and context") + logger.progress("Creating configuration and context") val testDokkaConfiguration = this.getConfiguration() val dokkaConfiguration = testDokkaConfiguration.toDokkaConfiguration(projectDir = outputDirectory).also { it.verify() } - return dokkaConfiguration to createContext(dokkaConfiguration) + return dokkaConfiguration to createContext(dokkaConfiguration, logger) } /** * 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") + private fun TestProject.initializeTestFiles(relativeToDir: File, logger: DokkaLogger) { + logger.progress("Initializing test files relative to the \"$relativeToDir\" directory") this.getTestData().getFiles().forEach { val testDataFile = relativeToDir.resolve(it.pathFromProjectRoot.removePrefix("/")) @@ -128,7 +131,7 @@ object TestProjectAnalyzer { throw IllegalStateException("Unable to create dirs \"${testDataFile.parentFile}\"", e) } - analysisLogger.debug("Creating \"${testDataFile.absolutePath}\"") + logger.debug("Creating \"${testDataFile.absolutePath}\"") check(testDataFile.createNewFile()) { "Unable to create a test file: ${testDataFile.absolutePath}" } @@ -163,11 +166,11 @@ object TestProjectAnalyzer { } } - private fun createContext(dokkaConfiguration: DokkaConfiguration): DokkaContext { - analysisLogger.progress("Creating DokkaContext from test configuration") + private fun createContext(dokkaConfiguration: DokkaConfiguration, logger: DokkaLogger): DokkaContext { + logger.progress("Creating DokkaContext from test configuration") return DokkaContext.create( configuration = dokkaConfiguration, - logger = analysisLogger, + logger = logger, pluginOverrides = listOf() ) } @@ -176,12 +179,12 @@ object TestProjectAnalyzer { * 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") + private fun generateDocumentableModel(context: DokkaContext, logger: DokkaLogger): DModule { + logger.progress("Generating the documentable model") val sourceSetModules = context .configuration .sourceSets - .map { sourceSet -> translateSources(sourceSet, context) } + .map { sourceSet -> translateSources(sourceSet, context, logger) } .flatten() if (sourceSetModules.isEmpty()) { @@ -196,12 +199,16 @@ object TestProjectAnalyzer { * Translates input source files to the documentable model by using * all registered [SourceToDocumentableTranslator] core extensions. */ - private fun translateSources(sourceSet: DokkaConfiguration.DokkaSourceSet, context: DokkaContext): List<DModule> { + private fun translateSources( + sourceSet: DokkaConfiguration.DokkaSourceSet, + context: DokkaContext, + logger: DokkaLogger + ): List<DModule> { 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}") + logger.debug("Translating sources for ${sourceSet.sourceSetID}") return translators.map { it.invoke(sourceSet, context) } } @@ -212,12 +219,18 @@ object TestProjectAnalyzer { * 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<InternalKotlinAnalysisPlugin>() + private fun createTestAnalysisServices( + context: DokkaContext, + logger: DokkaLogger + ): TestAnalysisServices { + logger.progress("Creating analysis services") + val publicAnalysisPlugin = context.plugin<KotlinAnalysisPlugin>() + val internalAnalysisPlugin = context.plugin<InternalKotlinAnalysisPlugin>() return TestAnalysisServices( - sampleProviderFactory = internalPlugin.querySingle { sampleProviderFactory }, - moduleAndPackageDocumentationReader = internalPlugin.querySingle { moduleAndPackageDocumentationReader } + sampleAnalysisEnvironmentCreator = publicAnalysisPlugin.querySingle { sampleAnalysisEnvironmentCreator }, + moduleAndPackageDocumentationReader = internalAnalysisPlugin.querySingle { + moduleAndPackageDocumentationReader + } ) } } diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt index 156fc4e4..baabeed7 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaConfigurationBuilder.kt @@ -49,7 +49,7 @@ class JavaTestSourceSetBuilder : BaseTestDokkaSourceSetBuilder() { return TestDokkaSourceSet( analysisPlatform = Platform.jvm, displayName = "JavaJvmSourceSet", - sourceSetID = DokkaSourceSetID(scopeId = "project", sourceSetName = "java"), + sourceSetID = JavaTestProject.DEFAULT_SOURCE_SET_ID, dependentSourceSets = setOf(), sourceRoots = additionalSourceRoots + setOf(JavaTestProject.DEFAULT_SOURCE_ROOT), classpath = additionalClasspath, // TODO [beresnev] is kotlin jvm stdlib needed here? diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.kt index 39f0f0f6..9ce85961 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/java/JavaTestProject.kt @@ -4,6 +4,7 @@ package org.jetbrains.dokka.analysis.test.api.jvm.java +import org.jetbrains.dokka.DokkaSourceSetID import org.jetbrains.dokka.analysis.test.api.TestData import org.jetbrains.dokka.analysis.test.api.TestDataFile import org.jetbrains.dokka.analysis.test.api.TestProject @@ -67,7 +68,8 @@ class JavaTestProject : TestProject, JavaFileCreator, MdFileCreator { ")" } - internal companion object { - internal const val DEFAULT_SOURCE_ROOT = "/src/main/java" + companion object { + const val DEFAULT_SOURCE_ROOT = "/src/main/java" + val DEFAULT_SOURCE_SET_ID = DokkaSourceSetID(scopeId = "project", sourceSetName = "java") } } diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt index e5424ead..79028056 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmConfigurationBuilder.kt @@ -44,7 +44,7 @@ class KotlinJvmTestSourceSetBuilder : BaseTestDokkaSourceSetBuilder() { return TestDokkaSourceSet( analysisPlatform = Platform.jvm, displayName = "KotlinJvmSourceSet", - sourceSetID = DokkaSourceSetID(scopeId = "project", sourceSetName = "kotlin"), + sourceSetID = KotlinJvmTestProject.DEFAULT_SOURCE_SET_ID, dependentSourceSets = setOf(), sourceRoots = additionalSourceRoots + setOf(KotlinJvmTestProject.DEFAULT_SOURCE_ROOT), classpath = additionalClasspath + setOf(getKotlinJvmStdlibJarPath()), diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt index 178a1dc3..d67e1321 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/kotlin/KotlinJvmTestProject.kt @@ -4,6 +4,7 @@ package org.jetbrains.dokka.analysis.test.api.jvm.kotlin +import org.jetbrains.dokka.DokkaSourceSetID import org.jetbrains.dokka.analysis.test.api.TestData import org.jetbrains.dokka.analysis.test.api.TestDataFile import org.jetbrains.dokka.analysis.test.api.TestProject @@ -85,8 +86,9 @@ class KotlinJvmTestProject : TestProject, KtFileCreator, MdFileCreator, KotlinSa ")" } - internal companion object { - internal const val DEFAULT_SOURCE_ROOT = "/src/main/kotlin" + companion object { + const val DEFAULT_SOURCE_ROOT = "/src/main/kotlin" + val DEFAULT_SOURCE_SET_ID = DokkaSourceSetID(scopeId = "project", sourceSetName = "kotlin") } } diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt index 32b7ce0a..45ca53e2 100644 --- a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/jvm/mixed/MixedJvmTestProject.kt @@ -10,6 +10,9 @@ 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.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.markdown.MarkdownTestData import org.jetbrains.dokka.analysis.test.api.markdown.MarkdownTestDataFile import org.jetbrains.dokka.analysis.test.api.markdown.MdFileCreator @@ -20,13 +23,14 @@ 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 { +class MixedJvmTestProject : TestProject, MdFileCreator, KotlinSampleFileCreator { 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() + private val kotlinSampleTestData = KotlinSampleTestData() @AnalysisTestDslMarker fun dokkaConfiguration(fillConfiguration: MixedJvmTestConfigurationBuilder.() -> Unit) { @@ -48,6 +52,15 @@ class MixedJvmTestProject : TestProject, MdFileCreator { 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() } @@ -62,7 +75,8 @@ class MixedJvmTestProject : TestProject, MdFileCreator { return flatListOf( this@MixedJvmTestProject.kotlinSourceDirectory.getFiles(), this@MixedJvmTestProject.javaSourceDirectory.getFiles(), - this@MixedJvmTestProject.markdownTestData.getFiles() + this@MixedJvmTestProject.markdownTestData.getFiles(), + this@MixedJvmTestProject.kotlinSampleTestData.getFiles() ) } } diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/DokkaLoggerUtils.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/DokkaLoggerUtils.kt new file mode 100644 index 00000000..87de4540 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/DokkaLoggerUtils.kt @@ -0,0 +1,63 @@ +/* + * 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.DokkaConsoleLogger +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.dokka.utilities.LoggingLevel + +/** + * Prints messages to the console according to the passed `consoleMinLevel` parameter, + * and collects **ALL** log messages, regarding of the set logging level. + * + * Useful if you need to verify that user-friendly log messages were emitted, + * in case they outline actionable problems or help solve a problem and are + * considered to be a vital part of this product. + * + * The collected messages can be retrieved by invoking [collectedLogMessages]. + */ +class CollectingDokkaConsoleLogger( + consoleMinLoggingLevel: LoggingLevel = LoggingLevel.INFO +) : DokkaLogger { + + private val consoleLogger = DokkaConsoleLogger(consoleMinLoggingLevel) + private val _collectedLogMessages = mutableListOf<String>() + + val collectedLogMessages: List<String> = _collectedLogMessages + + override var warningsCount: Int + get() = consoleLogger.warningsCount + set(value) { consoleLogger.warningsCount = value } + + override var errorsCount: Int + get() = consoleLogger.errorsCount + set(value) { consoleLogger.errorsCount = value } + + + override fun debug(message: String) { + _collectedLogMessages.add(message) + consoleLogger.debug(message) + } + + override fun info(message: String) { + _collectedLogMessages.add(message) + consoleLogger.info(message) + } + + override fun progress(message: String) { + _collectedLogMessages.add(message) + consoleLogger.progress(message) + } + + override fun warn(message: String) { + _collectedLogMessages.add(message) + consoleLogger.warn(message) + } + + override fun error(message: String) { + _collectedLogMessages.add(message) + consoleLogger.error(message) + } +} diff --git a/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/TestAnalysisApiUtils.kt b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/TestAnalysisApiUtils.kt new file mode 100644 index 00000000..18a04ae5 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-api/src/testFixtures/kotlin/org/jetbrains/dokka/analysis/test/api/util/TestAnalysisApiUtils.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.util + +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.DokkaSourceSetID +import org.jetbrains.dokka.analysis.test.api.analysis.TestAnalysisContext +import org.jetbrains.dokka.analysis.test.api.jvm.java.JavaTestProject +import org.jetbrains.dokka.analysis.test.api.jvm.kotlin.KotlinJvmTestProject + +/** + * @return the only existing source set or an exception + */ +fun TestAnalysisContext.singleSourceSet(): DokkaConfiguration.DokkaSourceSet { + return this.configuration.sourceSets.single() +} + +fun TestAnalysisContext.defaultKotlinSourceSet() = findSourceSetById(KotlinJvmTestProject.DEFAULT_SOURCE_SET_ID) +fun TestAnalysisContext.defaultJavaSourceSet() = findSourceSetById(JavaTestProject.DEFAULT_SOURCE_SET_ID) + +fun TestAnalysisContext.findSourceSetById(dokkaSourceSetID: DokkaSourceSetID): DokkaConfiguration.DokkaSourceSet { + return this.configuration.sourceSets.single { + it.sourceSetID == dokkaSourceSetID + } +} diff --git a/dokka-subprojects/analysis-kotlin-descriptors-compiler/api/analysis-kotlin-descriptors-compiler.api b/dokka-subprojects/analysis-kotlin-descriptors-compiler/api/analysis-kotlin-descriptors-compiler.api index 373ec268..c08cbd62 100644 --- a/dokka-subprojects/analysis-kotlin-descriptors-compiler/api/analysis-kotlin-descriptors-compiler.api +++ b/dokka-subprojects/analysis-kotlin-descriptors-compiler/api/analysis-kotlin-descriptors-compiler.api @@ -10,7 +10,6 @@ public final class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/Comp public final fun getKdocFinder ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; public final fun getKlibService ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; public final fun getKotlinAnalysis ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; - public final fun getKotlinSampleProviderFactory ()Lorg/jetbrains/dokka/plugability/Extension; public final fun getMockApplicationHack ()Lorg/jetbrains/dokka/plugability/ExtensionPoint; } @@ -85,21 +84,6 @@ public abstract class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/c public final fun get (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;)Lorg/jetbrains/dokka/analysis/kotlin/descriptors/compiler/configuration/AnalysisContext; } -public class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/KotlinSampleProvider : org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider { - public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V - public fun close ()V - public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; - public fun getSample (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/lang/String;)Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider$SampleSnippet; - protected fun processBody (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; - protected fun processImports (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; -} - -public final class org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/KotlinSampleProviderFactory : org/jetbrains/dokka/analysis/kotlin/internal/SampleProviderFactory { - public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V - public fun build ()Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider; - public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; -} - public final class org/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl : com/intellij/core/CoreJavaFileManager, org/jetbrains/kotlin/resolve/jvm/KotlinCliJavaFileManager { public static final field Companion Lorg/jetbrains/kotlin/cli/jvm/compiler/KotlinCliJavaFileManagerImpl$Companion; public fun <init> (Lcom/intellij/psi/PsiManager;)V 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 c59a43b2..e8ebceb0 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 @@ -11,6 +11,7 @@ import org.jetbrains.dokka.InternalDokkaApi import org.jetbrains.dokka.analysis.java.BreakingAbstractionKotlinLightMethodChecker import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.DokkaAnalysisConfiguration +import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.KotlinAnalysis import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.ProjectKotlinAnalysis import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.impl.* @@ -20,7 +21,6 @@ import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator.Defau import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.translator.DefaultExternalDocumentablesProvider import org.jetbrains.dokka.renderers.PostAction import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin -import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory import org.jetbrains.dokka.plugability.* import org.jetbrains.kotlin.asJava.elements.KtLightAbstractAnnotation @@ -75,13 +75,8 @@ public class CompilerDescriptorAnalysisPlugin : DokkaPlugin() { plugin<InternalKotlinAnalysisPlugin>().fullClassHierarchyBuilder providing { DescriptorFullClassHierarchyBuilder() } } - /** - * StdLib has its own a sample provider - * So it should have a possibility to override this extension - */ - @InternalDokkaApi - public val kotlinSampleProviderFactory: Extension<SampleProviderFactory, *, *> by extending { - plugin<InternalKotlinAnalysisPlugin>().sampleProviderFactory providing ::KotlinSampleProviderFactory + internal val descriptorSampleAnalysisEnvironmentCreator by extending { + plugin<KotlinAnalysisPlugin>().sampleAnalysisEnvironmentCreator providing ::DescriptorSampleAnalysisEnvironmentCreator } internal val descriptorSyntheticDocumentableDetector by extending { diff --git a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorSampleAnalysisEnvironment.kt b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorSampleAnalysisEnvironment.kt new file mode 100644 index 00000000..3df3d22c --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/DescriptorSampleAnalysisEnvironment.kt @@ -0,0 +1,195 @@ +/* + * 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.kotlin.descriptors.compiler.impl + +import com.intellij.psi.PsiElement +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DokkaConfiguration +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.SamplesKotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironment +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironmentCreator +import org.jetbrains.dokka.analysis.kotlin.sample.SampleSnippet +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.DokkaPluginApiPreview +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.lazy.ResolveSession +import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyPackageDescriptor +import org.jetbrains.kotlin.resolve.source.KotlinSourceElement + +internal class DescriptorSampleAnalysisEnvironmentCreator( + private val context: DokkaContext, +) : SampleAnalysisEnvironmentCreator { + + private val descriptorAnalysisPlugin = context.plugin<CompilerDescriptorAnalysisPlugin>() + + override fun <T> use(block: SampleAnalysisEnvironment.() -> T): T { + // Run from the thread of Dispatchers.Default as it can help + // avoid memory leaks through the compiler's ThreadLocals. + // Might not be relevant if the project stops using coroutines. + return runBlocking(Dispatchers.Default) { + @OptIn(DokkaPluginApiPreview::class) + SamplesKotlinAnalysis( + sourceSets = context.configuration.sourceSets, + context = context, + projectKotlinAnalysis = descriptorAnalysisPlugin.querySingle { kotlinAnalysis } + ).use { kotlinAnalysis -> + val sampleAnalysis = DescriptorSampleAnalysisEnvironment( + kdocFinder = descriptorAnalysisPlugin.querySingle { kdocFinder }, + kotlinAnalysis = kotlinAnalysis, + dokkaLogger = context.logger + ) + block(sampleAnalysis) + } + } + } +} + +internal class DescriptorSampleAnalysisEnvironment( + private val kdocFinder: KDocFinder, + private val kotlinAnalysis: KotlinAnalysis, + private val dokkaLogger: DokkaLogger, +) : SampleAnalysisEnvironment { + + override fun resolveSample( + sourceSet: DokkaConfiguration.DokkaSourceSet, + fullyQualifiedLink: String, + ): SampleSnippet? { + val resolveSession = kotlinAnalysis[sourceSet].resolveSession + + val samplePsiElement = resolveSession.resolveSamplePsiElement(sourceSet, fullyQualifiedLink) + if (samplePsiElement == null) { + dokkaLogger.debug("Cannot resolve sample element for: \"$fullyQualifiedLink\"") + return null + } else if (samplePsiElement.containingFile !is KtFile) { + dokkaLogger.warn("Unable to resolve non-Kotlin @sample links: \"$fullyQualifiedLink\"") + return null + } + + return SampleSnippet( + imports = processImports(samplePsiElement), + body = processBody(samplePsiElement) + ) + } + + private fun ResolveSession.resolveSamplePsiElement( + dokkaSourceSet: DokkaConfiguration.DokkaSourceSet, + fqLink: String, + ): PsiElement? { + val packageDescriptor = resolveNearestPackageDescriptor(fqLink) + if (packageDescriptor == null) { + dokkaLogger.debug( + "Unable to resolve package descriptor for @sample: \"$fqLink\";" + ) + return null + } + + val kdocLink = kdocFinder.resolveKDocLink( + fromDescriptor = packageDescriptor, + qualifiedName = fqLink, + sourceSet = dokkaSourceSet, + emptyBindingContext = true + ).firstOrNull() + + if (kdocLink == null) { + dokkaLogger.warn( + "Unable to resolve a @sample link: \"$fqLink\". Is it used correctly? " + + "Expecting a link to a reachable (resolvable) Kotlin function." + ) + return null + } else if (kdocLink.toSourceElement !is KotlinSourceElement) { + dokkaLogger.warn("Unable to resolve non-Kotlin @sample links: \"$fqLink\"") + return null + } else if (kdocLink !is FunctionDescriptor) { + dokkaLogger.warn("Unable to process a @sample link: \"$fqLink\". Only function links allowed.") + return null + } + return DescriptorToSourceUtils.descriptorToDeclaration(kdocLink) + } + + /** + * Tries to resolve [fqLink]'s package. + * + * Since [fqLink] can be both a link to a top-level function and a link to a function within a class, + * we cannot tell for sure if [fqLink] contains a class name or not (relying on case letters is error-prone, + * there are exceptions). But we know for sure that the last element in the link is the function. + * + * So we start with what we think is the deepest package path, and if we cannot find a package descriptor + * for it - we drop one level and try again, until we find something or reach root. + * + * This function should also account for links to declarations within the root package (`""`). + * + * Here are some examples: + * + * Given [fqLink] = `com.example.ClassName.functionName`: + * 1) First pass, trying to resolve package `com.example.ClassName`. Failure. + * 2) Second pass, trying to resolve package `com.example`. Success. + * + * Given [fqLink] = `com.example.functionName`: + * 1) First pass, trying to resolve package `com.example`. Success. + * + * Given [fqLink] = `ClassName.functionName` (root package): + * 1) First pass, trying to resolve package `ClassName`. Failure. + * 2) Second pass, trying to resolve package `""`. Success. + */ + private fun ResolveSession.resolveNearestPackageDescriptor(fqLink: String): LazyPackageDescriptor? { + val isRootPackage = !fqLink.contains('.') + val supposedPackageName = if (isRootPackage) "" else fqLink.substringBeforeLast(".") + + val packageDescriptor = this.getPackageFragment(FqName(supposedPackageName)) + if (packageDescriptor != null) { + return packageDescriptor + } + dokkaLogger.debug("Failed to resolve package \"$supposedPackageName\" for sample \"$fqLink\"") + + if (isRootPackage) { + // cannot go any deeper + return null + } + + return resolveNearestPackageDescriptor(supposedPackageName.substringBeforeLast(".")) + } + + private fun processImports(sampleElement: PsiElement): List<String> { + val psiFile = sampleElement.containingFile + + val importsList = (psiFile as? KtFile)?.importList ?: return emptyList() + return importsList.imports + .map { it.text.removePrefix("import ") } + .filter { it.isNotBlank() } + } + + private fun processBody(sampleElement: PsiElement): String { + return getSampleBody(sampleElement) + .trim { it == '\n' || it == '\r' } + .trimEnd() + .trimIndent() + } + + private fun getSampleBody(sampleElement: PsiElement): String { + return when (sampleElement) { + is KtDeclarationWithBody -> { + when (val bodyExpression = sampleElement.bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + + else -> sampleElement.text + } + } +} diff --git a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/KotlinSampleProvider.kt b/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/KotlinSampleProvider.kt deleted file mode 100644 index 5199abf5..00000000 --- a/dokka-subprojects/analysis-kotlin-descriptors-compiler/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/descriptors/compiler/impl/KotlinSampleProvider.kt +++ /dev/null @@ -1,119 +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.analysis.kotlin.descriptors.compiler.impl - -import com.intellij.psi.PsiElement -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.InternalDokkaApi -import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin -import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.KDocFinder -import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.configuration.SamplesKotlinAnalysis -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.querySingle -import org.jetbrains.dokka.analysis.kotlin.internal.SampleProvider -import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory -import org.jetbrains.dokka.plugability.DokkaPluginApiPreview -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.psi.KtBlockExpression -import org.jetbrains.kotlin.psi.KtDeclarationWithBody -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.resolve.lazy.ResolveSession - -public class KotlinSampleProviderFactory( - public val context: DokkaContext -): SampleProviderFactory { - override fun build(): SampleProvider { - return KotlinSampleProvider(context) - } - -} -/** - * It's declared as open since StdLib has its own sample transformer - * with [processBody] and [processImports] - */ -@InternalDokkaApi -public open class KotlinSampleProvider( - public val context: DokkaContext -): SampleProvider { - private val kDocFinder: KDocFinder = context.plugin<CompilerDescriptorAnalysisPlugin>().querySingle { kdocFinder } - private val analysis = lazy { - /** - * Run from the thread of [Dispatchers.Default]. It can help to avoid a memory leaks in `ThreadLocal`s (that keep `URLCLassLoader`) - * since we shut down Dispatchers.Default at the end of each task (see [org.jetbrains.dokka.DokkaConfiguration.finalizeCoroutines]). - * Currently, all `ThreadLocal`s are in a compiler/IDE codebase. - */ - runBlocking(Dispatchers.Default) { - @OptIn(DokkaPluginApiPreview::class) - SamplesKotlinAnalysis( - sourceSets = context.configuration.sourceSets, - context = context, - projectKotlinAnalysis = context.plugin<CompilerDescriptorAnalysisPlugin>() - .querySingle { kotlinAnalysis } - ) - } - } - protected open fun processBody(psiElement: PsiElement): String { - val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() - val lines = text.split("\n") - val indent = lines.filter(String::isNotBlank).map { it.takeWhile(Char::isWhitespace).count() }.minOrNull() ?: 0 - return lines.joinToString("\n") { it.drop(indent) } - } - - private fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { - is KtDeclarationWithBody -> { - when (val bodyExpression = psiElement.bodyExpression) { - is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") - else -> bodyExpression!!.text - } - } - else -> psiElement.text - } - - protected open fun processImports(psiElement: PsiElement): String { - val psiFile = psiElement.containingFile - return when(val text = (psiFile as? KtFile)?.importList?.text) { - is String -> text - else -> "" - } - } - - /** - * @return [SampleProvider.SampleSnippet] or null if it has not found by [fqLink] - */ - override fun getSample(sourceSet: DokkaConfiguration.DokkaSourceSet, fqLink: String): SampleProvider.SampleSnippet? { - return runBlocking(Dispatchers.Default) { - val resolveSession = analysis.value[sourceSet].resolveSession - val psiElement = fqNameToPsiElement(resolveSession, fqLink, sourceSet) - ?: return@runBlocking null.also { context.logger.warn("Cannot find PsiElement corresponding to $fqLink") } - val imports = - processImports(psiElement) - val body = processBody(psiElement) - return@runBlocking SampleProvider.SampleSnippet(imports, body) - } - } - override fun close() { - if(analysis.isInitialized()) - analysis.value.close() - } - - private fun fqNameToPsiElement(resolveSession: ResolveSession, functionName: String, dokkaSourceSet: DokkaConfiguration.DokkaSourceSet): PsiElement? { - val packageName = functionName.takeWhile { it != '.' } - val descriptor = resolveSession.getPackageFragment(FqName(packageName)) - ?: return null.also { context.logger.warn("Cannot find descriptor for package $packageName") } - - with (kDocFinder) { - val symbol = resolveKDocLink( - descriptor, - functionName, - dokkaSourceSet, - emptyBindingContext = true - ).firstOrNull() ?: return null.also { context.logger.warn("Unresolved function $functionName in @sample") } - return org.jetbrains.kotlin.resolve.DescriptorToSourceUtils.descriptorToDeclaration(symbol) - } - } -} diff --git a/dokka-subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api b/dokka-subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api index 4bddfcf1..dbd74f89 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api +++ b/dokka-subprojects/analysis-kotlin-symbols/api/analysis-kotlin-symbols.api @@ -2,18 +2,3 @@ public final class org/jetbrains/dokka/analysis/kotlin/symbols/plugin/SymbolsAna public fun <init> ()V } -public class org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider : org/jetbrains/dokka/analysis/kotlin/internal/SampleProvider { - public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V - public fun close ()V - public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; - public fun getSample (Lorg/jetbrains/dokka/DokkaConfiguration$DokkaSourceSet;Ljava/lang/String;)Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider$SampleSnippet; - protected fun processBody (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; - protected fun processImports (Lcom/intellij/psi/PsiElement;)Ljava/lang/String; -} - -public final class org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProviderFactory : org/jetbrains/dokka/analysis/kotlin/internal/SampleProviderFactory { - public fun <init> (Lorg/jetbrains/dokka/plugability/DokkaContext;)V - public fun build ()Lorg/jetbrains/dokka/analysis/kotlin/internal/SampleProvider; - public final fun getContext ()Lorg/jetbrains/dokka/plugability/DokkaContext; -} - diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt index d8a4e476..36492cdd 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/KDocProvider.kt @@ -64,7 +64,7 @@ internal fun KtAnalysisSession.getKDocDocumentationFrom(symbol: KtSymbol, logger parseFromKDocTag( kDocTag = kDocContent.contentTag, - externalDri = { link -> resolveKDocLink(link).ifUnresolved { logger.logUnresolvedLink(link.getLinkText(), kdocLocation) } }, + externalDri = { link -> resolveKDocLinkDRI(link).ifUnresolved { logger.logUnresolvedLink(link.getLinkText(), kdocLocation) } }, kdocLocation = kdocLocation ) } diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt index 9a0b81bd..c719c855 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/ResolveKDocLink.kt @@ -9,6 +9,7 @@ import org.jetbrains.dokka.analysis.kotlin.symbols.translators.getDRIFromSymbol import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.utilities.DokkaLogger import org.jetbrains.kotlin.analysis.api.KtAnalysisSession +import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.kdoc.psi.api.KDoc import org.jetbrains.kotlin.kdoc.psi.impl.KDocLink @@ -34,7 +35,23 @@ internal inline fun DRI?.ifUnresolved(action: () -> Unit): DRI? = this ?: run { * * @return [DRI] or null if the [link] is unresolved */ -internal fun KtAnalysisSession.resolveKDocTextLink(link: String, context: PsiElement? = null): DRI? { +internal fun KtAnalysisSession.resolveKDocTextLinkDRI(link: String, context: PsiElement? = null): DRI? { + val kDocLink = createKDocLink(link, context) + return kDocLink?.let { resolveKDocLinkDRI(it) } +} + +/** + * If the [link] is ambiguous, i.e. leads to more than one declaration, + * it returns deterministically any declaration. + * + * @return [KtSymbol] or null if the [link] is unresolved + */ +internal fun KtAnalysisSession.resolveKDocTextLinkSymbol(link: String, context: PsiElement? = null): KtSymbol? { + val kDocLink = createKDocLink(link, context) + return kDocLink?.let { resolveToSymbol(it) } +} + +private fun KtAnalysisSession.createKDocLink(link: String, context: PsiElement? = null): KDocLink? { val psiFactory = context?.let { KtPsiFactory.contextual(it) } ?: KtPsiFactory(this.useSiteModule.project) val kDoc = psiFactory.createComment( """ @@ -43,8 +60,8 @@ internal fun KtAnalysisSession.resolveKDocTextLink(link: String, context: PsiEle */ """.trimIndent() ) as? KDoc - val kDocLink = kDoc?.getDefaultSection()?.children?.filterIsInstance<KDocLink>()?.singleOrNull() - return kDocLink?.let { resolveKDocLink(it) } + + return kDoc?.getDefaultSection()?.children?.filterIsInstance<KDocLink>()?.singleOrNull() } /** @@ -53,9 +70,13 @@ internal fun KtAnalysisSession.resolveKDocTextLink(link: String, context: PsiEle * * @return [DRI] or null if the [link] is unresolved */ -internal fun KtAnalysisSession.resolveKDocLink(link: KDocLink): DRI? { - val lastNameSegment = link.children.filterIsInstance<KDocName>().lastOrNull() - val linkedSymbol = lastNameSegment?.mainReference?.resolveToSymbols()?.firstOrNull() +internal fun KtAnalysisSession.resolveKDocLinkDRI(link: KDocLink): DRI? { + val linkedSymbol = resolveToSymbol(link) return if (linkedSymbol == null) null else getDRIFromSymbol(linkedSymbol) } + +private fun KtAnalysisSession.resolveToSymbol(kDocLink: KDocLink): KtSymbol? { + val lastNameSegment = kDocLink.children.filterIsInstance<KDocName>().lastOrNull() + return lastNameSegment?.mainReference?.resolveToSymbols()?.firstOrNull() +} diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt index 4622ffd8..9f2e6873 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/SyntheticKDocProvider.kt @@ -54,7 +54,7 @@ internal fun KtAnalysisSession.getGeneratedKDocDocumentationFrom(symbol: KtSymbo private fun KtAnalysisSession.loadTemplate(filePath: String): DocumentationNode? { val kdoc = loadContent(filePath) ?: return null val externalDriProvider = { link: String -> - resolveKDocTextLink(link) + resolveKDocTextLinkDRI(link) } val parser = MarkdownParser(externalDriProvider, filePath) diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt index 0ee95e45..846ee61d 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/java/KotlinDocCommentParser.kt @@ -11,7 +11,7 @@ import org.jetbrains.dokka.analysis.java.parsers.DocCommentParser import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.* import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logUnresolvedLink import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.parseFromKDocTag -import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocLink +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocLinkDRI import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin import org.jetbrains.dokka.model.doc.DocumentationNode import org.jetbrains.dokka.plugability.DokkaContext @@ -42,7 +42,7 @@ internal class KotlinDocCommentParser( return analyze(kotlinAnalysis.getModule(sourceSet)) { parseFromKDocTag( kDocTag = element.comment, - externalDri = { link -> resolveKDocLink(link).ifUnresolved { context.logger.logUnresolvedLink(link.getLinkText(), elementName) } }, + externalDri = { link -> resolveKDocLinkDRI(link).ifUnresolved { context.logger.logUnresolvedLink(link.getLinkText(), elementName) } }, kdocLocation = null, parseWithChildren = parseWithChildren ) diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt index f5cfbdb9..1fdebe24 100644 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/kdoc/moduledocs/ModuleAndPackageDocumentationParsingContext.kt @@ -10,7 +10,7 @@ import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.logUnresolvedLink import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.KotlinAnalysis import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Module import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.moduledocs.ModuleAndPackageDocumentation.Classifier.Package -import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLink +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLinkDRI import org.jetbrains.dokka.analysis.markdown.jb.MarkdownParser import org.jetbrains.dokka.model.doc.DocumentationNode import org.jetbrains.dokka.utilities.DokkaLogger @@ -47,7 +47,7 @@ internal fun ModuleAndPackageDocumentationParsingContext( MarkdownParser( externalDri = { link -> analyze(sourceModule) { - resolveKDocTextLink( + resolveKDocTextLinkDRI( link, contextPsi ).ifUnresolved { 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 122e0b10..23bb0dc5 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 @@ -9,6 +9,7 @@ import com.intellij.psi.PsiAnnotation import org.jetbrains.dokka.CoreExtensions import org.jetbrains.dokka.analysis.java.BreakingAbstractionKotlinLightMethodChecker import org.jetbrains.dokka.analysis.java.JavaAnalysisPlugin +import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.KotlinInheritDocTagContentProvider import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.java.DescriptorKotlinDocCommentCreator @@ -117,8 +118,8 @@ public class SymbolsAnalysisPlugin : DokkaPlugin() { plugin<InternalKotlinAnalysisPlugin>().externalDocumentablesProvider providing ::SymbolExternalDocumentablesProvider } - internal val kotlinSampleProviderFactory by extending { - plugin<InternalKotlinAnalysisPlugin>().sampleProviderFactory providing ::KotlinSampleProviderFactory + internal val symbolSampleAnalysisEnvironmentCreator by extending { + plugin<KotlinAnalysisPlugin>().sampleAnalysisEnvironmentCreator providing ::SymbolSampleAnalysisEnvironmentCreator } @OptIn(DokkaPluginApiPreview::class) diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt deleted file mode 100644 index e453c72d..00000000 --- a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/KotlinSampleProvider.kt +++ /dev/null @@ -1,100 +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.analysis.kotlin.symbols.services - -import com.intellij.psi.PsiElement -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.InternalDokkaApi -import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.querySingle -import org.jetbrains.dokka.analysis.kotlin.internal.SampleProvider -import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory -import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SamplesKotlinAnalysis -import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin -import org.jetbrains.kotlin.analysis.api.analyze -import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.name.Name -import org.jetbrains.kotlin.psi.KtBlockExpression -import org.jetbrains.kotlin.psi.KtDeclarationWithBody -import org.jetbrains.kotlin.psi.KtFile - -public class KotlinSampleProviderFactory( - public val context: DokkaContext -): SampleProviderFactory { - override fun build(): SampleProvider { - return KotlinSampleProvider(context) - } - -} -/** - * It's declared as open since StdLib has its own sample transformer - * with [processBody] and [processImports] - */ -@InternalDokkaApi -public open class KotlinSampleProvider( - public val context: DokkaContext -): SampleProvider { - private val kotlinAnalysisOfRegularSources = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } - - private val kotlinAnalysisOfSamples = SamplesKotlinAnalysis( - sourceSets = context.configuration.sourceSets, context = context - ) - - protected open fun processBody(psiElement: PsiElement): String { - val text = processSampleBody(psiElement).trim { it == '\n' || it == '\r' }.trimEnd() - val lines = text.split("\n") - val indent = lines.filter(String::isNotBlank).minOfOrNull { it.takeWhile(Char::isWhitespace).count() } ?: 0 - return lines.joinToString("\n") { it.drop(indent) } - } - - private fun processSampleBody(psiElement: PsiElement): String = when (psiElement) { - is KtDeclarationWithBody -> { - when (val bodyExpression = psiElement.bodyExpression) { - is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") - else -> bodyExpression!!.text - } - } - else -> psiElement.text - } - - protected open fun processImports(psiElement: PsiElement): String { - val psiFile = psiElement.containingFile - return when(val text = (psiFile as? KtFile)?.importList?.text) { - is String -> text - else -> "" - } - } - - /** - * @return [SampleProvider.SampleSnippet] or null if it has not found by [fqLink] - */ - override fun getSample(sourceSet: DokkaConfiguration.DokkaSourceSet, fqLink: String): SampleProvider.SampleSnippet? { - return kotlinAnalysisOfSamples.getModuleOrNull(sourceSet)?.let { getSampleFromModule(it, fqLink) } - ?: getSampleFromModule( - kotlinAnalysisOfRegularSources.getModule(sourceSet), fqLink - ) - } - private fun getSampleFromModule(module: KtSourceModule, fqLink: String): SampleProvider.SampleSnippet? { - val psiElement = analyze(module) { - val lastDotIndex = fqLink.lastIndexOf('.') - - val functionName = if (lastDotIndex == -1) fqLink else fqLink.substring(lastDotIndex + 1, fqLink.length) - val packageName = if (lastDotIndex == -1) "" else fqLink.substring(0, lastDotIndex) - getTopLevelCallableSymbols(FqName(packageName), Name.identifier(functionName)).firstOrNull()?.psi - } - ?: return null.also { context.logger.warn("Cannot find PsiElement corresponding to $fqLink") } - val imports = - processImports(psiElement) - val body = processBody(psiElement) - - return SampleProvider.SampleSnippet(imports, body) - } - - override fun close() { - kotlinAnalysisOfSamples.close() - } -} diff --git a/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSampleAnalysisEnvironment.kt b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSampleAnalysisEnvironment.kt new file mode 100644 index 00000000..37095df5 --- /dev/null +++ b/dokka-subprojects/analysis-kotlin-symbols/src/main/kotlin/org/jetbrains/dokka/analysis/kotlin/symbols/services/SymbolSampleAnalysisEnvironment.kt @@ -0,0 +1,118 @@ +/* + * 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.kotlin.symbols.services + +import com.intellij.psi.PsiElement +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironment +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironmentCreator +import org.jetbrains.dokka.analysis.kotlin.sample.SampleSnippet +import org.jetbrains.dokka.analysis.kotlin.symbols.kdoc.resolveKDocTextLinkSymbol +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.KotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SamplesKotlinAnalysis +import org.jetbrains.dokka.analysis.kotlin.symbols.plugin.SymbolsAnalysisPlugin +import org.jetbrains.dokka.plugability.DokkaContext +import org.jetbrains.dokka.plugability.plugin +import org.jetbrains.dokka.plugability.querySingle +import org.jetbrains.dokka.utilities.DokkaLogger +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.api.symbols.sourcePsiSafe +import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtDeclarationWithBody +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtFunction + +internal class SymbolSampleAnalysisEnvironmentCreator( + private val context: DokkaContext, +) : SampleAnalysisEnvironmentCreator { + + private val projectKotlinAnalysis = context.plugin<SymbolsAnalysisPlugin>().querySingle { kotlinAnalysis } + + override fun <T> use(block: SampleAnalysisEnvironment.() -> T): T { + return runBlocking(Dispatchers.Default) { + SamplesKotlinAnalysis( + sourceSets = context.configuration.sourceSets, + context = context + ).use { samplesKotlinAnalysis -> + val sampleAnalysisEnvironment = SymbolSampleAnalysisEnvironment( + samplesKotlinAnalysis = samplesKotlinAnalysis, + projectKotlinAnalysis = projectKotlinAnalysis, + dokkaLogger = context.logger + ) + block(sampleAnalysisEnvironment) + } + } + } +} + +private class SymbolSampleAnalysisEnvironment( + private val samplesKotlinAnalysis: KotlinAnalysis, + private val projectKotlinAnalysis: KotlinAnalysis, + private val dokkaLogger: DokkaLogger, +) : SampleAnalysisEnvironment { + + override fun resolveSample(sourceSet: DokkaSourceSet, fullyQualifiedLink: String): SampleSnippet? { + val psiElement = findPsiElement(sourceSet, fullyQualifiedLink) + if (psiElement == null) { + dokkaLogger.warn( + "Unable to resolve a @sample link: \"$fullyQualifiedLink\". Is it used correctly? " + + "Expecting a link to a reachable (resolvable) top-level Kotlin function." + ) + return null + } else if (psiElement.language != KotlinLanguage.INSTANCE) { + dokkaLogger.warn("Unable to resolve non-Kotlin @sample links: \"$fullyQualifiedLink\"") + return null + } else if (psiElement !is KtFunction) { + dokkaLogger.warn("Unable to process a @sample link: \"$fullyQualifiedLink\". Only function links allowed.") + return null + } + + val imports = processImports(psiElement) + val body = processBody(psiElement) + + return SampleSnippet(imports, body) + } + + private fun findPsiElement(sourceSet: DokkaSourceSet, fqLink: String): PsiElement? { + val ktSourceModule = samplesKotlinAnalysis.getModuleOrNull(sourceSet) + ?: projectKotlinAnalysis.getModule(sourceSet) + + return analyze(ktSourceModule) { + resolveKDocTextLinkSymbol(fqLink) + ?.sourcePsiSafe() + } + } + + private fun processImports(psiElement: PsiElement): List<String> { + val psiFile = psiElement.containingFile + val importsList = (psiFile as? KtFile)?.importList ?: return emptyList() + return importsList.imports + .map { it.text.removePrefix("import ") } + .filter { it.isNotBlank() } + } + + private fun processBody(sampleElement: PsiElement): String { + return getSampleBody(sampleElement) + .trim { it == '\n' || it == '\r' } + .trimEnd() + .trimIndent() + } + + private fun getSampleBody(sampleElement: PsiElement): String { + return when (sampleElement) { + is KtDeclarationWithBody -> { + when (val bodyExpression = sampleElement.bodyExpression) { + is KtBlockExpression -> bodyExpression.text.removeSurrounding("{", "}") + else -> bodyExpression!!.text + } + } + + else -> sampleElement.text + } + } +} diff --git a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt index 1ba049c8..6e4fef4e 100644 --- a/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt +++ b/dokka-subprojects/plugin-base/src/main/kotlin/org/jetbrains/dokka/base/transformers/pages/DefaultSamplesTransformer.kt @@ -4,6 +4,7 @@ package org.jetbrains.dokka.base.transformers.pages +import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.DisplaySourceSet import org.jetbrains.dokka.model.doc.Sample @@ -13,18 +14,18 @@ import org.jetbrains.dokka.plugability.DokkaContext import org.jetbrains.dokka.plugability.plugin import org.jetbrains.dokka.plugability.querySingle import org.jetbrains.dokka.transformers.pages.PageTransformer -import org.jetbrains.dokka.analysis.kotlin.internal.InternalKotlinAnalysisPlugin -import org.jetbrains.dokka.analysis.kotlin.internal.SampleProvider -import org.jetbrains.dokka.analysis.kotlin.internal.SampleProviderFactory +import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironmentCreator +import org.jetbrains.dokka.analysis.kotlin.sample.SampleSnippet internal const val KOTLIN_PLAYGROUND_SCRIPT = "https://unpkg.com/kotlin-playground@1/dist/playground.min.js" internal class DefaultSamplesTransformer(val context: DokkaContext) : PageTransformer { - private val sampleProviderFactory: SampleProviderFactory = context.plugin<InternalKotlinAnalysisPlugin>().querySingle { sampleProviderFactory } + private val sampleAnalysisEnvironment: SampleAnalysisEnvironmentCreator = + context.plugin<KotlinAnalysisPlugin>().querySingle { sampleAnalysisEnvironmentCreator } override fun invoke(input: RootPageNode): RootPageNode { - return sampleProviderFactory.build().use { sampleProvider -> + return sampleAnalysisEnvironment.use { input.transformContentPagesTree { page -> val samples = (page as? WithDocumentables)?.documentables?.flatMap { it.documentation.entries.flatMap { entry -> @@ -33,7 +34,7 @@ internal class DefaultSamplesTransformer(val context: DokkaContext) : PageTransf } ?: return@transformContentPagesTree page val newContent = samples.fold(page.content) { acc, (sampleSourceSet, sample) -> - sampleProvider.getSample(sampleSourceSet, sample.name) + resolveSample(sampleSourceSet, sample.name) ?.let { acc.addSample(page, sample.name, it) } ?: acc @@ -51,14 +52,44 @@ internal class DefaultSamplesTransformer(val context: DokkaContext) : PageTransf private fun ContentNode.addSample( contentPage: ContentPage, fqLink: String, - sample: SampleProvider.SampleSnippet, + sample: SampleSnippet, ): ContentNode { val node = contentCode(contentPage.content.sourceSets, contentPage.dri, createSampleBody(sample.imports, sample.body), "kotlin") return dfs(fqLink, node) } - fun createSampleBody(imports: String, body: String) = - """ |$imports + /** + * If both [imports] and [body] are present, it should return + * + * ```kotlin + * import com.example.One + * import com.example.Two + * + * fun main() { + * //sampleStart + * println("Sample function body") + * println("Another line") + * //sampleEnd + * } + * ``` + * + * If [imports] are empty, it should return: + * + * ```kotlin + * fun main() { + * //sampleStart + * println("Sample function body") + * println("Another line") + * //sampleEnd + * } + * ``` + * + * Notice the presence/absence of the new line before the body. + */ + private fun createSampleBody(imports: List<String>, body: String) = + // takeIf {} is needed so that joinToString's postfix is not added for empty lists, + // and trimMargin() then removes the first empty line + """ |${imports.takeIf { it.isNotEmpty() }?.joinToString(separator = "\n", postfix = "\n") { "import $it" } ?: "" } |fun main() { | //sampleStart | $body diff --git a/dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt b/dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt index 1b73ffee..f03969ac 100644 --- a/dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt +++ b/dokka-subprojects/plugin-base/src/test/kotlin/linkableContent/LinkableContentTest.kt @@ -6,6 +6,7 @@ package linkableContent import org.jetbrains.dokka.SourceLinkDefinitionImpl import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest +import org.jetbrains.dokka.base.transformers.pages.DefaultSamplesTransformer import org.jetbrains.dokka.base.transformers.pages.sourcelinks.SourceLinksTransformer import org.jetbrains.dokka.model.WithGenerics import org.jetbrains.dokka.model.dfs @@ -192,10 +193,8 @@ class LinkableContentTest : BaseAbstractTest() { } testFromData(configuration) { - renderingStage = { rootPageNode, _ -> - // TODO [beresnev] :((( -// val newRoot = DefaultSamplesTransformer(dokkaContext).invoke(rootPageNode) - val newRoot = rootPageNode + renderingStage = { rootPageNode, dokkaContext -> + val newRoot = DefaultSamplesTransformer(dokkaContext).invoke(rootPageNode) val moduleChildren = newRoot.children assertEquals(1, moduleChildren.size) val packageChildren = moduleChildren.first().children @@ -212,12 +211,16 @@ class LinkableContentTest : BaseAbstractTest() { .let { it as ContentCodeBlock }.children.single() .let { it as ContentText }.text assertEquals( - """|import p2.${name}Class - |fun main() { - | //sampleStart - | ${name}Class().printWithExclamation("Hi, $name") - | //sampleEnd - |}""".trimMargin(), + """ + |import p2.${name}Class + |import kotlin.collections.List + |import kotlin.collections.Map + | + |fun main() { + | //sampleStart + | ${name}Class().printWithExclamation("Hi, $name") + | //sampleEnd + |}""".trimMargin(), text ) } diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt index 5dc791d7..3ea25268 100644 --- a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jsMain/resources/Samples.kt @@ -5,6 +5,8 @@ package samples import p2.JsClass +import kotlin.collections.List +import kotlin.collections.Map class SamplesJs { diff --git a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt index f32538cc..0a618b2a 100644 --- a/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt +++ b/dokka-subprojects/plugin-base/src/test/resources/linkable/samples/jvmMain/resources/Samples.kt @@ -5,6 +5,8 @@ package samples import p2.JvmClass +import kotlin.collections.List +import kotlin.collections.Map class SamplesJvm { |