Don't use Type-safe Project Accessors with Kotlin Gradle DSL

Gradle 7.0 introduced the type-safe project accessors feature for IDE support of referencing projects in dependencies. This allowed you to replace this:

dependencies {
  implementation(project(":lib1"))
}

with this

dependencies {
  implementation(projects.lib1)
}

This is great! Improved IDE support is always a good thing for developer experience and it's super convenient to have autocomplete like this.

The Problem

There's a hidden but serious cost here.

To make this work, Gradle generates these accessors in Java, and it's based on the included projects in your settings. i.e.

// settings.gradle
include(":lib1")

This generates a file like this

package org.gradle.accessors.dm;

// imports...

@NonNullApi
public class RootProjectAccessor extends TypeSafeProjectDependencyFactory {


    @Inject
    public RootProjectAccessor(DefaultProjectDependencyFactory factory, ProjectFinder finder) {
        super(factory, finder);
    }

    /**
     * Creates a project dependency on the project at path ":lib1"
     */
    public Lib1ProjectDependency getLib1() { return new Lib1ProjectDependency(getFactory(), create(":lib1")); }

}

When Kotlin incremental compilation kicks in, it looks at the ABI of its depedencies. In this case, the accessors dependencies produce a certain ABI of all its public getters, and this is an input to all of your project's build files. The API surface area of these getters is more or less equal to the set of included projects.

Now, what happens if you change the set of included projects? The ABI changes, and those kotlin DSL build files all have to recompile now. That sucks! Especially in a larger codebase.

What's worse is that it's quite easy for your project's buildscript classpath to become an input to your project's compilation tasks (sadly I've yet to track down how or why this happens, but I do observe it in practice), and as a result your compilation tasks will also then be invalidated.

So, just removing an unused project from settings can cause a cascade of cache failures: buildscript file recompilation -> configuration cache miss -> possible compilation task cache miss.


This becomes especially rough if you use tools like Spotlight (which you should!) or Focus, as they are designed to minimize the set of included projects for a given build.

I'd actually go as far as saying that type-safe project accessors shouldn't be compatible with Kotlin Gradle DSL until there's a better IC solution or alternative IDE support, and highly recommend disabling them if you use them today. I put together a little script to do this if you want to borrow it here, though note it assumes a Spotlight all-projects.txt file (you can modify this as needed to read from elsewhere, such as settings.gradle.kts).

Other notes

  • This also affects version catalogs, but these are less invasive with tools like the ones I mentioned above and most changes to them are value changes (non-ABI changing) rather than changed dependencies.
  • This doesn't seem to affect Groovy buildscripts, but I don't understand enough about how those are compiled/Groovy IC works to say why. That said, Groovy is very much on its way out and this post is in no way advice to use Groovy.
  • This behavior is the same in Gradle <9.0.0's custom IC implementation and kotlinc's native IC implementation.