diff options
author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-01-10 13:14:43 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-10 13:14:43 +0100 |
commit | 7544a215fb580ae0c47d1f397334f150d1a1ec65 (patch) | |
tree | a30aa62c827e3ba88a498a7406ac57fa7334b270 /mkdocs/src/doc/docs/developer_guide/plugin-development/sample-plugin-tutorial.md | |
parent | 2161c397e1b1aadcf3d39c8518258e9bdb2b431a (diff) | |
download | dokka-7544a215fb580ae0c47d1f397334f150d1a1ec65.tar.gz dokka-7544a215fb580ae0c47d1f397334f150d1a1ec65.tar.bz2 dokka-7544a215fb580ae0c47d1f397334f150d1a1ec65.zip |
Revise documentation (#2728)
Co-authored-by: Sarah Haggarty <sarahhaggarty@users.noreply.github.com>
Diffstat (limited to 'mkdocs/src/doc/docs/developer_guide/plugin-development/sample-plugin-tutorial.md')
-rw-r--r-- | mkdocs/src/doc/docs/developer_guide/plugin-development/sample-plugin-tutorial.md | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/mkdocs/src/doc/docs/developer_guide/plugin-development/sample-plugin-tutorial.md b/mkdocs/src/doc/docs/developer_guide/plugin-development/sample-plugin-tutorial.md new file mode 100644 index 00000000..fdea0207 --- /dev/null +++ b/mkdocs/src/doc/docs/developer_guide/plugin-development/sample-plugin-tutorial.md @@ -0,0 +1,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). |