Control output folder and source set for a KSP annotation processor

636 Views Asked by At

In an Android project (native with mixed java / kotlin) we are using a KSP based annotation processor to collect class definitions that are annotated with a specific project internal annotation. All works fine, we generate a top level function from that data which is again used in the build process.

We implemented the annotation processor in a separate module inside our android project to keep it separate from the actual application code. That means that we are using a string that contains the annotation name to scan for annotated classes, since we cannot add a dependency to the module where the annotation is actually declared.

Now I wanted to add a primitive unit test that just checks if that top level function in the generated file exists and contains a non empty list. This should prevent situation where the annotation got refactored (renamed) while the name inside the annotation processor is unchanged. That situation would lead to a broken application since formally the file would get generated, but it would contain an empty list. Which is why my test checks if that list is not empty.

Here I ran into an issue though: when executing the unit test KSP is triggered again and creates a second generated file with a top level function that is indeed empty. Probably because KSP does not see the actual classes it is meant to process. That file is created under a different folder in the build folder, devDebug versus devDebugUnitTest where "devDebug" is the current build variant.

So I have two questions:

  1. How can I tell KSP to always place files generated with the environment.codeGenerator.createNewFile() method inside the same folder, no matter what scope it gets triggered from?

  2. How can I tell KSP to also scan the actual application source folders so that it creates a list that contains the expected entries? I found a few examples for adding to the srcDirs variable in gradle, but those are all for modules using various android plugins which we do not use inside that specific module. Because that module has nothing to do with Android, but only works on a kotlin code level.

Here is the gradle file for that processing module:

plugins {
    id 'java-library'
    id 'org.jetbrains.kotlin.jvm'
    id 'com.google.devtools.ksp'}

java {
    sourceCompatibility = versions.java.javaVersion
    targetCompatibility = versions.java.javaVersion
}

dependencies {
    implementation libs.googleDevtoolsKspSymbolProcessing
}

And this is the core part of the implemented processor:

class SomeAnnotationSymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor {

    override fun process(resolver: Resolver): List<KSClassDeclaration> {
        val annotatedClasses: Sequence<KSClassDeclaration> =
            resolver.getSymbolsWithAnnotation(SOME_ANNOTATION)
                .filterIsInstance<KSClassDeclaration>()
                .filter {
                    it.annotations.any()
                }

        try {
            environment.logger.info("Creating file '$GENERATED_CLASS_NAME' based on found '$SOME_ANNOTATION_NAME' annotations.")
            annotatedClasses.mapNotNull { it.containingFile }.run {
                environment.codeGenerator.createNewFile(
                    dependencies = Dependencies(
                        false,
                        *this.toList().toTypedArray(),
                    ),
                    packageName = SCAN_BARCODES_PACKAGE,
                    fileName = GENERATED_CLASS_NAME,
                ).also {
                    it.write(assembleClassImplementation(annotatedClasses.toList()).toByteArray())
                }
            }
        } catch (_: FileAlreadyExistsException) {
            environment.logger.info("Existing file '$GENERATED_CLASS_NAME' has been overwritten silently.")
        }

        // Provide list of skipped symbols to the next round of symbol processing.
        // Those symbols will get processed in the next round, the processor automatically takes care of that based on the `Sequence` logic.
        return annotatedClasses.filterNot { it.validate() }.toList().also {
            environment.logger.info("Files not yet considered valid and held back for next round: ${it.joinToString(", ")}")
        }
    }
0

There are 0 best solutions below