Skip to content

Latest commit

 

History

History
458 lines (338 loc) · 13.9 KB

File metadata and controls

458 lines (338 loc) · 13.9 KB

//: # (description: Add an annotation processor based on Kotlin Symbol Processing (KSP) to your project, or use the KSP API to create your own.)

In this guide you will learn:

  • 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.

Add a KSP-based processor to your project

To use an external processor in your project, add KSP to the 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:

// build.gradle.kts

plugins {  
    kotlin("jvm") version "%kotlinVersion%"  
    id("com.google.devtools.ksp") version "%kspVersion%"
}
// build.gradle

plugins {
    id 'org.jetbrains.kotlin.jvm' version '%kotlinVersion%'
    id 'com.google.devtools.ksp' version '%kspVersion%'
}

To find the latest version of KSP, check the GitHub Releases.

{style="tip"}

In the top-level dependencies {} block, add the processor you want to use. This example uses Moshi, but the approach is the same for other processors:

// build.gradle.kts

dependencies {
    ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
// build.gradle

dependencies {
    ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.2'
}

Create your own processor

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.

Add KSP to the project

Create a new Kotlin project and add the KSP plugin:

  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.

    Creating a new project{width=700}

  4. Add the KSP plugin to the build.gradle(.kts) file:

    // build.gradle.kts
    
    plugins { 
        kotlin("jvm") version "%kotlinVersion%"
        id("com.google.devtools.ksp") version "%kspVersion%" apply false
    }
    // build.gradle
    
    plugins {
        id 'org.jetbrains.kotlin.jvm' version '%kotlinVersion%'
        id 'com.google.devtools.ksp' version '%kspVersion%' apply false
    }

Create an annotation

Create a new module at the root of the project and declare an annotation:

  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{width=700}

  4. In the module, create a HelloWorldAnnotation.kt file and declare an annotation called HelloWorldAnnotation:

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

    // processor/build.gradle.kts
        
    plugins {  
        kotlin("jvm")
    }
        
    dependencies {
        implementation(project(":annotations"))
        implementation("com.google.devtools.ksp:symbol-processing-api:2.3.6")  
    }
    // 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:

    // processor/src/main/kotlin/HelloWorldProcessor.kt
    
    class HelloWorldProcessor(val codeGenerator: CodeGenerator) : SymbolProcessor {
        // 1️⃣ process() function
        override fun process(resolver: Resolver): List<KSAnnotated> {
            resolver
                .getSymbolsWithAnnotation("com.example.annotations.HelloWorldAnnotation")
                .filter { it.validate() }
                .filterIsInstance<KSFunctionDeclaration>()
                .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())
    }

    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:

    // 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.

    • 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<D, R> interface.

      {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

      {style="tip"}

  4. Create a HelloWorldProcessorProvider.kt file. In it, declare a HelloWorldProcessorProvider class which inherits from SymbolProcessorProvider:

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

    ## processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
        
    HelloWorldProcessorProvider
    

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:

  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:

    // app/build.gradle.kts
    
    plugins {
        kotlin("jvm")
        id("com.google.devtools.ksp")
    }
    
    dependencies { 
        implementation(project(":annotations"))
        ksp(project(":processor"))
    }
    // 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:

    // settings.gradle.kts
    
    include("annotations")
    include("app")
    include("processor")
    // settings.gradle
    
    include 'processor'
    include 'annotations'
    include 'app'
  4. In the app module, create a Main.kt file and add the following code:

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

    Hello world from function generated by KSP
    

    KSP generates code in the GeneratedHelloWorld.kt file:

    app/build/generated/ksp/main/kotlin/GeneratedHelloWorld.kt
    

Explore the project structure

Your project's final file structure should look like this:

.
├── 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?