KotlinPoet

repository·main·Indexed 26 days ago

https://github.com/square/kotlinpoet

A library for generating .kt source files using a type-safe API to build Kotlin constructs such as classes, functions, and properties. It provides tools for handling annotations, anonymous inner classes, callable references, and structured code generation via CodeBlock and control flow APIs.

Tokens
14.4K
Snippets
58
Records
65
Agent score
87%

What's inside KotlinPoet

  1. Generate a secondary constructor using FunSpec

    main

    While FunSpec is primarily for functions, you can use FunSpec.constructorBuilder() to define a secondary constructor. You define parameters and statements just as you would for a regular method. KotlinPoet will automatically place the constructor before methods in the generated output.

    val flux = FunSpec.constructorBuilder()
      .addParameter("greeting", String::class)
      .addStatement("this.%N = %N", "greeting", "greeting")
      .build()
    
    val helloWorld = TypeSpec.classBuilder("HelloWorld")
      .addProperty("greeting", String::class, KModifier.PRIVATE)
      .addFunction(flux)
      .build()
  2. Define an interface with abstract methods

    main

    When defining an interface using TypeSpec.interfaceBuilder, interface methods must be explicitly marked with the KModifier.ABSTRACT modifier in your KotlinPoet code. However, KotlinPoet will omit this modifier in the generated source code because ABSTRACT is the default for interface members.

    val helloWorld = TypeSpec.interfaceBuilder("HelloWorld")
      .addProperty("buzz", String::class)
      .addFunction(
        FunSpec.builder("beep")
          .addModifiers(KModifier.ABSTRACT)
          .build()
      )
      .build()
  3. Add multiple context parameters to a function

    main

    To add multiple context parameters, you can use the .contextParameter(ContextParameter) method by passing in ContextParameter objects. This is useful when you have complex types or want to reuse parameter definitions.

    val loggerType = ClassName("java.util.logging", "Logger")
    val configType = ClassName("com.example", "Config")
    
    val logger = ContextParameter("logger", loggerType)
    val config = ContextParameter("config", configType)
    
    val processData = FunSpec.builder("processData")
      .contextParameter(logger)
      .contextParameter(config)
      .addStatement("%N.info(\"Processing with config: ${'$'}%N\")", logger, config)
      .build()
  4. Merge primary constructor parameters and properties

    main

    By default, KotlinPoet does not merge primary constructor parameters and properties even if they share the same name. To generate a concise primary constructor like class HelloWorld(private val greeting: String), you must explicitly tell KotlinPoet that the property is initialized via the constructor parameter using .initializer("parameterName") on the PropertySpec.

    val flux = FunSpec.constructorBuilder()
      .addParameter("greeting", String::class)
      .build()
    
    val helloWorld = TypeSpec.classBuilder("HelloWorld")
      .primaryConstructor(flux)
      .addProperty(
        PropertySpec.builder("greeting", String::class)
          .initializer("greeting")
          .addModifiers(KModifier.PRIVATE)
          .build()
      )
      .build()
  5. Declare parameters on methods and constructors

    main

    You can declare parameters for functions (FunSpec) or constructors using two primary methods:

    1. FunSpec.addParameter(): A convenient API for adding simple parameters by providing a name and a type.
    2. ParameterSpec.builder(): An extended builder pattern required when you need to add additional metadata to a parameter, such as a defaultValue or annotations (e.g., @Inject).
    val android = ParameterSpec.builder("android", String::class)
      .defaultValue("\"pie\"")
      .build()
    
    val welcomeOverlords = FunSpec.builder("welcomeOverlords")
      .addParameter(android)
      .addParameter("robot", String::class)
      .build()
  6. Convert Kotlin metadata to KotlinPoet source representations

    main

    The interop:kotlin-metadata API provides functions to convert core kotlin-metadata Km types directly into KotlinPoet TypeSpec or FileSpec objects. This includes full type resolution, signatures, and enclosed elements.

    Note: The generated sources are stub implementations. Implementation details (like function bodies or property getters) are replaced with TODO() placeholders.

    data class Taco(val seasoning: String, val soft: Boolean) {
      fun prepare() {
      }
    }
    
    // Convert to TypeSpec
    val typeSpec = Taco::class.toTypeSpec()
    
    // Or convert to FileSpec
    val fileSpec = Taco::class.toFileSpec()
  7. Generate a primary constructor using TypeSpec.primaryConstructor

    main

    To generate a primary constructor for a class, use the .primaryConstructor(funSpec) method on a TypeSpec.Builder. Note that by default, this generates a constructor signature followed by an init block if the parameters are not explicitly linked to properties.

    val flux = FunSpec.constructorBuilder()
      .addParameter("greeting", String::class)
      .build()
    
    val helloWorld = TypeSpec.classBuilder("HelloWorld")
      .primaryConstructor(flux)
      .addProperty("greeting", String::class, KModifier.PRIVATE)
      .build()
  8. Use ClassInspector to improve source generation accuracy

    main

    Because generated sources are a "best effort" representation, they may be incomplete. To improve accuracy for annotations, companion objects, JVM modifiers, and overrides, you should provide an optional ClassInspector instance to toTypeSpec() or toFileSpec().

    Implementations are available in the com.squareup.kotlinpoet.metadata.classinspectors package:

    • Reflective/javax Elements implementations: Use these to assist in parsing underlying JVM code.
  9. Use %L for literals in KotlinPoet statements

    main

    When building code with KotlinPoet, you can use the %L placeholder within string templates to emit literal values directly into the generated output. Unlike standard Kotlin string templates, %L values are emitted with no escaping. This is useful for injecting raw code fragments, primitives, or specific KotlinPoet types into statements. It behaves similarly to %s in String.format().

    private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec {
      return FunSpec.builder(name)
        .returns(Int::class)
        .addStatement("var result = 0")
        .beginControlFlow("for (i in %L..<%L)", from, to)
        .addStatement("result = result %L i", op)
        .endControlFlow()
        .addStatement("return result")
        .build()
    }
  10. Emit callable references to constructors, functions, and properties

    main

    You can generate Kotlin callable references (using the :: syntax) in your generated code using the following methods:

    • Constructors: Use ClassName.constructorReference().
    • Functions and Properties: Use MemberName.reference().

    Note that if top-level classes or members have conflicting names, you may need to use aliased imports, similar to how member names are handled.

    val helloClass = ClassName("com.example.hello", "Hello")
    val worldFunction: MemberName = helloClass.member("world")
    val byeProperty: MemberName = helloClass.nestedClass("World").member("bye")
    
    val factoriesFun = FunSpec.builder("factories")
      .addStatement("val hello = %L", helloClass.constructorReference())
      .addStatement("val world = %L", worldFunction.reference())
      .addStatement("val bye = %L", byeProperty.reference())
      .build()
    
    FileSpec.builder("com.example", "HelloWorld")
      .addFunction(factoriesFun)
      .build()
  11. Resolve KSP Type Parameters with TypeParameterResolver

    main

    When converting KSP types that rely on type parameters (from classes, functions, or typealiases), you must use a TypeParameterResolver to ensure parameters are correctly resolved by their index.

    1. Create a resolver: Call toTypeParameterResolver() on a List<KSTypeParameter> (e.g., ksClassDeclaration.typeParameters.toTypeParameterResolver()).
    2. Compose resolvers for nesting: For nested elements like functions inside classes, pass the enclosing resolver as the parent parameter to the child's toTypeParameterResolver(parent = ...) call.
    3. Use the resolver: Pass the TypeParameterResolver instance into toTypeName() calls.
  12. Handle member name collisions with addAliasedImport()

    main

    If multiple MemberName objects have the same simple name but different packages, importing them directly will cause a collision. To resolve this, use FileSpec.addAliasedImport(member, alias) to create an aliased import (e.g., import package.Member as alias).

    val isTacoVegan = MemberName("com.squareup.tacos", "isVegan")
    val isCakeVegan = MemberName("com.squareup.cakes", "isVegan")
    
    val file = FileSpec.builder("com.squareup.example", "Test")
      .addAliasedImport(isTacoVegan, "isTacoVegan")
      .addAliasedImport(isCakeVegan, "isCakeVegan")
      .addFunction(
        FunSpec.builder("main")
          .addStatement("println(taco.%M)", isTacoVegan)
          .addStatement("println(cake.%M)", isCakeVegan)
          .build()
      )
      .build()