aboutsummaryrefslogtreecommitdiff
path: root/mkdocs/src/doc/docs/developer_guide/plugin-development/sample-plugin-tutorial.md
blob: fdea0207e43c52fc49ed0586f5a9cab7966bf717 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# 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:

```kotlin
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](https://github.com/Kotlin/dokka/examples/plugin/hide-internal-api).

## Preparing the project

We'll begin by using [Dokka plugin template](https://github.com/Kotlin/dokka-plugin-template). Press the 
`Use this template` button and 
[open this project in IntelliJ IDEA](https://www.jetbrains.com/idea/guide/tutorials/working-with-gradle/opening-a-gradle-project/).

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`:

```kotlin
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`:
```kotlin
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](../architecture/extension_points/core_extensions.md), 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:

```kotlin
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](../architecture/extension_points/introduction.md), we now know
how to register our extensions:

```kotlin
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](../workflow.md#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()`. 

```bash
./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:

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

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

```bash
./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):

```kotlin
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:

```kotlin
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.

```kotlin
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](https://kotlinlang.org/docs/multiplatform-discover-project.html#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).