kotlin-compile-testing: compilation errors are hidden when KSP is used

Not sure what happened yet but between 20201106 and 20201023 versions of KSP, it started having a side effect where no kotlin compilation error is reported.

@Test
    fun badCodeFailsCompilation() {
        val src = SourceFile.kotlin("Foo.kt", """
            class Foo {
                val x:NonExistingType = TODO()
            }
        """.trimIndent())
        val instance = mock<SymbolProcessor>()
        val result = KotlinCompilation().apply {
            sources = listOf(src)
            symbolProcessors = listOf(instance)
        }.compile()
        assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
    }

Removing symbolProcessors = listOf(instance) line fixes the test. Recently, KSP re-implemented its plugin so this might be related to https://github.com/google/ksp/commit/328706164554f3036dba08333cdac6d52e8d0ab3.

Notice that the error is not from KSP processor, it simply drops the error in kotlin source code.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 2
  • Comments: 27 (24 by maintainers)

Most upvoted comments

Hi @yigit, @tschuchortdev. I’m interested in this rewrite. I think with the introduction of ksp the separation of kapt from the core components makes a lot of sense. What’s the current state of things? Is anyone working on this? Can I help?

@tschuchortdev @yigit Cannot say whether it is helpful, but here is the (extremely suboptimal) code for the approach @mvdbos mentioned

object KSPRuntimeCompiler {
    fun compile(compilation: KotlinCompilation): KotlinCompilation.Result {
        val pass1 = compilation.compile()
        require(pass1.exitCode == KotlinCompilation.ExitCode.OK) {
            "Cannot do the 1st pass"
        }
        val pass2 = KotlinCompilation().apply {
            sources = compilation.sources + compilation.kspGeneratedSourceFiles
        }.compile()
        require(pass2.exitCode == KotlinCompilation.ExitCode.OK) {
            "Cannot do the 2nd pass"
        }
        return pass2
    }
    private val KotlinCompilation.kspGeneratedSourceFiles: List<SourceFile>
        get() = kspSourcesDir.resolve("kotlin")
            .walk()
            .filter { it.isFile }
            .map { SourceFile.fromPath(it.absoluteFile) }
            .toList()
}

If we do that, we would need to recalculate sources for each of them (or ask them to do it). (e.g. not do this https://github.com/tschuchortdev/kotlin-compile-testing/blob/master/core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt#L606 or do it for every step, or ask every step to be responsible to run it)

Is that necessary? I think it would be enough to keep a list of paths to source files and we could make the writing of inline source files an explicit step in the compilation that is deferred. We could even make the workingDir an extra parameter to the compile function to make it more clear. It might become annoying syntactically when many actions are deferred, but then again, we have used flatMap all over for years with RxJava.

That is why, if we want to let user call compile multiple times, it is better if that operation never changes the KotlinCompilation object (even its internals).

We can also create copies of the KotlinCompilation object to “change” properties without changing the original object. I don’t want to get too hung up on the KotlinCompilation class as it exists now. It does a lot of things, perhaps too much. If we want to keep a somewhat similar interface, then I imagine we will end up with a function that builds the “task graph” under the hood from different classes that each have their own parameters, in order to present a simple interface for the user, or maybe something with the decorator pattern and lots of interface delegation to keep down the boilerplate.

The second compilation will have generated sources from the first one which would be a very surprising behavior from API perspective.

I don’t think so. The kapt generated sources are placed in a different folder and if we keep a list of paths, then we can avoid this even if they are in the same directory. In any case, we shouldn’t rely on the directory structure to know which files belong to a compilation. The user could have multiple source files belonging to different tests in a single resource folder.

Sorry, I took steps can edit KotlinCompile as they can add sources, which would have an impact on later steps / compile calls. Restricting them to report such extra changes in a well defined interface avoids these problems (kind of scopes it).

data class IntermediateResult(val exitCode:ExitCode, generatedSources : List<File>, anything else)

Perhaps I am overengineering, but that doesn’t look general enough to me to support all the use cases. For example, how would you add compiler plugins for following steps?

I’m not sure if we have that case but we can basically add it to the IntermediateResult as a parameter too then core compilation would merge them.

I’ll try to prototype the thing i’ve mentioned above. (separate user defined configuration/data from intermediate ones). I’ll ignore the current API of KotlinCompile (I think i got too attached to it). I should be possible to keep that api based on the new infra, at the least. I’ll try to prototype something and get back to you.