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 KotlinCompile → AbstractKotlinCompile → AbstractKotlinCompileTool → 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
Summary
After upgrading from KSP 1.0.31 (
KspTaskJvm) to KSP 2.0.4 (KspAATask), 38 modules that were previously skipped asNO-SOURCEnow execute KSP unnecessarily. The root cause is thatKspAATaskdeclares its source inputs using unfilteredFileTreeobjects, whileKspTaskJvminherited Kotlin source file pattern filtering (*.kt,*.kts) fromKotlinCompile.Environment
Evidence
We compared two warm-cache builds on the same codebase, differing only in Kotlin/KSP version:
KspTaskJvm)KspAATask)kspDebugKotlintasksno-source(skipped)avoided_up_to_dateexecuted_cacheable38 modules lost their
NO-SOURCEskip. 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:
KspTaskJvminherits source filtering fromKotlinCompileKspTaskJvmextendsKotlinCompile→AbstractKotlinCompile→AbstractKotlinCompileTool→ implementsKotlinCompileTool.The
KotlinCompileToolinterface declares:And
KspTaskJvmadds:sourcesis filtered throughsourceFileFilterwhich only matches*.ktand*.kts. A source directory containing no Kotlin files produces an emptysourcescollection. Combined with the@SkipWhenEmptyannotation, Gradle marks the task asNO-SOURCE.KSP2:
KspAATaskuses unfiltered file treesKspAATaskextendsDefaultTask(notKotlinCompile) and declares sources on a nested config object: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 treesfileTree().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@SkipWhenEmptyfrom triggeringNO-SOURCE.Comparison table
KspTaskJvm)KspAATask)KotlinCompileDefaultTask*.kt,*.ktspattern@SkipWhenEmptyonsources+javaSourcessourceRoots+commonSourceRoots+javaSourceRootsskipCondition()overridesources.isEmpty && javaSources.isEmptySuggested fix
Add file extension filters to the source file trees in
KspAATask:This restores the pattern filtering that
KotlinCompileprovides to KSP1, allowing Gradle to correctly mark modules with no Kotlin/Java sources asNO-SOURCE.Impact