blob: dcdab86776ac13af1c1e9ee56cfecb27ed449179 [file] [log] [blame]
/*
* Copyright (c) 2017 Uber Technologies, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.uber.nullaway;
import com.google.errorprone.CompilationTestHelper;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link com.uber.nullaway.NullAway}. */
@RunWith(JUnit4.class)
@SuppressWarnings("CheckTestExtendsBaseClass")
public class NullAwayTest {
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
private CompilationTestHelper compilationHelper;
@Before
public void setup() {
compilationHelper = CompilationTestHelper.newInstance(NullAway.class, getClass());
compilationHelper.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:KnownInitializers="
+ "com.uber.nullaway.testdata.CheckFieldInitNegativeCases.Super.doInit,"
+ "com.uber.nullaway.testdata.CheckFieldInitNegativeCases"
+ ".SuperInterface.doInit2",
"-XepOpt:NullAway:AnnotatedPackages=com.uber,com.ubercab,io.reactivex",
// We give the following in Regexp format to test that support
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated",
"-XepOpt:NullAway:ExcludedClasses="
+ "com.uber.nullaway.testdata.Shape_Stuff,"
+ "com.uber.nullaway.testdata.excluded",
"-XepOpt:NullAway:ExcludedClassAnnotations=com.uber.nullaway.testdata.TestAnnot",
"-XepOpt:NullAway:CastToNonNullMethod=com.uber.nullaway.testdata.Util.castToNonNull",
"-XepOpt:NullAway:ExternalInitAnnotations=com.uber.ExternalInit",
"-XepOpt:NullAway:ExcludedFieldAnnotations=com.uber.ExternalFieldInit"));
}
@Test
public void coreNullabilityPositiveCases() {
compilationHelper.addSourceFile("NullAwayPositiveCases.java").doTest();
}
@Test
public void nullabilityAnonymousClass() {
compilationHelper.addSourceFile("NullAwayAnonymousClass.java").doTest();
}
@Test
public void coreNullabilityNegativeCases() {
compilationHelper
.addSourceFile("NullAwayNegativeCases.java")
.addSourceFile("OtherStuff.java")
.addSourceFile("TestAnnot.java")
.addSourceFile("unannotated/UnannotatedClass.java")
.doTest();
}
@Test
public void coreNullabilitySkipClass() {
compilationHelper.addSourceFile("Shape_Stuff.java").doTest();
compilationHelper.addSourceFile("excluded/Shape_Stuff2.java").doTest();
compilationHelper.addSourceFile("AnnotatedClass.java").addSourceFile("TestAnnot.java").doTest();
}
@Test
public void skipNestedClass() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:ExcludedClassAnnotations=com.uber.lib.MyExcluded"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"public class Test {",
" @com.uber.lib.MyExcluded",
" static class Inner {",
" @Nullable",
" static Object foo() {",
" Object x = null; x.toString();",
" return x;",
" }",
" }",
" static void bar() {",
" // BUG: Diagnostic contains: dereferenced expression Inner.foo()",
" Inner.foo().toString();",
" }",
"}")
.doTest();
}
@Test
public void coreNullabilitySkipPackage() {
compilationHelper.addSourceFile("unannotated/UnannotatedClass.java").doTest();
}
@Test
public void coreNullabilityNativeModels() {
compilationHelper
.addSourceFile("NullAwayNativeModels.java")
.addSourceFile("androidstubs/WebView.java")
.addSourceFile("androidstubs/TextUtils.java")
.doTest();
}
@Test
public void initFieldPositiveCases() {
compilationHelper.addSourceFile("CheckFieldInitPositiveCases.java").doTest();
}
@Test
public void initFieldNegativeCases() {
compilationHelper.addSourceFile("CheckFieldInitNegativeCases.java").doTest();
}
@Test
public void assertSupportPositiveCases() {
compilationHelper.addSourceFile("CheckAssertSupportPositiveCases.java").doTest();
}
@Test
public void assertSupportNegativeCases() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:AssertsEnabled=true"))
.addSourceFile("CheckAssertSupportNegativeCases.java")
.doTest();
}
@Test
public void java8PositiveCases() {
compilationHelper.addSourceFile("NullAwayJava8PositiveCases.java").doTest();
}
@Test
public void java8NegativeCases() {
compilationHelper.addSourceFile("NullAwayJava8NegativeCases.java").doTest();
}
@Test
public void rxSupportPositiveCases() {
compilationHelper.addSourceFile("NullAwayRxSupportPositiveCases.java").doTest();
}
@Test
public void rxSupportNegativeCases() {
compilationHelper.addSourceFile("NullAwayRxSupportNegativeCases.java").doTest();
}
@Test
public void functionalMethodSuperInterface() {
compilationHelper.addSourceFile("NullAwaySuperFunctionalInterface.java").doTest();
}
@Test
public void functionalMethodOverrideSuperInterface() {
compilationHelper.addSourceFile("NullAwayOverrideFunctionalInterfaces.java").doTest();
}
@Test
public void readBeforeInitPositiveCases() {
compilationHelper
.addSourceFile("ReadBeforeInitPositiveCases.java")
.addSourceFile("Util.java")
.doTest();
}
@Test
public void readBeforeInitNegativeCases() {
compilationHelper
.addSourceFile("ReadBeforeInitNegativeCases.java")
.addSourceFile("Util.java")
.doTest();
}
@Test
public void tryFinallySupport() {
compilationHelper.addSourceFile("NullAwayTryFinallyCases.java").doTest();
}
@Test
public void externalInitSupport() {
compilationHelper
.addSourceLines(
"ExternalInit.java",
"package com.uber;",
"@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS)",
"public @interface ExternalInit {}")
.addSourceLines(
"Test.java",
"package com.uber;",
"@ExternalInit",
"class Test {",
" Object f;",
// no error here due to external init
" public Test() {}",
" // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field",
" public Test(int x) {}",
"}")
.addSourceLines(
"Test2.java",
"package com.uber;",
"@ExternalInit",
"class Test2 {",
// no error here due to external init
" Object f;",
"}")
.addSourceLines(
"Test3.java",
"package com.uber;",
"@ExternalInit",
"class Test3 {",
" Object f;",
" // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field",
" public Test3(int x) {}",
"}")
.doTest();
}
@Test
public void externalInitexternalInitSupportFields() {
compilationHelper
.addSourceLines(
"ExternalFieldInit.java",
"package com.uber;",
"@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS)",
"public @interface ExternalFieldInit {}")
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" @ExternalFieldInit Object f;",
// no error here due to external init
" public Test() {}",
// no error here due to external init
" public Test(int x) {}",
"}")
.addSourceLines(
"Test2.java",
"package com.uber;",
"class Test2 {",
// no error here due to external init
" @ExternalFieldInit Object f;",
"}")
.addSourceLines(
"Test3.java",
"package com.uber;",
"class Test3 {",
" @ExternalFieldInit Object f;",
// no error here due to external init
" @ExternalFieldInit", // See GitHub#184
" public Test3() {}",
// no error here due to external init
" @ExternalFieldInit", // See GitHub#184
" public Test3(int x) {}",
"}")
.doTest();
}
@Test
public void generatedAsUnannotated() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:TreatGeneratedAsUnannotated=true"))
.addSourceLines(
"Generated.java",
"package com.uber;",
"@javax.annotation.Generated(\"foo\")",
"public class Generated { public void takeObj(Object o) {} }")
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" void foo() { (new Generated()).takeObj(null); }",
"}")
.doTest();
}
@Test
public void generatedAsUnannotatedPlusRestrictive() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:TreatGeneratedAsUnannotated=true",
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
.addSourceLines(
"Generated.java",
"package com.uber;",
"@javax.annotation.Generated(\"foo\")",
"public class Generated {",
" @javax.annotation.Nullable",
" public Object retNull() {",
" return null;",
" }",
"}")
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" void foo() { (new Generated()).retNull().toString(); }",
"}")
.doTest();
}
@Test
public void basicContractAnnotation() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber"))
.addSourceLines(
"NullnessChecker.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import org.jetbrains.annotations.Contract;",
"public class NullnessChecker {",
" @Contract(\"_, null -> true\")",
" static boolean isNull(boolean flag, @Nullable Object o) { return o == null; }",
" @Contract(\"null -> false\")",
" static boolean isNonNull(@Nullable Object o) { return o != null; }",
" @Contract(\"null -> fail\")",
" static void assertNonNull(@Nullable Object o) { if (o != null) throw new Error(); }",
"}")
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"class Test {",
" String test1(@Nullable Object o) {",
" return NullnessChecker.isNonNull(o) ? o.toString() : \"null\";",
" }",
" String test2(@Nullable Object o) {",
" return NullnessChecker.isNull(false, o) ? \"null\" : o.toString();",
" }",
" String test3(@Nullable Object o) {",
" NullnessChecker.assertNonNull(o);",
" return o.toString();",
" }",
"}")
.doTest();
}
@Test
public void impliesNonNullContractAnnotation() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber"))
.addSourceLines(
"NullnessChecker.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import org.jetbrains.annotations.Contract;",
"public class NullnessChecker {",
" @Contract(\"!null -> !null\")",
" static @Nullable Object noOp(@Nullable Object o) { return o; }",
"}")
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"class Test {",
" String test1(@Nullable Object o1) {",
" // BUG: Diagnostic contains: dereferenced expression",
" return NullnessChecker.noOp(o1).toString();",
" }",
" String test2(Object o2) {",
" return NullnessChecker.noOp(o2).toString();",
" }",
"}")
.doTest();
}
@Test
public void malformedContractAnnotations() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import org.jetbrains.annotations.Contract;",
"class Test {",
" @Contract(\"!null -> -> !null\")",
" static @Nullable Object foo(@Nullable Object o) { return o; }",
" @Contract(\"!null -> !null\")",
" static @Nullable Object bar(@Nullable Object o, String s) { return o; }",
" @Contract(\"jabberwocky -> !null\")",
" static @Nullable Object baz(@Nullable Object o) { return o; }",
// We don't care as long as nobody calls the method:
" @Contract(\"!null -> -> !null\")",
" static @Nullable Object dontcare(@Nullable Object o) { return o; }",
" static Object test1() {",
" // BUG: Diagnostic contains: Invalid @Contract annotation",
" return foo(null);",
" }",
" static Object test2() {",
" // BUG: Diagnostic contains: Invalid @Contract annotation",
" return bar(null, \"\");",
" }",
" static Object test3() {",
" // BUG: Diagnostic contains: Invalid @Contract annotation",
" return baz(null);",
" }",
"}")
.doTest();
}
@Test
public void contractNonVarArg() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber"))
.addSourceLines(
"NullnessChecker.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import org.jetbrains.annotations.Contract;",
"public class NullnessChecker {",
" @Contract(\"null -> fail\")",
" static void assertNonNull(@Nullable Object o) { if (o != null) throw new Error(); }",
"}")
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"class Test {",
" void test(java.util.function.Function<Object, Object> fun) {",
" NullnessChecker.assertNonNull(fun.apply(new Object()));",
" }",
"}")
.doTest();
}
@Test
public void testEnumInit() {
compilationHelper
.addSourceLines(
"SomeEnum.java",
"package com.uber;",
"import java.util.Random;",
"enum SomeEnum {",
" FOO, BAR;",
" final Object o;",
" final Object p;",
" private SomeEnum() {",
" this.o = new Object();",
" this.p = new Object();",
" this.o.equals(this.p);",
" }",
"}")
.doTest();
}
@Test
public void testGenericAnonymousInner() {
compilationHelper
.addSourceLines(
"GenericSuper.java",
"package com.uber;",
"class GenericSuper<T> {",
" T x;",
" GenericSuper(T y) {",
" this.x = y;",
" }",
"}")
.addSourceLines(
"AnonSub.java",
"package com.uber;",
"import java.util.List;",
"import javax.annotation.Nullable;",
"class AnonSub {",
" static GenericSuper<List<String>> makeSuper(List<String> list) {",
" return new GenericSuper<List<String>>(list) {};",
" }",
" static GenericSuper<List<String>> makeSuperBad(@Nullable List<String> list) {",
" // BUG: Diagnostic contains: passing @Nullable parameter 'list' where @NonNull",
" return new GenericSuper<List<String>>(list) {};",
" }",
"}")
.doTest();
}
@Test
public void testThriftIsSet() {
compilationHelper
.addSourceLines("TBase.java", "package org.apache.thrift;", "public interface TBase {}")
.addSourceLines(
"Generated.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"public class Generated implements org.apache.thrift.TBase {",
" public @Nullable Object id;",
" public boolean isFixed;",
" @Nullable public Object getId() { return this.id; }",
" // this is to ensure we don't crash on unions",
" public boolean isSet() { return false; }",
" public boolean isSetId() { return this.id != null; }",
" public boolean isFixed() { return this.isFixed; }",
" public boolean isSetIsFixed() { return false; }",
"}")
.addSourceLines(
"Client.java",
"package com.uber;",
"public class Client {",
" public void testNeg(Generated g) {",
" if (g.isSetId()) {",
" g.getId().toString();",
" g.id.hashCode();",
" }",
" if (g.isSetIsFixed()) {",
" g.isFixed();",
" }",
" if (g.isSet()) {}",
" }",
" public void testPos(Generated g) {",
" if (!g.isSetId()) {",
" // BUG: Diagnostic contains: dereferenced expression g.getId() is @Nullable",
" g.getId().hashCode();",
" } else {",
" g.id.toString();",
" }",
" java.util.List<Generated> l = new java.util.ArrayList<>();",
" if (l.get(0).isSetId()) {",
" // BUG: Diagnostic contains: dereferenced expression l.get(0).getId()",
" l.get(0).getId().hashCode();",
" }",
" }",
"}")
.doTest();
}
@Test
public void testThriftIsSetWithGenerics() {
compilationHelper
.addSourceLines(
"TBase.java", "package org.apache.thrift;", "public interface TBase<T, F> {}")
.addSourceLines(
"Generated.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"public class Generated implements org.apache.thrift.TBase<String, Integer> {",
" public @Nullable Object id;",
" @Nullable public Object getId() { return this.id; }",
" public boolean isSetId() { return this.id != null; }",
"}")
.addSourceLines(
"Client.java",
"package com.uber;",
"public class Client {",
" public void testNeg(Generated g) {",
" if (!g.isSetId()) {",
" return;",
" }",
" Object x = g.getId();",
" if (x.toString() == null) return;",
" g.getId().toString();",
" g.id.hashCode();",
" }",
"}")
.doTest();
}
@Test
public void testThriftIsSetWithArg() {
compilationHelper
.addSourceLines(
"TBase.java",
"package org.apache.thrift;",
"public interface TBase {",
" boolean isSet(String fieldName);",
"}")
.addSourceLines(
"Client.java",
"package com.uber;",
"public class Client {",
" public void testNeg(org.apache.thrift.TBase tBase) {",
" if (tBase.isSet(\"Hello\")) {",
" System.out.println(\"set\");",
" }",
" }",
"}")
.doTest();
}
/** we do not have proper support for Thrift unions yet; just checks that we don't crash */
@Test
public void testThriftUnion() {
compilationHelper
.addSourceLines(
"TBase.java", "package org.apache.thrift;", "public interface TBase<T, F> {}")
.addSourceLines(
"TUnion.java",
"package org.apache.thrift;",
"public abstract class TUnion<T, F> implements TBase<T, F> {",
" protected Object value_;",
" public Object getFieldValue() {",
" return this.value_;",
" }",
"}")
.addSourceLines(
"Generated.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"public class Generated extends org.apache.thrift.TUnion<String, Integer> {",
" public Object getId() { return getFieldValue(); }",
" public boolean isSetId() { return true; }",
"}")
.addSourceLines(
"Client.java",
"package com.uber;",
"public class Client {",
" public void testNeg(Generated g) {",
" if (!g.isSetId()) {",
" return;",
" }",
" Object x = g.getId();",
" if (x.toString() == null) return;",
" }",
"}")
.doTest();
}
@Test
public void erasedIterator() {
// just checking for crash
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import java.util.*;",
"class Test {",
" static class Foo implements Iterable {",
" public Iterator iterator() {",
" return new Iterator() {",
" @Override",
" public boolean hasNext() {",
" return false;",
" }",
" @Override",
" public Iterator next() {",
" throw new NoSuchElementException();",
" }",
" };",
" }",
" }",
" static void testErasedIterator(Foo foo) {",
" for (Object x : foo) {",
" x.hashCode();",
" }",
" }",
"}")
.doTest();
}
@Test
public void compoundAssignment() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" static void assignments() {",
" String x = null; x += \"hello\";",
" // BUG: Diagnostic contains: unboxing of a @Nullable value",
" Integer y = null; y += 3;",
" // BUG: Diagnostic contains: unboxing of a @Nullable value",
" boolean b = false; Boolean c = null; b |= c;",
" }",
" static Integer returnCompound() {",
" Integer z = 7;",
" return (z += 10);",
" }",
"}")
.doTest();
}
@Test
public void arrayIndexUnbox() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" static void indexUnbox() {",
" Integer x = null; int[] fizz = { 0, 1 };",
" // BUG: Diagnostic contains: unboxing of a @Nullable value",
" int y = fizz[x];",
" }",
"}")
.doTest();
}
@Test
public void unannotatedClass() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedClasses=com.uber.UnAnnot"))
.addSourceLines(
"UnAnnot.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"public class UnAnnot {",
" @Nullable static Object retNull() { return null; }",
"}")
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"class Test {",
" @Nullable static Object nullRetSameClass() { return null; }",
" void test() {",
" UnAnnot.retNull().toString();",
// make sure other classes in the package still get analyzed
" Object x = nullRetSameClass();",
" // BUG: Diagnostic contains: dereferenced expression x is @Nullable",
" x.hashCode();",
" }",
"}")
.doTest();
}
@Test
public void cfNullableArrayField() {
compilationHelper
.addSourceLines(
"CFNullable.java",
"package com.uber;",
"import org.checkerframework.checker.nullness.qual.Nullable;",
"import java.util.List;",
"abstract class CFNullable<E> {",
" List<E> @Nullable [] table;",
"}")
.doTest();
}
@Test
public void typeUseJarReturn() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.lib.*;",
"class Test {",
" void foo(CFNullableStuff.NullableReturn r) {",
" // BUG: Diagnostic contains: dereferenced expression",
" r.get().toString();",
" }",
"}")
.doTest();
}
@Test
public void typeUseJarParam() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.lib.*;",
"class Test {",
" void foo(CFNullableStuff.NullableParam p) {",
" p.doSomething(null);",
" // BUG: Diagnostic contains: passing @Nullable parameter",
" p.doSomething2(null, new Object());",
" p.doSomething2(new Object(), null);",
" }",
"}")
.doTest();
}
@Test
public void typeUseJarField() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.lib.*;",
"class Test {",
" void foo(CFNullableStuff c) {",
" // BUG: Diagnostic contains: dereferenced expression c.f",
" c.f.toString();",
" }",
"}")
.doTest();
}
@Test
public void typeUseJarOverride() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.lib.*;",
"import org.checkerframework.checker.nullness.qual.Nullable;",
"class Test {",
" class Test1 implements CFNullableStuff.NullableReturn {",
" public @Nullable Object get() { return null; }",
" }",
" class Test2 implements CFNullableStuff.NullableParam {",
" // BUG: Diagnostic contains: parameter o is @NonNull",
" public void doSomething(Object o) {}",
" // BUG: Diagnostic contains: parameter p is @NonNull",
" public void doSomething2(Object o, Object p) {}",
" }",
" class Test3 implements CFNullableStuff.NullableParam {",
" public void doSomething(@Nullable Object o) {}",
" public void doSomething2(Object o, @Nullable Object p) {}",
" }",
"}")
.doTest();
}
@Test
public void tryWithResourcesSupport() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import java.io.BufferedReader;",
"import java.io.FileReader;",
"import java.io.IOException;",
"class Test {",
" String foo(String path, @Nullable String s, @Nullable Object o) throws IOException {",
" try (BufferedReader br = new BufferedReader(new FileReader(path))) {",
" // Code inside try-resource gets analyzed",
" // BUG: Diagnostic contains: dereferenced expression",
" o.toString();",
" s = br.readLine();",
" return s;",
" }",
" }",
"}")
.doTest();
}
@Test
public void tryWithResourcesSupportInit() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import java.io.BufferedReader;",
"import java.io.FileReader;",
"import java.io.IOException;",
"class Test {",
" private String path;",
" private String f;",
" Test(String p) throws IOException {",
" path = p;",
" try (BufferedReader br = new BufferedReader(new FileReader(path))) {",
" f = br.readLine();",
" }",
" }",
"}")
.doTest();
}
@Test
public void tryFinallySupportInit() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import java.io.BufferedReader;",
"import java.io.FileReader;",
"import java.io.IOException;",
"class Test {",
" private String path;",
" private String f;",
" Test(String p) throws IOException {",
" path = p;",
" try {",
" BufferedReader br = new BufferedReader(new FileReader(path));",
" f = br.readLine();",
" } finally {",
" f = \"DEFAULT\";",
" }",
" }",
"}")
.doTest();
}
@Test
public void supportObjectsIsNull() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import java.util.Objects;",
"import javax.annotation.Nullable;",
"class Test {",
" private void foo(@Nullable String s) {",
" if (!Objects.isNull(s)) {",
" s.toString();",
" }",
" }",
"}")
.doTest();
}
@Test
public void defaultPermissiveOnUnannotated() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=false"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import com.uber.lib.unannotated.RestrictivelyAnnotatedClass;",
"class Test {",
" Object test() {",
" // Assume methods take @Nullable, even if annotated otherwise",
" RestrictivelyAnnotatedClass.consumesObjectUnannotated(null);",
" RestrictivelyAnnotatedClass.consumesObjectNonNull(null);",
" // Ignore explict @Nullable return",
" return RestrictivelyAnnotatedClass.returnsNull();",
" }",
"}")
.doTest();
}
@Test
public void acknowledgeRestrictiveAnnotationsWhenFlagSet() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import com.uber.lib.unannotated.RestrictivelyAnnotatedClass;",
"class Test {",
" Object test() {",
" RestrictivelyAnnotatedClass.consumesObjectUnannotated(null);",
" // BUG: Diagnostic contains: @NonNull is required",
" RestrictivelyAnnotatedClass.consumesObjectNonNull(null);",
" // BUG: Diagnostic contains: @NonNull is required",
" RestrictivelyAnnotatedClass.consumesObjectNotNull(null);",
" // BUG: Diagnostic contains: returning @Nullable",
" return RestrictivelyAnnotatedClass.returnsNull();",
" }",
"}")
.doTest();
}
@Test
public void restrictivelyAnnotatedMethodsWorkWithNullnessFromDataflow() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import com.uber.lib.unannotated.RestrictivelyAnnotatedClass;",
"class Test {",
" Object test1(RestrictivelyAnnotatedClass instance) {",
" if (instance.getField() != null) {",
" return instance.getField();",
" }",
" throw new Error();",
" }",
" Object test2(RestrictivelyAnnotatedClass instance) {",
" if (instance.field != null) {",
" return instance.field;",
" }",
" throw new Error();",
" }",
"}")
.doTest();
}
@Test
public void restrictivelyAnnotatedMethodsWorkWithNullnessFromDataflow2() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import com.uber.lib.unannotated.RestrictivelyAnnotatedGenericContainer;",
"class Test {",
" String test(RestrictivelyAnnotatedGenericContainer<Integer> instance) {",
" if (instance.getField() == null) {",
" return \"\";",
" }",
" return instance.getField().toString();",
" }",
"}")
.doTest();
}
@Test
public void OverridingRestrictivelyAnnotatedMethod() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
.addSourceLines(
"TestNegativeCases.java",
"package com.uber;",
"import com.uber.lib.unannotated.RestrictivelyAnnotatedClass;",
"import org.checkerframework.checker.nullness.qual.NonNull;",
"import javax.annotation.Nullable;",
"public class TestNegativeCases extends RestrictivelyAnnotatedClass {",
" TestNegativeCases(){ super(new Object()); }",
" @Override public void acceptsNonNull(@Nullable Object o) { }",
" @Override public void acceptsNonNull2(Object o) { }",
" @Override public void acceptsNullable2(@Nullable Object o) { }",
" @Override public Object returnsNonNull() { return new Object(); }",
" @Override public Object returnsNullable() { return new Object(); }",
" @Override public @Nullable Object returnsNullable2() { return new Object();}",
"}")
.addSourceLines(
"TestPositiveCases.java",
"package com.uber;",
"import com.uber.lib.unannotated.RestrictivelyAnnotatedClass;",
"import org.checkerframework.checker.nullness.qual.NonNull;",
"import javax.annotation.Nullable;",
"public class TestPositiveCases extends RestrictivelyAnnotatedClass {",
" TestPositiveCases(){ super(new Object()); }",
" // BUG: Diagnostic contains: parameter o is @NonNull",
" public void acceptsNullable(Object o) { }",
" // BUG: Diagnostic contains: method returns @Nullable",
" public @Nullable Object returnsNonNull2() { return new Object(); }",
"}")
.doTest();
}
@Test
public void testCastToNonNull() {
compilationHelper
.addSourceFile("Util.java")
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import static com.uber.nullaway.testdata.Util.castToNonNull;",
"class Test {",
" Object test1(@Nullable Object o) {",
" return castToNonNull(o);",
" }",
" Object test2(Object o) {",
" // BUG: Diagnostic contains: passing known @NonNull parameter 'o' to CastToNonNullMethod",
" return castToNonNull(o);",
" }",
"}")
.doTest();
}
@Test
public void testReadStaticInConstructor() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"class Test {",
" // BUG: Diagnostic contains: @NonNull static field o not initialized",
" static Object o;",
" Object f, g;",
" public Test() {",
" f = new String(\"hi\");",
" o = new Object();",
" g = o;",
" }",
"}")
.doTest();
}
@Test
public void jarinferLoadStubsTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import com.uber.nullaway.jarinfer.toys.unannotated.Toys;",
"class Test {",
" void test1(@Nullable String s) {",
" // BUG: Diagnostic contains: passing @Nullable parameter 's'",
" Toys.test1(s, \"let's\", \"try\");",
" }",
"}")
.doTest();
}
@Test
public void jarinferNullableReturnsTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated",
"-XepOpt:NullAway:JarInferUseReturnAnnotations=true"))
.addSourceLines(
"Test.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import com.uber.nullaway.jarinfer.toys.unannotated.Toys;",
"class Test {",
" void test1(@Nullable String s) {",
" // BUG: Diagnostic contains: passing @Nullable parameter 'Toys.getString(false, s)'",
" Toys.test1(Toys.getString(false, s), \"let's\", \"try\");",
" }",
"}")
.doTest();
}
@Test
public void customErrorURL() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:ErrorURL=http://mydomain.com/nullaway"))
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" static void foo() {",
" // BUG: Diagnostic contains: mydomain.com",
" Object x = null; x.toString();",
" }",
"}")
.doTest();
}
@Test
public void defaultURL() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber"))
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" static void foo() {",
" // BUG: Diagnostic contains: t.uber.com/nullaway",
" Object x = null; x.toString();",
" }",
"}")
.doTest();
}
@Test
public void invokeNativeFromInitializer() {
compilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"class Test {",
" Object f;",
" private native void foo();",
" // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field f",
" Test() {",
" foo();",
" }",
"}")
.doTest();
}
@Test
public void overrideFailsOnExplicitlyNullableLibraryModelParam() {
compilationHelper
.addSourceLines( // Dummy android.view.GestureDetector.OnGestureListener interface
"GestureDetector.java",
"package android.view;",
"public class GestureDetector {",
" public static interface OnGestureListener {",
// Ignore other methods for this test, to make code shorter on both files:
" boolean onScroll(MotionEvent me1, MotionEvent me2, float f1, float f2);",
" }",
"}")
.addSourceLines( // Dummy android.view.MotionEvent class
"MotionEvent.java", "package android.view;", "public class MotionEvent { }")
.addSourceLines(
"Test.java",
"package com.uber;",
"import android.view.GestureDetector;",
"import android.view.MotionEvent;",
"class Test implements GestureDetector.OnGestureListener {",
" Test() { }",
" @Override",
" // BUG: Diagnostic contains: parameter me1 is @NonNull",
" public boolean onScroll(MotionEvent me1, MotionEvent me2, float f1, float f2) {",
" return false; // NoOp",
" }",
"}")
.addSourceLines(
"Test2.java",
"package com.uber;",
"import javax.annotation.Nullable;",
"import android.view.GestureDetector;",
"import android.view.MotionEvent;",
"class Test2 implements GestureDetector.OnGestureListener {",
" Test2() { }",
" @Override",
" public boolean onScroll(@Nullable MotionEvent me1, MotionEvent me2, float f1, float f2) {",
" return false; // NoOp",
" }",
"}")
.doTest();
}
@Test
public void testCapturingScopes() {
compilationHelper.addSourceFile("CapturingScopes.java").doTest();
}
}