@@ -19,8 +19,11 @@ import org.gradle.api.plugins.ExtensionAware
1919import org.gradle.api.plugins.JavaBasePlugin
2020import org.gradle.api.plugins.JavaPluginExtension
2121import org.gradle.api.provider.ListProperty
22+ import org.gradle.api.provider.Property
2223import org.gradle.api.provider.Provider
24+ import org.gradle.api.specs.Spec
2325import org.gradle.api.tasks.Input
26+ import org.gradle.api.tasks.Optional
2427import org.gradle.api.tasks.PathSensitivity
2528import org.gradle.api.tasks.SourceSet
2629import org.gradle.api.tasks.SourceSetContainer
@@ -86,10 +89,24 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
8689 // that creates the directory's contents.
8790 val cfManifestFiles = project.files(writeManifestTask.flatMap { it.cfBuildDir })
8891
92+ // Whether to run the Checker Framework on a [JavaCompile] task, by task name.
93+ // configureJavaCompileTasks sets each value; a task whose value is never set, because
94+ // configureJavaCompileTasks leaves the task alone, is not compiled with the Checker Framework.
95+ val cfEnabled = HashMap <String , Property <Boolean >>()
96+
8997 project.tasks.withType<JavaCompile >().configureEach {
9098 (options as ExtensionAware )
9199 .extensions
92100 .create(" checkerFrameworkCompile" , CheckerFrameworkCompileExtension ::class .java)
101+
102+ // The task action that does the configuration that has to run after all other configuration
103+ // of the task is registered here, while this plugin is being applied, rather than in
104+ // configureJavaCompileTasks below. Registering it as early as possible puts it last among the
105+ // task's doFirst actions, because doFirst prepends. What it does is decided by the `enabled`
106+ // property, whose value configureJavaCompileTasks sets after the build script has run.
107+ val enabled = project.objects.property(Boolean ::class .java)
108+ cfEnabled[name] = enabled
109+ doFirst(ApplyCheckerFrameworkOptions (enabled, cfExtension.checkers, cfManifestFiles))
93110 }
94111
95112 // Register the actions that add dependencies now, rather than after the build script has run,
@@ -107,13 +124,17 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
107124
108125 // Configure after the build script has run, so that the values of the extensions and of the
109126 // project properties are the ones the user requested, no matter when a task is realized.
110- afterEvaluateOrNow(project) { configureJavaCompileTasks(project, cfExtension, cfManifestFiles) }
127+ afterEvaluateOrNow(project) {
128+ configureJavaCompileTasks(project, cfExtension, cfManifestFiles, cfEnabled)
129+ }
111130
112131 // Handle Lombok
113132 project.pluginManager.withPlugin(" io.freefair.lombok" ) {
114133 val javaPluginExtension: JavaPluginExtension =
115134 project.extensions.getByType(JavaPluginExtension ::class .java)
116- javaPluginExtension.sourceSets.configureEach { addCheckDelombokTask(this , project) }
135+ javaPluginExtension.sourceSets.configureEach {
136+ addCheckDelombokTask(this , project, cfEnabled)
137+ }
117138 }
118139 }
119140
@@ -235,11 +256,20 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
235256 }
236257 }
237258
238- /* * Configures every [JavaCompile] task on which the Checker Framework should be run. */
259+ /* *
260+ * Configures every [JavaCompile] task on which the Checker Framework should be run.
261+ *
262+ * @param project current project
263+ * @param cfExtension the plugin's configuration options
264+ * @param cfManifestFiles the Checker Framework manifest directory
265+ * @param cfEnabled for each task, the property that says whether to run the Checker Framework on
266+ * it, which this method sets
267+ */
239268 private fun configureJavaCompileTasks (
240269 project : Project ,
241270 cfExtension : CheckerFrameworkExtension ,
242271 cfManifestFiles : FileCollection ,
272+ cfEnabled : Map <String , Property <Boolean >>,
243273 ) {
244274 project.tasks.withType<JavaCompile >().configureEach {
245275 // The "skipCheckerFramework" project property is read here, rather than once outside this
@@ -309,28 +339,34 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
309339 .withPropertyName(" checkerFrameworkManifest" )
310340 .withPathSensitivity(PathSensitivity .RELATIVE )
311341
312- // Put the manifest directory on the annotation processor path here, rather than only in the
313- // task action below , so that the Checker Framework is found even if the task action's
342+ // Put the manifest directory on the annotation processor path here, rather than only in
343+ // ApplyCheckerFrameworkOptions , so that the Checker Framework is found even if that action's
314344 // changes to the path come too late.
315345 // If the annotationProcessorPath is null, then annotation processing is disabled, so there
316346 // is no need to add things to the path.
317347 options.annotationProcessorPath =
318348 options.annotationProcessorPath?.plus(manifestFilesIfEnabled)
319349
320- // The rest of the configuration must be done after every other configuration of the task,
321- // so that neither the user nor another plugin can accidentally undo it. A task action runs
322- // after all configuration, no matter in what order the configuration was registered.
323- doFirst(
324- ApplyCheckerFrameworkOptions (enabled, cfExtension.checkers, cfManifestFiles, requestedFork)
325- )
350+ // The rest of the configuration is done by the task action that was registered while this
351+ // plugin was being applied; enabling it here is what makes that action do anything.
352+ cfEnabled.getValue(name).set(enabled)
326353 }
327354 }
328355
329356 /* *
330357 * Adds a checkDelombokCompileJava task, for the given source set, that copies the compileJava
331358 * task, but changes the source to the result of the delombok task.
359+ *
360+ * @param sourceSet the source set to add the task for
361+ * @param project current project
362+ * @param cfEnabled for each task, the property that says whether to run the Checker Framework on
363+ * it
332364 */
333- private fun addCheckDelombokTask (sourceSet : SourceSet , project : Project ) {
365+ private fun addCheckDelombokTask (
366+ sourceSet : SourceSet ,
367+ project : Project ,
368+ cfEnabled : Map <String , Property <Boolean >>,
369+ ) {
334370
335371 val checkerTaskProvider: TaskProvider <JavaCompile > =
336372 project.tasks.register(
@@ -365,10 +401,11 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
365401 project.layout.buildDirectory.dir(sourceSet.getTaskName(" checkerFramework" , " Classes" ))
366402 )
367403 checkerTask.options.compilerArgs = ArrayList (compileTask.options.compilerArgs)
368- // This discards whatever this plugin put on the checker task's annotation processor path,
369- // but ApplyCheckerFrameworkOptions restores the manifest directory at execution time, and
370- // the manifest directory is a declared input of the task in any case.
371404 checkerTask.options.annotationProcessorPath = compileTask.options.annotationProcessorPath
405+
406+ // Running the Checker Framework is the only purpose of this task, so do not run the task at
407+ // all if the Checker Framework is disabled on it.
408+ checkerTask.onlyIf(RunOnlyIfCheckerFrameworkEnabled (cfEnabled.getValue(checkerTask.name)))
372409 project.tasks.named(" build" ).configure { dependsOn(checkerTask) }
373410 }
374411 }
@@ -549,10 +586,30 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
549586 }
550587 }
551588
589+ /* *
590+ * Runs a task only if the Checker Framework is enabled on it.
591+ *
592+ * @param enabled whether to run the Checker Framework on the task; the task does not run if the
593+ * property has no value, which means that this plugin left the task alone
594+ */
595+ internal class RunOnlyIfCheckerFrameworkEnabled (private val enabled : Provider <Boolean >) :
596+ Spec <Task > {
597+ override fun isSatisfiedBy (task : Task ): Boolean {
598+ return enabled.getOrElse(false )
599+ }
600+ }
601+
552602 /* *
553603 * The part of the Checker Framework configuration of a [JavaCompile] task that has to run after
554604 * all other configuration of the task. Because it is a task action, it runs after configuration
555- * is complete, and neither the user nor another plugin can undo its effect.
605+ * is complete. It is registered while this plugin is being applied, so it also runs after every
606+ * `doFirst` action that a build script or another plugin registers later, and therefore has the
607+ * last word about the options that it sets.
608+ *
609+ * @param enabled whether to run the Checker Framework on the task; this action does nothing if
610+ * the property has no value, which means that this plugin left the task alone
611+ * @param checkers the checkers to run
612+ * @param cfManifestFiles the Checker Framework manifest directory
556613 */
557614 internal class ApplyCheckerFrameworkOptions (
558615 private val enabled : Provider <Boolean >,
@@ -580,20 +637,19 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
580637 if (checkerNames.isEmpty()) {
581638 throw IllegalStateException (" Must specify checkers for the Checker Framework." )
582639 }
640+ val options = (task as JavaCompile ).options
641+ // If the annotationProcessorPath is null, then annotation processing is disabled, so no
642+ // checker will run and there is nothing to configure.
643+ val annotationProcessorPath = options.annotationProcessorPath ? : return
583644
584645 // Must fork for the JVM arguments to be applied. Configuration time requests forking if the
585646 // Checker Framework was enabled then, but this ensures that no other configuration has undone
586647 // it and that a compilation that the user enabled later forks as well.
587648 options.isFork = true
588649
589- // If the annotationProcessorPath is null, then annotation processing is disabled, so there
590- // is no need to add things to the path. The path already contains the manifest directory
591- // unless some other configuration replaced the path.
592- val annotationProcessorPath = options.annotationProcessorPath
593- if (
594- annotationProcessorPath != null &&
595- ! annotationProcessorPath.files.containsAll(cfManifestFiles.files)
596- ) {
650+ // The path already contains the manifest directory unless some other configuration replaced
651+ // the path.
652+ if (! annotationProcessorPath.files.containsAll(cfManifestFiles.files)) {
597653 options.annotationProcessorPath = annotationProcessorPath.plus(cfManifestFiles)
598654 }
599655
@@ -628,7 +684,7 @@ class CheckerFrameworkPlugin @Inject constructor() : Plugin<Project> {
628684
629685 /* * Provides extraJavacArgs to the compiler, if the Checker Framework is enabled. */
630686 internal class CheckerFrameworkCompilerArgumentProvider (
631- @get:Input val extraJavacArgs : Provider <List <String >>
687+ @get:Input @get:Optional val extraJavacArgs : Provider <List <String >>
632688 ) : CommandLineArgumentProvider {
633689 override fun asArguments (): Iterable <String ?> {
634690 return extraJavacArgs.getOrElse(emptyList())
0 commit comments