diff --git a/docs/images/ksp/ksp-new-module.png b/docs/images/ksp/ksp-new-module.png new file mode 100644 index 00000000000..64596e5dfe4 Binary files /dev/null and b/docs/images/ksp/ksp-new-module.png differ diff --git a/docs/images/ksp/ksp-new-project.png b/docs/images/ksp/ksp-new-project.png new file mode 100644 index 00000000000..2bc57a4c78f Binary files /dev/null and b/docs/images/ksp/ksp-new-project.png differ diff --git a/docs/kr.tree b/docs/kr.tree index a2d111b5963..d8b2363366c 100644 --- a/docs/kr.tree +++ b/docs/kr.tree @@ -341,7 +341,7 @@ - + diff --git a/docs/topics/ksp/ksp-quickstart.md b/docs/topics/ksp/ksp-quickstart.md index 641c38b3396..c7acc2ef304 100644 --- a/docs/topics/ksp/ksp-quickstart.md +++ b/docs/topics/ksp/ksp-quickstart.md @@ -1,81 +1,27 @@ -[//]: # (title: KSP quickstart) +[//]: # (title: Getting started with KSP) +[//]: # (description: Add an annotation processor based on Kotlin Symbol Processing (KSP) to your project, or use the KSP API to create your own.) -For a quick start, you can create your own processor or get a [sample one](https://github.com/google/ksp/tree/main/examples/playground). +In this guide you will learn: -## Add a processor +* How to add KSP-based annotation processors to your project. +* How to create your own annotation processor with the KSP API. +* Where to find the code generated by the processor. -To add a processor, you need to include the KSP Gradle Plugin and add a dependency on the processor: +## Add a KSP-based processor to your project -1. Add the KSP Gradle Plugin `com.google.devtools.ksp` to your `build.gradle(.kts)` file: - - - - - ```kotlin - plugins { - id("com.google.devtools.ksp") version "%kspVersion%" - } - ``` - - - - - ```groovy - plugins { - id 'com.google.devtools.ksp' version '%kspVersion%' - } - ``` - - - - -2. Add a dependency on the processor. -This example uses [Dagger](https://dagger.dev/dev-guide/ksp.html). Replace it with the processor you want to add. - - - - - ```kotlin - dependencies { - implementation("com.google.dagger:dagger-compiler:2.51.1") - ksp("com.google.dagger:dagger-compiler:2.51.1") - } - ``` - - - - - ```groovy - dependencies { - implementation 'com.google.dagger:dagger-compiler:2.51.1' - ksp 'com.google.dagger:dagger-compiler:2.51.1' - } - ``` - - - - -3. Run `./gradlew build`. You can find the generated code in the `build/generated/ksp` directory. - -Here is a full example: +To use an external processor in your project, add KSP to the [`plugins {}` block](https://docs.gradle.org/current/userguide/plugins.html#sec:plugins_block) +in your `build.gradle(.kts)` file. If the processor is only needed in a specific module, add it to that module's +`build.gradle(.kts)` file instead: ```kotlin -plugins { - id("com.google.devtools.ksp") version "%kspVersion%" - kotlin("jvm") -} +// build.gradle.kts -repositories { - mavenCentral() -} - -dependencies { - implementation(kotlin("stdlib-jdk8")) - implementation("com.google.dagger:dagger-compiler:2.51.1") - ksp("com.google.dagger:dagger-compiler:2.51.1") +plugins { + kotlin("jvm") version "%kotlinVersion%" + id("com.google.devtools.ksp") version "%kspVersion%" } ``` @@ -83,210 +29,32 @@ dependencies { ```groovy +// build.gradle + plugins { - id 'com.google.devtools.ksp' version '%kspSupportedKotlinVersion%-%%' id 'org.jetbrains.kotlin.jvm' version '%kotlinVersion%' -} - -repositories { - mavenCentral() -} - -dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib:%kotlinVersion%' - implementation 'com.google.dagger:dagger-compiler:2.51.1' - ksp 'com.google.dagger:dagger-compiler:2.51.1' + id 'com.google.devtools.ksp' version '%kspVersion%' } ``` -## Create a processor of your own - -1. Create an empty gradle project. -2. Specify version `%kspSupportedKotlinVersion%` of the Kotlin plugin in the root project for use in other project modules: - - - - - ```kotlin - plugins { - kotlin("jvm") version "%kspSupportedKotlinVersion%" apply false - } - - buildscript { - dependencies { - classpath(kotlin("gradle-plugin", version = "%kspSupportedKotlinVersion%")) - } - } - ``` - - - - - ```groovy - plugins { - id 'org.jetbrains.kotlin.jvm' version '%kspSupportedKotlinVersion%' apply false - } - - buildscript { - dependencies { - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:%kspSupportedKotlinVersion%' - } - } - ``` - - - - -3. Add a module for hosting the processor. - -4. In the module's build script, apply Kotlin plugin and add the KSP API to the `dependencies` block. - - - - - ```kotlin - plugins { - kotlin("jvm") - } - - repositories { - mavenCentral() - } - - dependencies { - implementation("com.google.devtools.ksp:symbol-processing-api:%kspVersion%") - } - ``` - - - - - ```groovy - plugins { - id 'org.jetbrains.kotlin.jvm' version '%kotlinVersion%' - } - - repositories { - mavenCentral() - } - - dependencies { - implementation 'com.google.devtools.ksp:symbol-processing-api:%kspVersion%' - } - ``` - - - - -5. You'll need to implement [`com.google.devtools.ksp.processing.SymbolProcessor`](https://github.com/google/ksp/tree/main/api/src/main/kotlin/com/google/devtools/ksp/processing/SymbolProcessor.kt) - and [`com.google.devtools.ksp.processing.SymbolProcessorProvider`](https://github.com/google/ksp/tree/main/api/src/main/kotlin/com/google/devtools/ksp/processing/SymbolProcessorProvider.kt). - Your implementation of `SymbolProcessorProvider` will be loaded as a service to instantiate the `SymbolProcessor` you implement. - Note the following: - * Implement [`SymbolProcessorProvider.create()`](https://github.com/google/ksp/blob/master/api/src/main/kotlin/com/google/devtools/ksp/processing/SymbolProcessorProvider.kt) - to create a `SymbolProcessor`. Pass dependencies that your processor needs (such as `CodeGenerator`, processor options) - through the parameters of `SymbolProcessorProvider.create()`. - * Your main logic should be in the [`SymbolProcessor.process()`](https://github.com/google/ksp/blob/master/api/src/main/kotlin/com/google/devtools/ksp/processing/SymbolProcessor.kt) method. - * Use `resolver.getSymbolsWithAnnotation()` to get the symbols you want to process, given the fully-qualified name of - an annotation. - * A common use case for KSP is to implement a customized visitor (interface `com.google.devtools.ksp.symbol.KSVisitor`) - for operating on symbols. A simple template visitor is `com.google.devtools.ksp.symbol.KSDefaultVisitor`. - * For sample implementations of the `SymbolProcessorProvider` and `SymbolProcessor` interfaces, see the following files - in the sample project. - * `src/main/kotlin/BuilderProcessor.kt` - * `src/main/kotlin/TestProcessor.kt` - * After writing your own processor, register your processor provider to the package by including its fully-qualified - name in `src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider`. - -## Use your own processor in a project - -1. Create another module that contains a workload where you want to try out your processor. - - - - - ```kotlin - pluginManagement { - repositories { - gradlePluginPortal() - } - } - ``` - - - - - ```groovy - pluginManagement { - repositories { - gradlePluginPortal() - } - } - ``` - - - - -2. In the module's build script, apply the `com.google.devtools.ksp` plugin with the specified version and - add your processor to the list of dependencies. +> To find the latest version of KSP, check the GitHub [Releases](https://github.com/google/ksp/releases). +> +{style="tip"} - - - - ```kotlin - plugins { - id("com.google.devtools.ksp") version "%kspVersion%" - } - - dependencies { - implementation(kotlin("stdlib-jdk8")) - implementation(project(":test-processor")) - ksp(project(":test-processor")) - } - ``` - - - - - ```groovy - plugins { - id 'com.google.devtools.ksp' version '%kspVersion%' - } - - dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib:%kotlinVersion%' - implementation project(':test-processor') - ksp project(':test-processor') - } - ``` - - - - -3. Run `./gradlew build`. You can find the generated code under - `build/generated/ksp`. - -Here's a sample build script to apply the KSP plugin to a workload: +In the top-level `dependencies {}` block, add the processor you want to use. This example uses +[Moshi](https://github.com/square/moshi?tab=readme-ov-file#codegen), but the approach is the same for other processors: ```kotlin -plugins { - id("com.google.devtools.ksp") version "%kspVersion%" - kotlin("jvm") -} - -repositories { - mavenCentral() -} +// build.gradle.kts dependencies { - implementation(kotlin("stdlib-jdk8")) - implementation(project(":test-processor")) - ksp(project(":test-processor")) + ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2") } ``` @@ -294,137 +62,397 @@ dependencies { ```groovy -plugins { - id 'com.google.devtools.ksp' version '%kspVersion%' - id 'org.jetbrains.kotlin.jvm' version '%kotlinVersion%' -} - -repositories { - mavenCentral() -} +// build.gradle dependencies { - implementation 'org.jetbrains.kotlin:kotlin-stdlib:%kotlinVersion%' - implementation project(':test-processor') - ksp project(':test-processor') + ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.2' } ``` -## Pass options to processors -Processor options in `SymbolProcessorEnvironment.options` are specified in gradle build scripts: +## Create your own processor -```none -ksp { - arg("option1", "value1") - arg("option2", "value2") - ... -} -``` +By following these steps you will create a simple annotation processor that will generate a `helloWorld()` function. +While not very useful in practice, it demonstrates the basics of creating your own processors and annotations. -## Make IDE aware of generated code +### Add KSP to the project -> Generated source files are registered automatically since KSP 1.8.0-1.0.9. -> If you're using KSP 1.0.9 or newer and don't need to make the IDE aware of generated resources, -> feel free to skip this section. -> -{style="note"} +Create a new Kotlin project and add the KSP plugin: -By default, IntelliJ IDEA or other IDEs don't know about the generated code. So it will mark references to generated -symbols unresolvable. To make an IDE be able to reason about the generated symbols, mark the -following paths as generated source roots: +1. In IntelliJ IDEA, select **File** | **New** | **Project**. +2. In the list on the left, choose **Kotlin**. +3. Choose **Gradle** as the build system and click **Create**. -```text -build/generated/ksp/main/kotlin/ -build/generated/ksp/main/java/ -``` + ![Creating a new project](ksp-new-project.png){width=700} -If your IDE supports resource directories, also mark the following one: +4. Add the KSP plugin to the `build.gradle(.kts)` file: -```text -build/generated/ksp/main/resources/ -``` + + + + ```kotlin + // build.gradle.kts + + plugins { + kotlin("jvm") version "%kotlinVersion%" + id("com.google.devtools.ksp") version "%kspVersion%" apply false + } + ``` + + + + + ```groovy + // build.gradle + + plugins { + id 'org.jetbrains.kotlin.jvm' version '%kotlinVersion%' + id 'com.google.devtools.ksp' version '%kspVersion%' apply false + } + ``` + + + -It may also be necessary to configure these directories in your KSP consumer module's build script: +### Create an annotation - - +Create a new module at the root of the project and declare an annotation: -```kotlin -kotlin { - sourceSets.main { - kotlin.srcDir("build/generated/ksp/main/kotlin") +1. Select **File** | **New** | **Module**. +2. In the list on the left, select **Kotlin**. +3. Specify the following fields and click **create**: + + * **Name**: annotations + * **Build system**: Gradle + + ![Creating a new module](ksp-new-module.png){width=700} + +4. In the module, create a `HelloWorldAnnotation.kt` file and declare an annotation called `HelloWorldAnnotation`: + + ```kotlin + // annotations/src/main/kotlin/com/example/annotations/HelloWorldAnnotation.kt + + package com.example.annotations + + annotation class HelloWorldAnnotation + ``` + +### Create and register a processor + +1. Create another module at the root of the project called **processor**. + +2. Add the KSP API and the annotation you declared as dependencies in the module's `build.gradle(.kts)` file: + + + + + ```kotlin + // processor/build.gradle.kts + + plugins { + kotlin("jvm") } - sourceSets.test { - kotlin.srcDir("build/generated/ksp/test/kotlin") + + dependencies { + implementation(project(":annotations")) + implementation("com.google.devtools.ksp:symbol-processing-api:2.3.6") } -} -``` + ``` + + + + + ```groovy + // processor/build.gradle + + plugins { + id 'org.jetbrains.kotlin.jvm' + } + + dependencies { + implementation project ':annotations' + implementation 'com.google.devtools.ksp:symbol-processing-api:2.3.6' + } + ``` - - + + + +3. In the processor module, create a new `HelloWorldProcessor.kt` file and add the following code: + + ```kotlin + // processor/src/main/kotlin/HelloWorldProcessor.kt + + class HelloWorldProcessor(val codeGenerator: CodeGenerator) : SymbolProcessor { + // 1️⃣ process() function + override fun process(resolver: Resolver): List { + resolver + .getSymbolsWithAnnotation("com.example.annotations.HelloWorldAnnotation") + .filter { it.validate() } + .filterIsInstance() + .forEach { it.accept(HelloWorldVisitor(), Unit) } + + return emptyList() + } + + // 2️⃣ Visitor + inner class HelloWorldVisitor : KSVisitorVoid() { + override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) { + createNewFileFrom(function).use { file -> + file.write( + """ + fun helloWorld(): Unit { + println("Hello world from function generated by KSP") + } + """.trimIndent() + ) + } + } + } + + // 3️⃣ createNewFileFrom() function + private fun createNewFileFrom(function: KSFunctionDeclaration): OutputStream { + return codeGenerator.createNewFile( + dependencies = createDependencyOn(function), + packageName = "", + fileName = "GeneratedHelloWorld" + ) + } + + // 3️⃣ createDependencyOn() function + private fun createDependencyOn(function: KSFunctionDeclaration): Dependencies { + return Dependencies(aggregating = false, function.containingFile!!) + } + } + + // Utility function for writing string to OutputStream + fun OutputStream.write(string: String): Unit { + this.write(string.toByteArray()) + } + ``` -```groovy -kotlin { - sourceSets { - main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin' - test.kotlin.srcDirs += 'build/generated/ksp/test/kotlin' + Add the imports that are suggested by the IDE. Make sure to import the `Resolver` and `Dependencies` classes + from `com.google.devtools.ksp.processing`. Alternatively, copy these lines at the top of `HelloWorldProcessor.kt`: + + ```kotlin + // processor/src/main/kotlin/HelloWorldProcessor.kt + + import com.google.devtools.ksp.processing.CodeGenerator + import com.google.devtools.ksp.processing.Dependencies + import com.google.devtools.ksp.processing.Resolver + import com.google.devtools.ksp.processing.SymbolProcessor + import com.google.devtools.ksp.symbol.KSAnnotated + import com.google.devtools.ksp.symbol.KSFunctionDeclaration + import com.google.devtools.ksp.symbol.KSVisitorVoid + import com.google.devtools.ksp.validate + import java.io.OutputStream + ``` + {collapsible="true" collapsed-title="Import statements"} + + + Let's go through the code: + + * 1️⃣ The `process()` function contains the main logic of the processor. It gets all symbols + annotated with `HelloWorldAnnotation` and calls `HelloWorldVisitor` for each one. + + The `process()` function returns a list of unprocessed symbols to process in a later round. In this example, + it safely returns `emptyList()`. For more information, see [Multiple round processing](ksp-multi-round.md). + + * 2️⃣ Processors traverse KSP's view of the Kotlin abstract syntax tree (AST) using visitors. Inside the `HelloWorldPocessor` + class, the `HelloWorldVisitor` class is the visitor. Since the `HelloWorldAnnotation` is only used on a function, + only `visitFunctionDeclaration()` is overridden. + + > `KSVisitorVoid` is one of the visitor classes provided by KSP that you can override and adapt. + > You can also create your own visitor by implementing the [`KSVisitor` interface](https://github.com/google/ksp/blob/main/api/src/main/kotlin/com/google/devtools/ksp/symbol/KSVisitor.kt). + > + {style="tip"} + + * 3️⃣ `createNewFileFrom()` creates the file where KSP generates code. `createDependencyOn()` makes the output + file depend on the source file where the annotation is used. + + > To learn more about how KSP creates and manages files, visit the source code for the + > [`CodeGenerator` interface](https://github.com/google/ksp/blob/main/api/src/main/kotlin/com/google/devtools/ksp/processing/CodeGenerator.kt) + > + {style="tip"} + +4. Create a `HelloWorldProcessorProvider.kt` file. In it, declare a `HelloWorldProcessorProvider` class which inherits + from `SymbolProcessorProvider`: + + ```kotlin + // processor/src/main/kotlin/HelloWorldProcessorProvider.kt + + import com.google.devtools.ksp.processing.SymbolProcessor + import com.google.devtools.ksp.processing.SymbolProcessorEnvironment + import com.google.devtools.ksp.processing.SymbolProcessorProvider + + class HelloWorldProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return HelloWorldProcessor(environment.codeGenerator) + } } -} -``` + ``` - - +5. Register the processor provider. In the `resources/META-INF/services` directory, create a +`com.google.devtools.ksp.processing.SymbolProcessorProvider` file and add the provider's fully qualified name: -If you are using IntelliJ IDEA and KSP in a Gradle plugin then the above snippet will give the following warning: -```text -Execution optimizations have been disabled for task ':publishPluginJar' to ensure correctness due to the following reasons: -Gradle detected a problem with the following location: '../build/generated/ksp/main/kotlin'. -Reason: Task ':publishPluginJar' uses this output of task ':kspKotlin' without declaring an explicit or implicit dependency. -``` + ```text + ## processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider + + HelloWorldProcessorProvider + ``` -In this case, use the following script instead: +### Use your processor - - +Now you are ready to test your processor. Follow these steps to create a client module and have your processor +generate code based on an annotated element: -```kotlin -plugins { - // ... - idea -} +1. Create a module called `app` at the root of your project. +2. In the module's `build.gradle(.kts)` file: + + * Add the KSP plugin to the `plugins {}` block. + * Add your processor and annotation to the `dependencies {}` block. + + For example: + + + -idea { - module { - // Not using += due to https://github.com/gradle/gradle/issues/8749 - sourceDirs = sourceDirs + file("build/generated/ksp/main/kotlin") // or tasks["kspKotlin"].destination - testSourceDirs = testSourceDirs + file("build/generated/ksp/test/kotlin") - generatedSourceDirs = generatedSourceDirs + file("build/generated/ksp/main/kotlin") + file("build/generated/ksp/test/kotlin") + ```kotlin + // app/build.gradle.kts + + plugins { + kotlin("jvm") + id("com.google.devtools.ksp") } -} -``` - - + dependencies { + implementation(project(":annotations")) + ksp(project(":processor")) + } + ``` -```groovy -plugins { - // ... - id 'idea' -} + + + + ```groovy + // app/build.gradle + + plugins { + id 'com.google.devtools.ksp' + } + + dependencies { + implementation project (':annotations') + ksp project (':processor') + } + ``` + + + + +3. In the project-level `settings.gradle(.kts)` file, ensure that all the submodules were automatically included: + + + + + ```kotlin + // settings.gradle.kts + + include("annotations") + include("app") + include("processor") + ``` + + + + + ```groovy + // settings.gradle + + include 'processor' + include 'annotations' + include 'app' + ``` + + + -idea { - module { - // Not using += due to https://github.com/gradle/gradle/issues/8749 - sourceDirs = sourceDirs + file('build/generated/ksp/main/kotlin') // or tasks["kspKotlin"].destination - testSourceDirs = testSourceDirs + file('build/generated/ksp/test/kotlin') - generatedSourceDirs = generatedSourceDirs + file('build/generated/ksp/main/kotlin') + file('build/generated/ksp/test/kotlin') +4. In the `app` module, create a `Main.kt` file and add the following code: + + ```kotlin + // app/src/main/kotlin/Main.kt + + import com.example.annotations.HelloWorldAnnotation + + @HelloWorldAnnotation + fun main() { + helloWorld() } -} + ``` + + > The `main()` function calls `helloWorld()`, even though this function does not exist yet. Your IDE highlights + > `helloWorld()` as an undefined reference. This is expected: KSP generates the `helloWorld()` function when you + > build and run the project. + > + {style="note"} + +5. Run the program. You see the output of the `helloWorld()` function in your console: + + ```text + Hello world from function generated by KSP + ``` + + KSP generates code in the `GeneratedHelloWorld.kt` file: + + ```text + app/build/generated/ksp/main/kotlin/GeneratedHelloWorld.kt + ``` + +### Explore the project structure + +Your project's final file structure should look like this: + +```text +. +├── app +│ ├── build.gradle.kts +│ └── src +│ └── main +│ └── kotlin +│ └── Main.kt +├── annotations +│ ├── build.gradle.kts +│ └── src +│ └── main +│ └── kotlin +| └── com +| └── example +| └── annotations +| └── HelloWorldAnnotation.kt +├── processor +│ ├── build.gradle.kts +│ └── src +│ └── main +│ ├── kotlin +│ │ ├── HelloWorldProcessor.kt +│ │ └── HelloWorldProcessorProvider.kt +│ └── resources/META-INF/services +| └── com.google.devtools.ksp.processing.SymbolProcessorProvider +├── build.gradle.kts +└── settings.gradle.kts + ``` +{collapsible="true" collapsed-title="Project structure"} - - +> You might have additional files and directories. +> +{style="tip"} + +## What's next? + +* Explore the full code for this example in [the KSP repository](https://github.com/google/ksp/tree/main/examples/hello-world). +* Find more complex, real-world examples in [the KSP repository](https://github.com/google/ksp/blob/main/examples/playground/test-processor/src/main/kotlin/BuilderProcessor.kt). +* Browse the list of [KSP supported libraries](ksp-overview.md#supported-libraries).