Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Metaprogramming with Kotlin Symbol Processing

Avatar for Anthony Restaino Anthony Restaino
June 25, 2025
39

Metaprogramming with Kotlin Symbol Processing

Avatar for Anthony Restaino

Anthony Restaino

June 25, 2025
Tweet

Transcript

  1. Less Ancient History April 1960 - LISP published January 1996

    - Java 1.0 published February 2005 - JSR 269 - AP • Annotation Processing (AP) • Generate Java code • Released in Java 6
  2. Recent History April 1960 - LISP published January 1996 -

    Java 1.0 published February 2005 - JSR 269 - AP March 2017 - KAPT • Runs on top of Java AP • Generate Java or Kotlin code • AP compatible
  3. Recent History April 1960 - LISP published January 1996 -

    Java 1.0 published February 2005 - JSR 269 - AP March 2017 - KAPT September 2020 - KSP • Kotlin fi rst code generation • AP and KAPT incompatible
  4. Processor Provider class TaggerSymbolProcessorProvider : SymbolProcessorProvider { override fun create(

    environment: SymbolProcessorEnvironment ): SymbolProcessor = TaggerSymbolProcessor( environment.codeGenerator ) }
  5. Processor Provider class TaggerSymbolProcessorProvider : SymbolProcessorProvider { override fun create(

    environment: SymbolProcessorEnvironment ): SymbolProcessor = TaggerSymbolProcessor( environment.codeGenerator ) } com.tagger.TaggerSymbolProcessorProvider
  6. Processor Provider class TaggerSymbolProcessorProvider : SymbolProcessorProvider { override fun create(

    environment: SymbolProcessorEnvironment ): SymbolProcessor = TaggerSymbolProcessor( environment.codeGenerator ) } com.tagger.TaggerSymbolProcessorProvider
  7. class TaggerSymbolProcessor( private val codeGenerator: CodeGenerator, ) : SymbolProcessor {

    override fun process(resolver: Resolver): List<KSAnnotated> { } } Processor
  8. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", "Droidcon") .build() } }
  9. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() } }
  10. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(ClassName("com.droidcon", "Droidcon")) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } }
  11. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(declaration.toClassName()) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } }
  12. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(declaration.toClassName()) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } .toList() }
  13. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(declaration.toClassName()) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } .toList() .takeIf { it.isNotEmpty() } }
  14. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(declaration.toClassName()) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } .toList() .takeIf { it.isNotEmpty() } ?. let { propertySpecs -> FileSpec .builder("com.tagger.generated", "Tags") .addProperties(propertySpecs) .build() } }
  15. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(declaration.toClassName()) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } .toList() .takeIf { it.isNotEmpty() } ?. let { propertySpecs -> FileSpec .builder("com.tagger.generated", "Tags") .addProperties(propertySpecs) .build() } ?. writeTo(codeGenerator, true) }
  16. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(declaration.toClassName()) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } .toList() .takeIf { it.isNotEmpty() } ?. let { propertySpecs -> FileSpec .builder("com.tagger.generated", "Tags") .addProperties(propertySpecs) .build() } ?. writeTo(codeGenerator, true) return emptyList() }
  17. override fun process(resolver: Resolver): List<KSAnnotated> { resolver.getSymbolsWithAnnotation(Tag :: class.qualifiedName !

    ! ) .filterIsInstance<KSClassDeclaration>() .map { declaration -> val funSpec = FunSpec .getterBuilder() .addStatement("return %S", declaration.simpleName.asString()) .build() PropertySpec .builder("TAG", String :: class) .receiver(declaration.toClassName()) .mutable(false) .getter(funSpec) .addOriginatingKSFile(declaration.containingFile !! ) .build() } .toList() .takeIf { it.isNotEmpty() } ?. let { propertySpecs -> FileSpec .builder("com.tagger.generated", "Tags") .addProperties(propertySpecs) .build() } ?. writeTo(codeGenerator, true) return emptyList() }
  18. Resources My sample KSP: github.com/anthonycr/Tagger Google’s sample KSP: github.com/google/ksp/tree/main/examples KSP

    documentation: kotlinlang.org/docs/ksp-quickstart.html KotlinPoet: github.com/square/kotlinpoet Complain about this talk: bsky.app/pro fi le/anthonycr.bsky.social