Skip to content

Sample plugin tutorial

We'll go over creating a simple plugin that covers a very common use case: generate documentation for everything except for members annotated with a custom @Internal annotation - they should be hidden.

The plugin will be tested with the following code:

package org.jetbrains.dokka.internal.test

annotation class Internal

fun shouldBeVisible() {}

@Internal
fun shouldBeExcludedFromDocumentation() {}

Expected behavior: function shouldBeExcludedFromDocumentation should not be visible in generated documentation.

Full source code of this tutorial can be found in Dokka's examples under hide-internal-api.

Preparing the project

We'll begin by using Dokka plugin template. Press the Use this template button and open this project in IntelliJ IDEA.

First, let's rename the pre-made template package and MyAwesomeDokkaPlugin class to something of our own.

For instance, package can be renamed to org.example.dokka.plugin and the class to HideInternalApiPlugin:

package org.example.dokka.plugin

import org.jetbrains.dokka.plugability.DokkaPlugin

class HideInternalApiPlugin : DokkaPlugin() {

}

After you do that, make sure to update the path to this class in resources/META-INF/services/org.jetbrains.dokka.plugability.DokkaPlugin:

org.example.dokka.plugin.HideInternalApiPlugin

At this point you can also change project name in settings.gradle.kts (to hide-internal-api in our case) and groupId in build.gradle.kts.

Extending Dokka

After preparing the project we can begin extending Dokka with our own extension.

Having read through Core extensions, it's clear that we need a PreMergeDocumentableTransformer extension in order to filter out undesired documentables.

Moreover, the article mentioned a convenient abstract transformer SuppressedByConditionDocumentableFilterTransformer which is perfect for our use case, so we can try to implement it.

Create a new class, place it next to your plugin and implement the abstract method. You should end up with this:

package org.example.dokka.plugin

import org.jetbrains.dokka.base.transformers.documentables.SuppressedByConditionDocumentableFilterTransformer
import org.jetbrains.dokka.model.Documentable
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.DokkaPlugin

class HideInternalApiPlugin : DokkaPlugin() {}

class HideInternalApiTransformer(context: DokkaContext) : SuppressedByConditionDocumentableFilterTransformer(context) {

    override fun shouldBeSuppressed(d: Documentable): Boolean {
        return false
    }
}

Now we somehow need to find all annotations applied to d: Documentable and see if our @Internal annotation is present. However, it's not very clear how to do that. What usually helps is stopping in debugger and having a look at what fields and values a given Documentable has.

To do that, we'll need to register our extension point first, then we can publish our plugin and set the breakpoint.

Having read through Introduction to extensions, we now know how to register our extensions:

class HideInternalApiPlugin : DokkaPlugin() {
    val myFilterExtension by extending {
        plugin<DokkaBase>().preMergeDocumentableTransformer providing ::HideInternalApiTransformer
    }
}

At this point we're ready to debug our plugin locally, it should already work, but do nothing.

Debugging

Please read through Debugging Dokka, it goes over the same steps in more detail and with examples. Below you will find rough instructions.

First, let's begin by publishing our plugin to mavenLocal().

./gradlew publishToMavenLocal

This will publish your plugin under the groupId, artifactId and version that you've specified in your build.gradle.kts. In our case it's org.example:hide-internal-api:1.0-SNAPSHOT.

Open a debug project of your choosing that has Dokka configured, and add our plugin to dependencies:

dependencies {
    dokkaPlugin("org.example:hide-internal-api:1.0-SNAPSHOT")
}

Next, in that project let's run dokkaHtml with debug enabled:

./gradlew clean dokkaHtml -Dorg.gradle.debug=true --no-daemon

Switch to the plugin project, set a breakpoint inside shouldBeSuppressed and run jvm remote debug.

If you've done everything correctly, it should stop in debugger and you should be able to observe the values contained inside d: Documentable.

Implementing plugin logic

Now that we've stopped at our breakpoint, let's skip until we see shouldBeExcludedFromDocumentation function in the place of d: Documentable (observe the changing name property).

Looking at what's inside the object, you might notice it has 3 values in extra, one of which is Annotations. Sounds like something we need!

Having poked around, we come up with the following monstrosity of a code for determining if a given documentable has @Internal annotation (it can of course be refactored.. later):

override fun shouldBeSuppressed(d: Documentable): Boolean {

    val annotations: List<Annotations.Annotation> =
        (d as? WithExtraProperties<*>)
            ?.extra
            ?.allOfType<Annotations>()
            ?.flatMap { it.directAnnotations.values.flatten() }
            ?: emptyList()

    return annotations.any { isInternalAnnotation(it) }
}

private fun isInternalAnnotation(annotation: Annotations.Annotation): Boolean {
   return annotation.dri.packageName == "org.jetbrains.dokka.internal.test"
           && annotation.dri.classNames == "Internal"
}

Seems like we're done with writing our plugin and can begin testing it manually.

Manual testing

At this point, the implementation of your plugin should look roughly like this:

package org.example.dokka.plugin

import org.jetbrains.dokka.base.DokkaBase
import org.jetbrains.dokka.base.transformers.documentables.SuppressedByConditionDocumentableFilterTransformer
import org.jetbrains.dokka.model.Annotations
import org.jetbrains.dokka.model.Documentable
import org.jetbrains.dokka.model.properties.WithExtraProperties
import org.jetbrains.dokka.plugability.DokkaContext
import org.jetbrains.dokka.plugability.DokkaPlugin

class HideInternalApiPlugin : DokkaPlugin() {
    val myFilterExtension by extending {
        plugin<DokkaBase>().preMergeDocumentableTransformer providing ::HideInternalApiTransformer
    }
}

class HideInternalApiTransformer(context: DokkaContext) : SuppressedByConditionDocumentableFilterTransformer(context) {

    override fun shouldBeSuppressed(d: Documentable): Boolean {
        val annotations: List<Annotations.Annotation> =
            (d as? WithExtraProperties<*>)
                ?.extra
                ?.allOfType<Annotations>()
                ?.flatMap { it.directAnnotations.values.flatten() }
                ?: emptyList()

        return annotations.any { isInternalAnnotation(it) }
    }

    private fun isInternalAnnotation(annotation: Annotations.Annotation): Boolean {
        return annotation.dri.packageName == "org.jetbrains.dokka.internal.test"
                && annotation.dri.classNames == "Internal"
    }
}

Bump plugin version in gradle.build.kts, publish it to maven local, open the debug project and run dokkaHtml (without debug this time). It should work, you should not be able to see shouldBeExcludedFromDocumentation function in generated documentation.

Manual testing is cool and all, but wouldn't it be better if we could somehow write unit tests for it? Indeed!

Unit testing

You might've noticed that plugin template comes with a pre-made test class. Feel free to move it to another package and rename it.

We are mostly interested in a single test case - functions annotated with @Internal should be hidden, while all other public functions should be visible.

Plugin API comes with a set of convenient test utilities that are used to test Dokka itself, so it covers a wide range of use cases. When in doubt, see Dokka's tests for reference.

Below you will find a complete unit test that passes, and the main takeaways below that.

package org.example.dokka.plugin

import org.jetbrains.dokka.base.testApi.testRunner.BaseAbstractTest
import org.junit.Test
import kotlin.test.assertEquals

class HideInternalApiPluginTest : BaseAbstractTest() {

    @Test
    fun `should hide annotated functions`() {
        val configuration = dokkaConfiguration {
            sourceSets {
                sourceSet {
                    sourceRoots = listOf("src/main/kotlin/basic/Test.kt")
                }
            }
        }
        val hideInternalPlugin = HideInternalApiPlugin()

        testInline(
            """
            |/src/main/kotlin/basic/Test.kt
            |package org.jetbrains.dokka.internal.test
            |
            |annotation class Internal
            |
            |fun shouldBeVisible() {}
            |
            |@Internal
            |fun shouldBeExcludedFromDocumentation() {}
        """.trimMargin(),
            configuration = configuration,
            pluginOverrides = listOf(hideInternalPlugin)
        ) {
            preMergeDocumentablesTransformationStage = { modules ->
                val testModule = modules.single { it.name == "root" }
                val testPackage = testModule.packages.single { it.name == "org.jetbrains.dokka.internal.test" }

                val packageFunctions = testPackage.functions
                assertEquals(1, packageFunctions.size)
                assertEquals("shouldBeVisible", packageFunctions[0].name)
            }
        }
    }
}

Note that the package of the tested code (inside testInline function) is the same as the package that we have hardcoded in our plugin. Make sure to change that to your own if you are following along, otherwise it will fail.

Things to note and remember:

  1. Your test class should extend BaseAbstractTest, which contains base utility methods for testing.
  2. You can configure Dokka to your liking, enable some specific settings, configure source sets, etc. All done via dokkaConfiguration DSL.
  3. testInline function is the main entry point for unit tests
  4. You can pass plugins to be used in a test, notice pluginOverrides parameter
  5. You can write asserts for different stages of generating documentation, the main ones being Documentables model generation, Pages generation and Output generation. Since we implemented our plugin to work during PreMergeDocumentableTransformer stage, we can test it on the same level (that is preMergeDocumentablesTransformationStage).
  6. You will need to write asserts using the model of whatever stage you choose. For Documentable transformation stage it's Documentable, for Page generation stage you would have Page model, and for Output you can have .html files that you will need to parse with JSoup (there are also utilities for that).
Back to top