Skip to content

Commit

Permalink
Add support for -XepOpt:NullAway:OnlyNullMarked
Browse files Browse the repository at this point in the history
  • Loading branch information
tbroyer committed Jan 12, 2025
1 parent 493aafe commit a4032ec
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 3 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -149,6 +208,7 @@ class NullAwayPluginIntegrationTest {
tasks.withType<JavaCompile>().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")
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Boolean>()

/**
* The list of packages that should be considered properly annotated according to the NullAway convention.
*/
Expand Down
14 changes: 13 additions & 1 deletion src/main/kotlin/net/ltgt/gradle/nullaway/NullAwayOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,25 @@ open class NullAwayOptions internal constructor(
*/
@get:Input val severity = objectFactory.property<CheckSeverity>().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<Boolean>().convention(nullawayExtension.onlyNullMarked)

/**
* The list of packages that should be considered properly annotated according to the NullAway convention; maps to `-XepOpt:NullAway:AnnotatedPackages`.
*
* This can be used to add to or override the [`annotatedPackages`][NullAwayExtension.annotatedPackages] at the project level.
*
* Defaults to the [list configured at the project-level][NullAwayExtension.annotatedPackages].
*/
@get:Input val annotatedPackages = objectFactory.listProperty<String>().convention(nullawayExtension.annotatedPackages)
@get:Input @get:Optional
val annotatedPackages = objectFactory.listProperty<String>().convention(nullawayExtension.annotatedPackages)

/** A list of subpackages to be excluded from the [annotatedPackages] list; maps to `-XepOpt:NullAway:UnannotatedSubPackages`. */
@get:Input @get:Optional
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit a4032ec

Please sign in to comment.