From 7b18f65badd21e9a92ae4128d60e27fa08c12963 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Tue, 7 May 2024 08:37:23 +0300 Subject: [PATCH] Provide a way to manually create SampleAnalysisEnvironment (#3589) * introduce DelicateDokkaApi annotation and mark new API with it --------- Co-authored-by: IgnatBeresnev --- .../api/analysis-kotlin-api.api | 3 +- .../sample/SampleAnalysisEnvironment.kt | 3 +- .../SampleAnalysisEnvironmentCreator.kt | 19 ++- .../test/sample/SampleAnalysisTest.kt | 115 ++++++++++++++++++ .../DescriptorSampleAnalysisEnvironment.kt | 30 +++-- .../SymbolSampleAnalysisEnvironment.kt | 30 +++-- dokka-subprojects/core/api/dokka-core.api | 3 + .../org/jetbrains/dokka/DelicateDokkaApi.kt | 20 +++ 8 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/DelicateDokkaApi.kt 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 75cadd1b66..46a2ddd0d3 100644 --- a/dokka-subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api +++ b/dokka-subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api @@ -78,11 +78,12 @@ public abstract interface class org/jetbrains/dokka/analysis/kotlin/sample/Funct public abstract fun rewrite (Ljava/util/List;Ljava/util/List;)Ljava/lang/String; } -public abstract interface class org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironment { +public abstract interface class org/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironment : java/io/Closeable { 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/sample/SampleAnalysisEnvironmentCreator { + public abstract fun create ()Lorg/jetbrains/dokka/analysis/kotlin/sample/SampleAnalysisEnvironment; public abstract fun use (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; } 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 index 820de136ea..91ce1a4488 100644 --- 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 @@ -5,6 +5,7 @@ package org.jetbrains.dokka.analysis.kotlin.sample import org.jetbrains.dokka.DokkaConfiguration +import java.io.Closeable /** * Fully-configured and ready-to-use sample analysis environment. @@ -17,7 +18,7 @@ import org.jetbrains.dokka.DokkaConfiguration * 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 { +public interface SampleAnalysisEnvironment : Closeable { /** * Resolves a Kotlin sample function by its fully qualified name, and returns its import statements and body. 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 index 1d019dc07a..43f3d92738 100644 --- 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 @@ -4,6 +4,7 @@ package org.jetbrains.dokka.analysis.kotlin.sample +import org.jetbrains.dokka.DelicateDokkaApi import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin /** @@ -16,7 +17,7 @@ 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 + * Configuring a 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. @@ -35,4 +36,20 @@ public interface SampleAnalysisEnvironmentCreator { * ``` */ public fun use(block: SampleAnalysisEnvironment.() -> T): T + + /** + * Creates a new instance of [SampleAnalysisEnvironment]. + * + * **WARNING**: This function offers a considerable amount of freedom and with it, + * the potential to misuse the API. + * A [SampleAnalysisEnvironment] once created needs to be manually closed + * otherwise it could lead to memory leaks, concurrency issues or other unexpected problems. + * + * Therefore, it's safest to use it through the [SampleAnalysisEnvironmentCreator.use] + * as it provides a controlled environment where everything is taken care of automatically. + * + * @return a new instance of [SampleAnalysisEnvironment] + */ + @DelicateDokkaApi + public fun create(): SampleAnalysisEnvironment } 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 fcb66e74c1..fc3b03ac19 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,6 +4,7 @@ package org.jetbrains.dokka.analysis.test.sample +import org.jetbrains.dokka.DelicateDokkaApi import org.jetbrains.dokka.analysis.kotlin.sample.SampleSnippet import org.jetbrains.dokka.analysis.test.api.kotlinJvmTestProject import org.jetbrains.dokka.analysis.test.api.mixedJvmTestProject @@ -14,6 +15,120 @@ import kotlin.test.* class SampleAnalysisTest { + @OptIn(DelicateDokkaApi::class) + @Test + fun `should resolve a valid sample if the environment is created manually`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + samples = setOf("/samples/collections.kt") + } + } + 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 sampleAnalysisEnvironment = sampleAnalysisEnvironmentCreator.create() + try { + val sample = sampleAnalysisEnvironment.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 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) + } finally { + sampleAnalysisEnvironment.close() + } + } + } + + @OptIn(DelicateDokkaApi::class) + @Test + fun `should resolve the same sample by multiple environments`() { + val testProject = kotlinJvmTestProject { + dokkaConfiguration { + kotlinSourceSet { + samples = setOf("/samples/collections.kt") + } + } + 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 sampleAnalysisEnvironment1 = sampleAnalysisEnvironmentCreator.create() + val sampleAnalysisEnvironment2 = sampleAnalysisEnvironmentCreator.create() + try { + val sample1 = sampleAnalysisEnvironment1.resolveSample( + sourceSet = context.singleSourceSet(), + fullyQualifiedLink = "org.jetbrains.dokka.sample.collections.specificPositionOperations" + ) + val sample2 = sampleAnalysisEnvironment2.resolveSample( + sourceSet = context.singleSourceSet(), + fullyQualifiedLink = "org.jetbrains.dokka.sample.collections.specificPositionOperations" + ) + assertNotNull(sample1) + assertNotNull(sample2) + assertEquals(sample1, sample2) + } finally { + sampleAnalysisEnvironment1.close() + sampleAnalysisEnvironment2.close() + } + } + } + @Test fun `should resolve a valid sample if set via the samples option`() { val testProject = kotlinJvmTestProject { 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 index ab8d92fcc8..6f5a065cda 100644 --- 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 @@ -10,6 +10,7 @@ import com.intellij.psi.PsiElementVisitor import com.intellij.psi.impl.source.tree.LeafPsiElement import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DelicateDokkaApi import org.jetbrains.dokka.DokkaConfiguration import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.descriptors.compiler.CompilerDescriptorAnalysisPlugin @@ -49,21 +50,22 @@ internal class DescriptorSampleAnalysisEnvironmentCreator( // 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( + create().use(block) + } + } + + @OptIn(DokkaPluginApiPreview::class, DelicateDokkaApi::class) + override fun create(): SampleAnalysisEnvironment { + return DescriptorSampleAnalysisEnvironment( + kdocFinder = descriptorAnalysisPlugin.querySingle { kdocFinder }, + kotlinAnalysis = SamplesKotlinAnalysis( sourceSets = context.configuration.sourceSets, context = context, projectKotlinAnalysis = descriptorAnalysisPlugin.querySingle { kotlinAnalysis } - ).use { kotlinAnalysis -> - val sampleAnalysis = DescriptorSampleAnalysisEnvironment( - kdocFinder = descriptorAnalysisPlugin.querySingle { kdocFinder }, - kotlinAnalysis = kotlinAnalysis, - sampleRewriter = sampleRewriter, - dokkaLogger = context.logger - ) - block(sampleAnalysis) - } - } + ), + sampleRewriter = sampleRewriter, + dokkaLogger = context.logger + ) } } @@ -221,6 +223,10 @@ internal class DescriptorSampleAnalysisEnvironment( } return textBuilder.toString() } + + override fun close() { + kotlinAnalysis.close() + } } private class SampleBuilder( 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 index 31fde08604..2c9457757f 100644 --- 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 @@ -10,6 +10,7 @@ import com.intellij.psi.PsiElementVisitor import com.intellij.psi.impl.source.tree.LeafPsiElement import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import org.jetbrains.dokka.DelicateDokkaApi import org.jetbrains.dokka.DokkaConfiguration.DokkaSourceSet import org.jetbrains.dokka.analysis.kotlin.KotlinAnalysisPlugin import org.jetbrains.dokka.analysis.kotlin.sample.SampleAnalysisEnvironment @@ -44,22 +45,23 @@ internal class SymbolSampleAnalysisEnvironmentCreator( rewriters.singleOrNull() } - override fun use(block: SampleAnalysisEnvironment.() -> T): T { return runBlocking(Dispatchers.Default) { - SamplesKotlinAnalysis( + create().use(block) + } + } + + @OptIn(DelicateDokkaApi::class) + override fun create(): SampleAnalysisEnvironment { + return SymbolSampleAnalysisEnvironment( + samplesKotlinAnalysis = SamplesKotlinAnalysis( sourceSets = context.configuration.sourceSets, context = context - ).use { samplesKotlinAnalysis -> - val sampleAnalysisEnvironment = SymbolSampleAnalysisEnvironment( - samplesKotlinAnalysis = samplesKotlinAnalysis, - projectKotlinAnalysis = projectKotlinAnalysis, - sampleRewriter = sampleRewriter, - dokkaLogger = context.logger - ) - block(sampleAnalysisEnvironment) - } - } + ), + projectKotlinAnalysis = projectKotlinAnalysis, + sampleRewriter = sampleRewriter, + dokkaLogger = context.logger + ) } } @@ -174,6 +176,10 @@ private class SymbolSampleAnalysisEnvironment( } return textBuilder.toString() } + + override fun close() { + samplesKotlinAnalysis.close() + } } private class SampleBuilder( diff --git a/dokka-subprojects/core/api/dokka-core.api b/dokka-subprojects/core/api/dokka-core.api index d286915f4a..9468661de7 100644 --- a/dokka-subprojects/core/api/dokka-core.api +++ b/dokka-subprojects/core/api/dokka-core.api @@ -38,6 +38,9 @@ public final class org/jetbrains/dokka/DefaultExternalLinksKt { public static final fun kotlinStdlib (Lorg/jetbrains/dokka/DokkaConfiguration$ExternalDocumentationLink$Companion;)Lorg/jetbrains/dokka/ExternalDocumentationLinkImpl; } +public abstract interface annotation class org/jetbrains/dokka/DelicateDokkaApi : java/lang/annotation/Annotation { +} + public abstract interface class org/jetbrains/dokka/DokkaBootstrap { public abstract fun configure (Ljava/lang/String;Ljava/util/function/BiConsumer;)V public abstract fun generate ()V diff --git a/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/DelicateDokkaApi.kt b/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/DelicateDokkaApi.kt new file mode 100644 index 0000000000..150d7fd8e9 --- /dev/null +++ b/dokka-subprojects/core/src/main/kotlin/org/jetbrains/dokka/DelicateDokkaApi.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package org.jetbrains.dokka + +/** + * Marks declarations in the Dokka that are **delicate** — + * they have limited use-case and shall be used with care in general code. + * Any use of a delicate declaration has to be carefully reviewed to make sure it is + * properly used and does not create problems like memory and resource leaks. + * Carefully read documentation of any declaration marked as `DelicateDokkaApi`. + */ +@RequiresOptIn( + level = RequiresOptIn.Level.WARNING, + message = "This is a delicate API and its use requires care." + + " Make sure you fully read and understand documentation of the declaration that is marked as a delicate API." +) +@Retention(AnnotationRetention.BINARY) +public annotation class DelicateDokkaApi