From a4032ec3e40dd3e89aba008140e78b5e79826c74 Mon Sep 17 00:00:00 2001 From: Thomas Broyer Date: Sun, 12 Jan 2025 17:50:47 +0100 Subject: [PATCH] Add support for -XepOpt:NullAway:OnlyNullMarked --- README.md | 3 + .../net/ltgt/gradle/nullaway/Fixtures.kt | 5 +- .../nullaway/GroovyDslIntegrationTest.kt | 1 + .../nullaway/NullAwayPluginIntegrationTest.kt | 62 ++++++++++++++++++- .../ltgt/gradle/nullaway/NullAwayExtension.kt | 5 ++ .../ltgt/gradle/nullaway/NullAwayOptions.kt | 14 ++++- 6 files changed, 87 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3822b55..e4388fc 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ and finally configure NullAway's annotated packages: ```gradle nullaway { annotatedPackages.add("net.ltgt") + // OR, starting with NullAway 0.12.3 and if you use JSpecify @NullMarked: + onlyNullMarked = true } ``` @@ -77,6 +79,7 @@ Each property (except for `severity`) maps to an `-XepOpt:NullAway:[propertyName | Property | Description | :------- | :---------- | `severity` | The check severity. Almost equivalent to `options.errorprone.check("NullAway", severity)` (NullAway won't actually appear in `options.errorprone.checks`). Can be set to `CheckSeverity.OFF` to disable NullAway. +| `onlyNullMarked` | Indicates that the `annotatedPackages` flag has been deliberately omitted, and that NullAway can proceed with only treating `@NullMarked` code as annotated, in accordance with the JSpecify specification. | `annotatedPackages` | The list of packages that should be considered properly annotated according to the NullAway convention. This can be used to add to or override the `annotatedPackages` at the project level. | `unannotatedSubPackages` | A list of subpackages to be excluded from the AnnotatedPackages list. | `unannotatedClasses` | A list of classes within annotated packages that should be treated as unannotated. diff --git a/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/Fixtures.kt b/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/Fixtures.kt index 76103f6..c46ede0 100644 --- a/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/Fixtures.kt +++ b/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/Fixtures.kt @@ -21,9 +21,12 @@ val errorproneVersion = val nullawayVersion = when { testJavaVersion < JavaVersion.VERSION_11 -> "0.10.26" - else -> "0.12.2" + else -> "0.12.3" } +// XXX: same test (reversed) as in nullawayVersion above +val nullawaySupportsOnlyNullMarked = testJavaVersion >= JavaVersion.VERSION_11 + const val FAILURE_SOURCE_COMPILATION_ERROR = "Failure.java:8: warning: [NullAway]" fun File.writeSuccessSource() { diff --git a/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/GroovyDslIntegrationTest.kt b/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/GroovyDslIntegrationTest.kt index 92a2185..4dae00a 100644 --- a/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/GroovyDslIntegrationTest.kt +++ b/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/GroovyDslIntegrationTest.kt @@ -102,6 +102,7 @@ class GroovyDslIntegrationTest { tasks.withType(JavaCompile).configureEach { options.errorprone.nullaway { severity = CheckSeverity.DEFAULT + ${if (nullawaySupportsOnlyNullMarked) "onlyNullMarked = false" else ""} annotatedPackages = project.nullaway.annotatedPackages unannotatedSubPackages = ["test.dummy"] unannotatedClasses = ["test.Unannotated"] diff --git a/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/NullAwayPluginIntegrationTest.kt b/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/NullAwayPluginIntegrationTest.kt index 259411f..a867c7f 100644 --- a/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/NullAwayPluginIntegrationTest.kt +++ b/src/integrationTest/kotlin/net/ltgt/gradle/nullaway/NullAwayPluginIntegrationTest.kt @@ -92,7 +92,7 @@ class NullAwayPluginIntegrationTest { // then assertThat(result.task(":compileJava")?.outcome).isEqualTo(TaskOutcome.FAILED) - assertThat(result.output).contains("Must specify annotated packages, using the -XepOpt:NullAway:AnnotatedPackages=[...] flag.") + assertThat(result.output).contains(" specify annotated packages, using the -XepOpt:NullAway:AnnotatedPackages=[...] flag") } @Test @@ -120,6 +120,65 @@ class NullAwayPluginIntegrationTest { assertThat(result.output).contains(FAILURE_SOURCE_COMPILATION_ERROR) } + @Test + fun `only null-marked option, false positive if unannotated`() { + assume().that(nullawaySupportsOnlyNullMarked).isTrue() + // given + buildFile.appendText( + """ + + nullaway { + onlyNullMarked.set(true) + annotatedPackages.empty() + } + """.trimIndent(), + ) + testProjectDir.writeFailureSource() + + // when + val result = testProjectDir.buildWithArgs(":compileJava") + + // then + assertThat(result.task(":compileJava")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + } + + @Test + fun `only null-marked option, compilation fails`() { + assume().that(nullawaySupportsOnlyNullMarked).isTrue() + // given + buildFile.appendText( + """ + + nullaway { + onlyNullMarked.set(true) + annotatedPackages.empty() + } + dependencies { + implementation("org.jspecify:jspecify:1.0.0") + } + """.trimIndent(), + ) + testProjectDir.writeFailureSource() + File(testProjectDir.resolve("src/main/java/test").apply { mkdirs() }, "package-info.java").apply { + createNewFile() + writeText( + """ + @NullMarked + package test; + + import org.jspecify.annotations.NullMarked; + """.trimIndent(), + ) + } + + // when + val result = testProjectDir.buildWithArgsAndFail(":compileJava") + + // then + assertThat(result.task(":compileJava")?.outcome).isEqualTo(TaskOutcome.FAILED) + assertThat(result.output).contains(FAILURE_SOURCE_COMPILATION_ERROR) + } + @Test fun `can disable nullaway`() { // given @@ -149,6 +208,7 @@ class NullAwayPluginIntegrationTest { tasks.withType().configureEach { options.errorprone.nullaway { severity.set(CheckSeverity.DEFAULT) + ${if (nullawaySupportsOnlyNullMarked) "onlyNullMarked.set(false)" else ""} unannotatedSubPackages.add("test.dummy") unannotatedClasses.add("test.Unannotated") knownInitializers.add("com.foo.Bar.method") diff --git a/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayExtension.kt b/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayExtension.kt index 777411a..062a7fe 100644 --- a/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayExtension.kt +++ b/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayExtension.kt @@ -8,6 +8,11 @@ import org.gradle.kotlin.dsl.* open class NullAwayExtension internal constructor( objectFactory: ObjectFactory, ) { + /** + * Indicates that the [annotatedPackages] flag has been deliberately omitted, and that NullAway can proceed with only treating `@NullMarked` code as annotated, in accordance with the JSpecify specification. + */ + val onlyNullMarked = objectFactory.property() + /** * The list of packages that should be considered properly annotated according to the NullAway convention. */ diff --git a/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayOptions.kt b/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayOptions.kt index 945aea6..3058892 100644 --- a/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayOptions.kt +++ b/src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayOptions.kt @@ -28,6 +28,16 @@ open class NullAwayOptions internal constructor( */ @get:Input val severity = objectFactory.property().convention(CheckSeverity.DEFAULT) + /** + * Indicates that the [annotatedPackages] flag has been deliberately omitted, and that NullAway can proceed with only treating `@NullMarked` code as annotated, in accordance with the JSpecify specification; maps to `-XepOpt:NullAway:OnlyNullMarked`. + * + * If this option is passed, then [annotatedPackages] must be empty. Note that even if this flag is omitted (and [annotatedPackages] is non-empty), any `@NullMarked` code will still be treated as annotated. + * + * Defaults to the [value configured at the project-level][NullAwayExtension.onlyNullMarked] + */ + @get:Input @get:Optional + val onlyNullMarked = objectFactory.property().convention(nullawayExtension.onlyNullMarked) + /** * The list of packages that should be considered properly annotated according to the NullAway convention; maps to `-XepOpt:NullAway:AnnotatedPackages`. * @@ -35,7 +45,8 @@ open class NullAwayOptions internal constructor( * * Defaults to the [list configured at the project-level][NullAwayExtension.annotatedPackages]. */ - @get:Input val annotatedPackages = objectFactory.listProperty().convention(nullawayExtension.annotatedPackages) + @get:Input @get:Optional + val annotatedPackages = objectFactory.listProperty().convention(nullawayExtension.annotatedPackages) /** A list of subpackages to be excluded from the [annotatedPackages] list; maps to `-XepOpt:NullAway:UnannotatedSubPackages`. */ @get:Input @get:Optional @@ -213,6 +224,7 @@ open class NullAwayOptions internal constructor( sequenceOf( "-Xep:NullAway${severity.getOrElse(CheckSeverity.DEFAULT).asArg}", listOption("AnnotatedPackages", annotatedPackages), + booleanOption("OnlyNullMarked", onlyNullMarked), listOption("UnannotatedSubPackages", unannotatedSubPackages), listOption("UnannotatedClasses", unannotatedClasses), listOption("KnownInitializers", knownInitializers),