Skip to content

Commit 8ab3129

Browse files
committed
Merge ../checker-framework-gradle-plugin-branch-config-cache-6 into config-cache-7
2 parents 00cb75b + f7a924f commit 8ab3129

5 files changed

Lines changed: 173 additions & 30 deletions

File tree

src/functionalTest/kotlin/org/checkerframework/plugin/gradle/CFGroovyPluginFunctionalTest.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,29 @@ class CFGroovyPluginFunctionalTest : GroovyPluginFunctionalTest() {
104104
assertThat(result.output).doesNotContain(NULLNESS_FAILURE)
105105
}
106106

107+
@Test
108+
fun `test null extraJavacArgs`() {
109+
buildFile.appendText(
110+
"""
111+
checkerFramework {
112+
version = "$TEST_CF_VERSION"
113+
checkers = ["org.checkerframework.checker.nullness.NullnessChecker"]
114+
extraJavacArgs = null
115+
}
116+
"""
117+
.trimIndent()
118+
)
119+
// given
120+
testProjectDir.writeNullnessFailure()
121+
122+
// when
123+
val result = testProjectDir.buildWithArgsAndFail("compileJava")
124+
125+
// then setting extraJavacArgs to null means the same thing as setting it to an empty list.
126+
assertThat(result.task(":compileJava")?.outcome).isEqualTo(TaskOutcome.FAILED)
127+
assertThat(result.output).contains(NULLNESS_FAILURE)
128+
}
129+
107130
@Test
108131
fun `test explicit processor added in afterEvaluate`() {
109132
buildFile.appendText(
@@ -142,7 +165,7 @@ class CFGroovyPluginFunctionalTest : GroovyPluginFunctionalTest() {
142165
checkerFramework {
143166
version = "$TEST_CF_VERSION"
144167
checkers = ["org.checkerframework.checker.tainting.TaintingChecker"]
145-
extraJavacArgs = ["-Aversion", "-Afilenames"]
168+
extraJavacArgs = ["-Anomsgtext", "-Afilenames"]
146169
}
147170
afterEvaluate {
148171
compileJava {
@@ -163,7 +186,7 @@ class CFGroovyPluginFunctionalTest : GroovyPluginFunctionalTest() {
163186
// checkers exchanged, both checkers run.
164187
assertThat(result.task(":compileJava")?.outcome).isEqualTo(TaskOutcome.FAILED)
165188
assertThat(result.output).contains("Note: NullnessChecker is type-checking")
166-
assertThat(result.output).contains(TAINTING_FAILURE_MESSAGE)
189+
assertThat(result.output).contains(TAINTING_FAILURE)
167190
}
168191

169192
@Test

src/functionalTest/kotlin/org/checkerframework/plugin/gradle/CFPluginFunctionalTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,37 @@ class CfPluginFunctionalTest : KotlinPluginFunctionalTest() {
233233
assertThat(result.task(":compileJava")?.outcome).isEqualTo(TaskOutcome.FAILED)
234234
}
235235

236+
@Test
237+
fun `test explicit processor added in a doFirst action`() {
238+
buildFile.appendText(
239+
"""
240+
configure<CheckerFrameworkExtension> {
241+
version = "$TEST_CF_VERSION"
242+
extraJavacArgs = listOf("-Anomsgtext","-Afilenames")
243+
checkers = listOf("org.checkerframework.checker.tainting.TaintingChecker")
244+
}
245+
tasks.named<JavaCompile>("compileJava") {
246+
doFirst {
247+
options.compilerArgs.add("-processor")
248+
options.compilerArgs.add("org.checkerframework.checker.nullness.NullnessChecker")
249+
}
250+
}
251+
"""
252+
.trimIndent()
253+
)
254+
// given
255+
testProjectDir.writeTaintingFailure()
256+
257+
// when
258+
val result = testProjectDir.buildWithArgsAndFail("compileJava")
259+
260+
// then the checkers are added to the -processor argument, even though the user added that
261+
// argument in a doFirst action rather than while configuring the task.
262+
assertThat(result.output).contains("Note: NullnessChecker is type-checking")
263+
assertThat(result.output).contains(TAINTING_FAILURE)
264+
assertThat(result.task(":compileJava")?.outcome).isEqualTo(TaskOutcome.FAILED)
265+
}
266+
236267
@Disabled("This works with Groovy but not Kotlin.")
237268
@Test
238269
fun `test disabling CF for some task`() {

src/functionalTest/kotlin/org/checkerframework/plugin/gradle/Fixtures.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,6 @@ fun File.writeNullnessFailure() {
7979
/** The Tainting Checker's error, in the "-Anomsgtext" format that omits the message text. */
8080
const val TAINTING_FAILURE = "Failure2Checkers.java:8: error: (argument)"
8181

82-
/** The Tainting Checker's error, in the default format that includes the message text. */
83-
const val TAINTING_FAILURE_MESSAGE = "Failure2Checkers.java:8: error: [argument]"
84-
8582
fun File.writeTaintingFailure() {
8683
File(this.resolve("src/main/java/test").apply { mkdirs() }, "Failure2Checkers.java").apply {
8784
createNewFile()

src/functionalTest/kotlin/org/checkerframework/plugin/gradle/OtherPluginsFunctionalTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,42 @@ class OtherPluginsFunctionalTest : KotlinPluginFunctionalTest() {
169169
.contains("Foo.java:12: error: [assignment] incompatible types in assignment.")
170170
}
171171

172+
@Test
173+
fun `test disabling CF for the delombok task only`() {
174+
buildFile.appendText(
175+
"""
176+
plugins {
177+
`java-library`
178+
id("org.checkerframework")
179+
id("io.freefair.lombok").version("9.2.0")
180+
}
181+
182+
configure<CheckerFrameworkExtension> {
183+
version = "$TEST_CF_VERSION"
184+
checkers = listOf("org.checkerframework.checker.nullness.NullnessChecker")
185+
extraJavacArgs = listOf("-Aversion")
186+
}
187+
tasks.named<JavaCompile>("checkDelombokCompileJava") {
188+
val cfOptions =
189+
(options as ExtensionAware).extensions.getByName("checkerFrameworkCompile")
190+
as CheckerFrameworkCompileExtension
191+
cfOptions.enabled.set(false)
192+
}
193+
"""
194+
.trimIndent()
195+
)
196+
// given
197+
testProjectDir.writeLombokExample()
198+
199+
// when
200+
val result = testProjectDir.buildWithArgs("checkDelombokCompileJava")
201+
202+
// then the task does not run at all, as the user asked: running the Checker Framework on the
203+
// delomboked source code is its only purpose.
204+
assertThat(result.task(":checkDelombokCompileJava")?.outcome).isEqualTo(TaskOutcome.SKIPPED)
205+
assertThat(result.output).doesNotContain("error:")
206+
}
207+
172208
@Test
173209
fun `test errorprone latest`() {
174210
val majorVersion = Runtime.version().feature()

src/main/kotlin/org/checkerframework/plugin/gradle/CheckerFrameworkPlugin.kt

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import org.gradle.api.plugins.ExtensionAware
1919
import org.gradle.api.plugins.JavaBasePlugin
2020
import org.gradle.api.plugins.JavaPluginExtension
2121
import org.gradle.api.provider.ListProperty
22+
import org.gradle.api.provider.Property
2223
import org.gradle.api.provider.Provider
24+
import org.gradle.api.specs.Spec
2325
import org.gradle.api.tasks.Input
26+
import org.gradle.api.tasks.Optional
2427
import org.gradle.api.tasks.PathSensitivity
2528
import org.gradle.api.tasks.SourceSet
2629
import 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

Comments
 (0)