O que é KSP em Kotlin?

KSP (Kotlin Symbol Processing) é uma API desenvolvida pelo Google para processar anotações e gerar código em tempo de compilação em projetos Kotlin. Ele é o substituto moderno do kapt (Kotlin Annotation Processing Tool), oferecendo performance até 2x melhor ao trabalhar diretamente com os simbolos do Kotlin em vez de gerar stubs Java intermediarios.

KSP entende conceitos nativos do Kotlin como propriedades, extension functions, nullable types e data classes, que o kapt (baseado no modelo Java) não consegue representar adequadamente.

Por que KSP em vez de kapt?

O kapt funciona gerando stubs Java a partir do código Kotlin, e depois executando processadores de anotacao Java padrão sobre esses stubs. Isso tem vários problemas:

  • Performance: gerar stubs e lento e consome memória.
  • Perda de informação: conceitos Kotlin (como nullable, data class, sealed class) se perdem na traducao para Java.
  • manutenção: os processadores precisam inferir informações Kotlin a partir da representacao Java.

KSP resolve tudo isso acessando a arvore de simbolos Kotlin diretamente.

Configurando KSP no projeto

// build.gradle.kts
plugins {
    kotlin("jvm") version "1.9.22"
    id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}

dependencies {
    // Adicionar processadores KSP
    ksp("com.example:meu-processador:1.0.0")
}

Para projetos multiplataforma:

plugins {
    kotlin("multiplatform") version "1.9.22"
    id("com.google.devtools.ksp") version "1.9.22-1.0.17"
}

dependencies {
    add("kspJvm", "com.example:meu-processador:1.0.0")
    add("kspIosArm64", "com.example:meu-processador:1.0.0")
}

Criando um processador KSP simples

Um processador KSP implementa a interface SymbolProcessor:

// Processador
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*

class MeuProcessador(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
) : SymbolProcessor {

    override fun process(resolver: Resolver): List<KSAnnotated> {
        val simbolos = resolver.getSymbolsWithAnnotation("com.example.AutoFactory")
            .filterIsInstance<KSClassDeclaration>()

        simbolos.forEach { classe ->
            gerarFactory(classe)
        }

        // Retorna simbolos nao processados para uma proxima rodada
        return emptyList()
    }

    private fun gerarFactory(classe: KSClassDeclaration) {
        val nomeClasse = classe.simpleName.asString()
        val nomePacote = classe.packageName.asString()
        val nomeFactory = "${nomeClasse}Factory"

        val arquivo = codeGenerator.createNewFile(
            dependencies = Dependencies(true, classe.containingFile!!),
            packageName = nomePacote,
            fileName = nomeFactory
        )

        arquivo.writer().use { writer ->
            writer.write("""
                package $nomePacote

                object $nomeFactory {
                    fun criar(): $nomeClasse {
                        return $nomeClasse()
                    }
                }
            """.trimIndent())
        }

        logger.info("Factory gerada: $nomeFactory")
    }
}

Provider do processador

O processador precisa de um SymbolProcessorProvider:

class MeuProcessadorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return MeuProcessador(
            codeGenerator = environment.codeGenerator,
            logger = environment.logger
        )
    }
}

Registre o provider no arquivo resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider:

com.example.MeuProcessadorProvider

Exemplo prático: gerador de Builder

// Anotacao
annotation class AutoBuilder

// Processador que gera builders
class BuilderProcessador(
    private val codeGenerator: CodeGenerator,
    private val logger: KSPLogger
) : SymbolProcessor {

    override fun process(resolver: Resolver): List<KSAnnotated> {
        resolver.getSymbolsWithAnnotation("com.example.AutoBuilder")
            .filterIsInstance<KSClassDeclaration>()
            .forEach { gerarBuilder(it) }
        return emptyList()
    }

    private fun gerarBuilder(classe: KSClassDeclaration) {
        val nome = classe.simpleName.asString()
        val pacote = classe.packageName.asString()
        val propriedades = classe.primaryConstructor?.parameters ?: return

        val builder = buildString {
            appendLine("package $pacote")
            appendLine()
            appendLine("class ${nome}Builder {")

            propriedades.forEach { param ->
                val nomeProp = param.name?.asString() ?: return@forEach
                val tipo = param.type.resolve().declaration.qualifiedName?.asString() ?: "Any"
                appendLine("    var $nomeProp: $tipo? = null")
            }

            appendLine()
            appendLine("    fun build(): $nome {")
            val args = propriedades.joinToString(", ") { param ->
                val n = param.name?.asString() ?: ""
                "$n = $n ?: throw IllegalStateException(\"$n nao definido\")"
            }
            appendLine("        return $nome($args)")
            appendLine("    }")
            appendLine("}")
        }

        codeGenerator.createNewFile(
            Dependencies(true, classe.containingFile!!),
            pacote, "${nome}Builder"
        ).writer().use { it.write(builder) }
    }
}

Uso:

@AutoBuilder
data class Pessoa(val nome: String, val idade: Int)

// Código gerado automaticamente:
// PessoaBuilder com metodos nome(), idade() e build()
fun main() {
    val pessoa = PessoaBuilder().apply {
        nome = "Ana"
        idade = 30
    }.build()
}

O KSP fornece uma API rica para inspecionar o código Kotlin:

fun inspecionarClasse(classe: KSClassDeclaration) {
    // Nome e pacote
    val nome = classe.simpleName.asString()
    val pacote = classe.packageName.asString()

    // Modificadores
    val eDataClass = Modifier.DATA in classe.modifiers
    val eSealed = Modifier.SEALED in classe.modifiers

    // Propriedades
    classe.getAllProperties().forEach { prop ->
        val nomeProp = prop.simpleName.asString()
        val tipo = prop.type.resolve()
        val nullable = tipo.isMarkedNullable
    }

    // Funções
    classe.getAllFunctions().forEach { func ->
        val nomeFunc = func.simpleName.asString()
        val parametros = func.parameters
    }

    // Supertipos
    classe.superTypes.forEach { superTipo ->
        val tipoResolvido = superTipo.resolve()
    }
}

Quando usar KSP

  • Reducao de boilerplate: gerar builders, factories, mappers e adaptadores automaticamente.
  • Validação em tempo de compilação: verificar que anotações estao sendo usadas corretamente.
  • Frameworks e bibliotecas: Room, Moshi, Koin e outras bibliotecas usam KSP para geracao de código.
  • serialização customizada: gerar serializadores e desserializadores específicos.
  • Documentação automatica: extrair informações do código para gerar documentação.

Casos de Uso no Mundo Real

  1. Room Database no Android: A biblioteca Room usa KSP para gerar implementacoes de DAOs (Data Access Objects) e validar queries SQL em tempo de compilação. Ao anotar uma interface com @Dao, o KSP gera automaticamente todo o código de acesso ao banco de dados SQLite, eliminando boilerplate e garantindo que queries invalidas sejam detectadas antes da execução.

  2. Moshi e serialização JSON: O Moshi utiliza KSP para gerar adaptadores JSON eficientes a partir de data classes Kotlin. Em vez de usar reflexao em tempo de execução (lento e propenso a erros), o KSP analisa as propriedades da classe e gera código otimizado de serialização e desserializacao durante a compilação.

  3. Koin e injecao de dependências: O framework Koin utiliza KSP para verificar o grafo de dependências em tempo de compilação, detectando dependências circulares ou faltantes antes mesmo da aplicação ser executada.

  4. Geracao de código para APIs internas: Equipes que mantém SDKs internos usam KSP para gerar automaticamente classes de mapeamento entre modelos de API e modelos de dominio, garantindo que mudancas no schema da API sejam refletidas em erros de compilação em vez de falhas em producao.

Boas Praticas

  • Sempre mantenha a versão do KSP sincronizada com a versão do Kotlin. O formato de versionamento do KSP segue o padrão <versão-kotlin>-<versão-ksp> (ex: 1.9.22-1.0.17).
  • Declare corretamente as dependências no CodeGenerator usando o parametro Dependencies, indicando quais arquivos fonte originaram o código gerado. Isso garante que o cache incremental funcione corretamente.
  • Retorne simbolos não processados da função process() quando eles dependem de código gerado por outros processadores, permitindo que sejam processados em rodadas subsequentes.
  • Use a API de teste kotlin-compile-testing para validar que seu processador gera código correto e que os erros de compilação esperados sao emitidos com mensagens claras.
  • Prefira KSP a kapt em novos projetos. Se já usa kapt, migre gradualmente verificando se as bibliotecas que você utiliza já oferecem suporte a KSP.

Perguntas Frequentes

P: Posso usar KSP e kapt no mesmo projeto? R: Sim, ambos podem coexistir no mesmo modulo. Isso e útil durante a migração gradual de kapt para KSP. Porem, manter ambos significa que o projeto ainda incorre no custo de performance do kapt para os processadores que ainda não foram migrados.

P: KSP funciona com Kotlin Multiplatform? R: Sim. O KSP suporta projetos Kotlin Multiplatform. Voce pode configurar processadores para targets específicos usando notacoes como kspJvm, kspIosArm64, etc. no bloco de dependências do Gradle.

P: Qual a diferenca entre KSP e um plugin de compilador Kotlin? R: KSP opera no nivel de simbolos (classes, funções, propriedades) e só pode gerar novos arquivos, não modificar código existente. Um plugin de compilador tem acesso ao IR (Intermediate Representation) e pode transformar o código existente. KSP e mais simples de usar e mais estavel entre versões do Kotlin, enquanto plugins de compilador sao mais poderosos mas exigem manutenção constante.

P: Como depurar um processador KSP? R: Voce pode usar logger.warn() ou logger.error() para emitir mensagens durante a compilação. Para depuração interativa, execute o build Gradle com a flag --no-daemon -Dorg.gradle.debug=true e conecte um debugger remoto na porta indicada.

Erros comuns

  1. Versão incompativel do KSP com Kotlin: a versão do KSP deve corresponder exatamente a versão do Kotlin (ex: KSP 1.9.22-1.0.17 para Kotlin 1.9.22).

  2. Nao declarar dependencies corretamente: o CodeGenerator precisa saber de quais arquivos o código gerado depende para invalidacao correta do cache.

  3. Processar na rodada errada: se um simbolo depende de código gerado por outro processador, ele pode não estar disponivel na primeira rodada. Retorne simbolos não processados da função process.

  4. Ignorar tipos nullable: diferente do kapt, KSP expõe nullability nativamente. Nao tratear isso gera código incorreto.

  5. Nao testar o processador: KSP oferece uma API de teste (kotlin-compile-testing) para verificar que o código gerado esta correto.

Termos relacionados

  • Gradle Plugin: o plugin KSP e aplicado no build.gradle.kts para habilitar o processamento.
  • Annotation: anotações que marcam classes e funções para processamento pelo KSP.
  • Serialization: a biblioteca kotlinx.serialization usa um plugin de compilador similar para gerar código.
  • kapt: o predecessor do KSP, baseado em annotation processing do Java.
  • Code Generation: o resultado principal do KSP e gerar arquivos fonte que são compilados junto com o projeto.
  • Kotlin Multiplatform: KSP suporta projetos multiplataforma, processando código de cada target.

KSP e uma ferramenta poderosa para eliminar boilerplate e garantir segurança em tempo de compilação. Se você desenvolve bibliotecas ou frameworks em Kotlin, dominar KSP permite criar APIs que são ao mesmo tempo expressivas e eficientes.