-
Notifications
You must be signed in to change notification settings - Fork 301
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Better
@MonotonicNonNull
support (#1149)
Fixes #1148 We add explicit support for any annotation named `@MonotonicNonNull` and add our own version of the annotation to our annotations package. The main additional support is that we now reason that once assigned a non-null value, `@MonotonicNull` fields remain non-null when accessed from subsequent lambdas, even if the lambdas are invoked asynchronously.
- Loading branch information
Showing
8 changed files
with
279 additions
and
10 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
annotations/src/main/java/com/uber/nullaway/annotations/MonotonicNonNull.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.uber.nullaway.annotations; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* Indicates that once the field becomes non-null, it never becomes null again. Inspired by the | ||
* identically-named annotation from the Checker Framework. A {@code @MonotonicNonNull} field can | ||
* only be assigned non-null values. The key reason to use this annotation with NullAway is to | ||
* enable reasoning about field non-nullness in nested lambdas / anonymous classes, e.g.: | ||
* | ||
* <pre> | ||
* class Foo { | ||
* {@literal @}MonotonicNonNull Object theField; | ||
* void foo() { | ||
* theField = new Object(); | ||
* Runnable r = () -> { | ||
* // No error, NullAway knows theField is non-null after assignment | ||
* theField.toString(); | ||
* } | ||
* } | ||
* } | ||
* </pre> | ||
*/ | ||
@Retention(RetentionPolicy.CLASS) | ||
@Target(ElementType.FIELD) | ||
public @interface MonotonicNonNull {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
nullaway/src/test/java/com/uber/nullaway/MonotonicNonNullTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package com.uber.nullaway; | ||
|
||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.junit.runners.JUnit4; | ||
|
||
@RunWith(JUnit4.class) | ||
public class MonotonicNonNullTests extends NullAwayTestsBase { | ||
|
||
@Test | ||
public void initializerExpression() { | ||
defaultCompilationHelper | ||
.addSourceLines( | ||
"Test.java", | ||
"package com.uber;", | ||
"import com.uber.nullaway.annotations.MonotonicNonNull;", | ||
"class Test {", | ||
" // this is fine; same as implicit initialization", | ||
" @MonotonicNonNull Object f1 = null;", | ||
" @MonotonicNonNull Object f2 = new Object();", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
public void assignments() { | ||
defaultCompilationHelper | ||
.addSourceLines( | ||
"Test.java", | ||
"package com.uber;", | ||
"import com.uber.nullaway.annotations.MonotonicNonNull;", | ||
"class Test {", | ||
" @MonotonicNonNull Object f1;", | ||
" void testPositive() {", | ||
" // BUG: Diagnostic contains: assigning @Nullable expression", | ||
" f1 = null;", | ||
" }", | ||
" void testNegative() {", | ||
" f1 = new Object();", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
public void lambdas() { | ||
defaultCompilationHelper | ||
.addSourceLines( | ||
"Test.java", | ||
"package com.uber;", | ||
"import com.uber.nullaway.annotations.MonotonicNonNull;", | ||
"class Test {", | ||
" @MonotonicNonNull Object f1;", | ||
" void testPositive() {", | ||
" Runnable r = () -> {", | ||
" // BUG: Diagnostic contains: dereferenced expression f1", | ||
" f1.toString();", | ||
" };", | ||
" }", | ||
" void testNegative() {", | ||
" f1 = new Object();", | ||
" Runnable r = () -> {", | ||
" f1.toString();", | ||
" };", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
public void anonymousClasses() { | ||
defaultCompilationHelper | ||
.addSourceLines( | ||
"Test.java", | ||
"package com.uber;", | ||
"import com.uber.nullaway.annotations.MonotonicNonNull;", | ||
"class Test {", | ||
" @MonotonicNonNull Object f1;", | ||
" void testPositive() {", | ||
" Runnable r = new Runnable() {", | ||
" @Override", | ||
" public void run() {", | ||
" // BUG: Diagnostic contains: dereferenced expression f1", | ||
" f1.toString();", | ||
" }", | ||
" };", | ||
" }", | ||
" void testNegative() {", | ||
" f1 = new Object();", | ||
" Runnable r = new Runnable() {", | ||
" @Override", | ||
" public void run() {", | ||
" f1.toString();", | ||
" }", | ||
" };", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
public void nestedObjects() { | ||
defaultCompilationHelper | ||
.addSourceLines( | ||
"Test.java", | ||
"package com.uber;", | ||
"import com.uber.nullaway.annotations.MonotonicNonNull;", | ||
"import org.jspecify.annotations.Nullable;", | ||
"class Test {", | ||
" class Foo {", | ||
" @MonotonicNonNull Object x;", | ||
" }", | ||
" final Foo f1 = new Foo();", | ||
" Foo f2 = new Foo(); // not final", | ||
" @Nullable Foo f3;", | ||
" void testPositive1() {", | ||
" f2.x = new Object();", | ||
" Runnable r = () -> {", | ||
" // report a bug since f2 may be overwritten", | ||
" // BUG: Diagnostic contains: dereferenced expression f2.x", | ||
" f2.x.toString();", | ||
" };", | ||
" }", | ||
" void testPositive2() {", | ||
" f3 = new Foo();", | ||
" f3.x = new Object();", | ||
" Runnable r = () -> {", | ||
" // report a bug since f3 may be overwritten", | ||
" // BUG: Diagnostic contains: dereferenced expression f3.x", | ||
" f3.x.toString();", | ||
" };", | ||
" }", | ||
" void testNegative() {", | ||
" f1.x = new Object();", | ||
" Runnable r = () -> {", | ||
" f1.x.toString();", | ||
" };", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
|
||
@Test | ||
public void accessPathsWithMethodCalls() { | ||
defaultCompilationHelper | ||
.addSourceLines( | ||
"Test.java", | ||
"package com.uber;", | ||
"import com.uber.nullaway.annotations.MonotonicNonNull;", | ||
"import org.jspecify.annotations.Nullable;", | ||
"class Test {", | ||
" class Foo {", | ||
" @MonotonicNonNull Object x;", | ||
" }", | ||
" Foo f1 = new Foo();", | ||
" final Foo getF1() {", | ||
" return f1;", | ||
" }", | ||
" final @Nullable Foo getOther() {", | ||
" return null;", | ||
" }", | ||
" void testPositive1() {", | ||
" getF1().x = new Object();", | ||
" Runnable r = () -> {", | ||
" // BUG: Diagnostic contains: dereferenced expression", | ||
" getF1().x.toString();", | ||
" };", | ||
" }", | ||
" void testPositive2() {", | ||
" if (getOther() != null) {", | ||
" getOther().x = new Object();", | ||
" Runnable r1 = () -> {", | ||
" // getOther() should be treated as @Nullable in the lambda", | ||
" // BUG: Diagnostic contains: dereferenced expression", | ||
" getOther().toString();", | ||
" };", | ||
" Runnable r2 = () -> {", | ||
" // BUG: Diagnostic contains: dereferenced expression", | ||
" getOther().x.toString();", | ||
" };", | ||
" }", | ||
" }", | ||
"}") | ||
.doTest(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters