Skip to content

KSP2 KspAATask lost no-source skip optimization from KSP1 KspTaskJvm #2947

@changusmc

Description

@changusmc

Summary

After upgrading from KSP 1.0.31 (KspTaskJvm) to KSP 2.0.4 (KspAATask), 38 modules that were previously skipped as NO-SOURCE now execute KSP unnecessarily. The root cause is that KspAATask declares its source inputs using unfiltered FileTree objects, while KspTaskJvm inherited Kotlin source file pattern filtering (*.kt, *.kts) from KotlinCompile.

Environment

  • Kotlin: 2.1.20 → 2.2.21
  • KSP: 2.1.20-1.0.31 → 2.2.21-2.0.4
  • Gradle: 9.2.1 → 9.3.1

Evidence

We compared two warm-cache builds on the same codebase, differing only in Kotlin/KSP version:

Metric KSP1 (KspTaskJvm) KSP2 (KspAATask)
Total kspDebugKotlin tasks 567 556
no-source (skipped) 48 10
avoided_up_to_date 178 270
executed_cacheable 341 276

38 modules lost their NO-SOURCE skip. These modules have KSP processors on the classpath (via Hilt/Dagger convention plugins) but contain no processable annotations — KSP initializes, finds nothing, and exits. Each one still costs 300–500ms of pure overhead.

Data collected from Develocity (Gradle Enterprise) build scans across ~900 builds from February–May 2026.

Root cause

KSP1: KspTaskJvm inherits source filtering from KotlinCompile

KspTaskJvm extends KotlinCompileAbstractKotlinCompileAbstractKotlinCompileTool → implements KotlinCompileTool.

The KotlinCompileTool interface declares:

// KotlinCompileTool (JetBrains/kotlin)
@get:InputFiles
@get:SkipWhenEmpty
@get:IgnoreEmptyDirectories
@get:PathSensitive(PathSensitivity.RELATIVE)
val sources: FileCollection  // filtered through sourceFileFilter: *.kt, *.kts

And KspTaskJvm adds:

// KspTaskJvm (google/ksp, KotlinFactories.kt ~line 229)
override fun skipCondition(): Boolean = sources.isEmpty && javaSources.isEmpty

@get:InputFiles
@get:SkipWhenEmpty
override val javaSources: FileCollection = super.javaSources.filter {
    !destination.get().isParentOf(it)
}

sources is filtered through sourceFileFilter which only matches *.kt and *.kts. A source directory containing no Kotlin files produces an empty sources collection. Combined with the @SkipWhenEmpty annotation, Gradle marks the task as NO-SOURCE.

KSP2: KspAATask uses unfiltered file trees

KspAATask extends DefaultTask (not KotlinCompile) and declares sources on a nested config object:

// KspAATask (google/ksp, KspAATask.kt ~line 472)
abstract class KspGradleConfig {
    @get:InputFiles
    @get:SkipWhenEmpty
    @get:IgnoreEmptyDirectories
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sourceRoots: ConfigurableFileCollection  // NO file extension filter

    @get:InputFiles
    @get:SkipWhenEmpty
    @get:IgnoreEmptyDirectories
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val javaSourceRoots: ConfigurableFileCollection  // NO file extension filter
}

Source wiring (~line 210):

cfg.sourceRoots.from(
    sourceSet.kotlin.srcDirs.filter { ... }.map {
        project.objects.fileTree().from(it)  // ALL files in directory
    }
)
cfg.javaSourceRoots.from(filtered)  // same unfiltered trees

fileTree().from(dir) includes all files in the directory — not just *.kt/*.java. Any file (resources, .gitkeep, build artifacts) makes the collection non-empty, preventing Gradle's @SkipWhenEmpty from triggering NO-SOURCE.

Comparison table

Aspect KSP1 (KspTaskJvm) KSP2 (KspAATask)
Base class KotlinCompile DefaultTask
Source file filter *.kt, *.kts pattern None (all files in dir)
@SkipWhenEmpty on sources + javaSources sourceRoots + commonSourceRoots + javaSourceRoots
skipCondition() override sources.isEmpty && javaSources.isEmpty None

Suggested fix

Add file extension filters to the source file trees in KspAATask:

// KspAATask.kt, registerKspAATask(), ~line 215
// Instead of:
project.objects.fileTree().from(it)

// Should be:
project.objects.fileTree().from(it).matching {
    it.include("**/*.kt", "**/*.kts", "**/*.java")
}

This restores the pattern filtering that KotlinCompile provides to KSP1, allowing Gradle to correctly mark modules with no Kotlin/Java sources as NO-SOURCE.

Impact

  • 38 modules unnecessarily run KSP per full rebuild
  • ~300–500ms overhead per module (KSP initialization + teardown with no processing)
  • ~15–20s total serial time wasted per full rebuild
  • Additionally contributes to cache key churn since these modules produce empty/trivial KSP outputs that change with compiler version bumps

Metadata

Metadata

Assignees

Labels

GradleIssues with KSP Gradle Plugin, AGP or Gradle.

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions