Skip to content

Commit

Permalink
Stabilize Sample analysis API
Browse files Browse the repository at this point in the history
  • Loading branch information
IgnatBeresnev committed Oct 12, 2023
1 parent b6a6ca8 commit bf38991
Show file tree
Hide file tree
Showing 28 changed files with 1,182 additions and 388 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ->
Expand All @@ -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
Expand All @@ -51,14 +52,14 @@ 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
private fun createSampleBody(imports: List<String>, body: String) =
""" |${imports.takeIf { it.isNotEmpty() }?.joinToString { "import $it\n" } ?: ""}
|fun main() {
| //sampleStart
| $body
Expand Down
21 changes: 11 additions & 10 deletions plugins/base/src/test/kotlin/linkableContent/LinkableContentTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -193,10 +194,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
Expand All @@ -213,12 +212,14 @@ 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
|
|fun main() {
| //sampleStart
| ${name}Class().printWithExclamation("Hi, $name")
| //sampleEnd
|}""".trimMargin(),
text
)
}
Expand Down
25 changes: 14 additions & 11 deletions subprojects/analysis-kotlin-api/api/analysis-kotlin-api.api
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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?
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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 statements 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')"
}
}
Loading

0 comments on commit bf38991

Please sign in to comment.