Access plugin's task extension in configuration phase

I’m writing a convention plugin in Java which is only supposed to do two things:

  1. apply some other plugin
  2. add some configuration for said plugin

This works fine for plugins that can be configured via some project “Extension” (for example, the Checkstyle plugin applies project.getExtensions().create("checkstyle", CheckstyleExtension.class, project)). However, the error prone plugin (GitHub - tbroyer/gradle-errorprone-plugin: Gradle plugin to use the error-prone compiler for Java) creates an extension for the JavaCompile tasks, which I’m unable to access.

When I try to access this extension, it fails.

Code:

public void apply(Project project) {
    project.getPluginManager().apply(ErrorPronePlugin.class);

    project.getTasks().withType(JavaCompile.class).configureEach(
        compileTask -> {
            compileTask.getExtensions().getByType(ErrorProneOptions.class);
        }
    );
}
Extension of type 'ErrorProneOptions' does not exist. Currently registered extension types: [ExtraPropertiesExtension]"

My gut feeling is that the plugin isn’t done with its configuration, i.e. I try to access the extension before it is created. I experiemented with project.afterEvaluate and project.getPluginManager().withPlugin, which didn’t help.

I had a look at the code used for my working code which uses a Kotlin script (kts):

tasks.withType<JavaCompile>().configureEach {
    options.errorprone.disable("EqualsGetClass")
}

The access to JavaCompile.options is straightforward, but CompileOptions.errorprone does not exist in any way. Instead, the “errorprone” plugin code defines an additional Getter, which seems to be a Kotlin concept (similar to traits?):

val CompileOptions.errorprone: ErrorProneOptions
    get() = (this as ExtensionAware).extensions.getByName<ErrorProneOptions>(ErrorProneOptions.NAME)

As such, I tried blindly casting CompileOptions to ExtensionAware and, voila, it worked. The part of my brain that’s used to static typing is angry, but the rest of me is happy.

project.getTasks().withType(JavaCompile.class).configureEach(
    compileTask -> {
        ErrorProneOptions errorProne = ((ExtensionAware) compileTask.getOptions()).getExtensions().getByType(ErrorProneOptions.class);
        errorProne.disable("EqualsGetClass");
    }
);

You could have seen that also from the errorprone snippet you posted in your original post, as it does exactly that to create the extension:

project.tasks.withType<JavaCompile>().configureEach {
    val errorproneOptions =
        (options as ExtensionAware).extensions.create(ErrorProneOptions.NAME, ErrorProneOptions::class.java)

options as ExtensionAware is casting options to the type ExtensionAware.

This type-unsafety is unnice, but a result of Gradle magic and the extreme dynamicness and duck-typing of Groovy that was the language of choice in the back old days when Gradle started.

Basically all internal types, as well as any type you let Gradle create for you through things like tasks.register, tasks.create, extensions.create, objects.newInstance, and so on and so forth, are decorated to be ExtensionAware. But only a handfull of the internal types also declare that interface explicitly, as it seems to be a bit awkward to formally implement it outside of the automatic decorating. That’s why you sometimes have to cast things to ExtensionAware to add or get extensions from certain domain objects.

1 Like