diff --git a/nullaway/src/main/java/com/uber/nullaway/NullAway.java b/nullaway/src/main/java/com/uber/nullaway/NullAway.java index e481729e07..ee9236d959 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullAway.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullAway.java @@ -36,7 +36,6 @@ import com.google.auto.service.AutoService; import com.google.auto.value.AutoValue; -import com.google.common.base.Preconditions; import com.google.common.base.Verify; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; @@ -1705,8 +1704,9 @@ private Description handleInvocation( List actualParams) { List formalParams = methodSymbol.getParameters(); + boolean varArgsMethod = methodSymbol.isVarArgs(); if (formalParams.size() != actualParams.size() - && !methodSymbol.isVarArgs() + && !varArgsMethod && !methodSymbol.isStatic() && methodSymbol.isConstructor() && methodSymbol.enclClass().isInner()) { @@ -1751,7 +1751,7 @@ private Description handleInvocation( } if (config.isJSpecifyMode()) { GenericsChecks.compareGenericTypeParameterNullabilityForCall( - formalParams, actualParams, methodSymbol.isVarArgs(), this, state); + formalParams, actualParams, varArgsMethod, this, state); } } @@ -1764,30 +1764,55 @@ private Description handleInvocation( // NOTE: the case of an invocation on a possibly-null reference // is handled by matchMemberSelect() for (int argPos = 0; argPos < argumentPositionNullness.length; argPos++) { - if (!Objects.equals(Nullness.NONNULL, argumentPositionNullness[argPos])) { + boolean varargPosition = varArgsMethod && argPos == formalParams.size() - 1; + boolean argIsNonNull = Objects.equals(Nullness.NONNULL, argumentPositionNullness[argPos]); + if (!varargPosition && !argIsNonNull) { continue; } - ExpressionTree actual = null; + ExpressionTree actual; boolean mayActualBeNull = false; - if (argPos == formalParams.size() - 1 && methodSymbol.isVarArgs()) { + if (varargPosition) { // Check all vararg actual arguments for nullability + // This is the case where no actual parameter is passed for the var args parameter + // (i.e. it defaults to an empty array) if (actualParams.size() <= argPos) { continue; } - for (ExpressionTree arg : actualParams.subList(argPos, actualParams.size())) { - actual = arg; - mayActualBeNull = mayBeNullExpr(state, actual); - if (mayActualBeNull) { - break; + actual = actualParams.get(argPos); + // check if the varargs arguments are being passed as an array + Type.ArrayType varargsArrayType = + (Type.ArrayType) formalParams.get(formalParams.size() - 1).type; + Type actualParameterType = ASTHelpers.getType(actual); + if (actualParameterType != null + && state.getTypes().isAssignable(actualParameterType, varargsArrayType) + && actualParams.size() == argPos + 1) { + // This is the case where an array is explicitly passed in the position of the var args + // parameter + // If varargs array itself is not @Nullable, cannot pass @Nullable array + if (!Nullness.varargsArrayIsNullable(formalParams.get(argPos), config)) { + mayActualBeNull = mayBeNullExpr(state, actual); + } + } else { + // This is the case were varargs are being passed individually, as 1 or more actual + // arguments starting at the position of the var args formal. + // If the formal var args accepts `@Nullable`, then there is nothing for us to check. + if (!argIsNonNull) { + continue; + } + // TODO report all varargs errors in a single build; this code only reports the first + // error + for (ExpressionTree arg : actualParams.subList(argPos, actualParams.size())) { + actual = arg; + mayActualBeNull = mayBeNullExpr(state, actual); + if (mayActualBeNull) { + break; + } } } - } else { + } else { // not the vararg position actual = actualParams.get(argPos); mayActualBeNull = mayBeNullExpr(state, actual); } - // This statement should be unreachable without assigning actual beforehand: - Preconditions.checkNotNull(actual); - // make sure we are passing a non-null value if (mayActualBeNull) { String message = "passing @Nullable parameter '" diff --git a/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java b/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java index c8c41bdfce..56d54512cf 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java @@ -38,6 +38,7 @@ import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.code.Attribute; +import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.TargetType; import com.sun.tools.javac.code.Type; @@ -278,7 +279,7 @@ public static Stream getAllAnnotationsForParameter( * Gets the type use annotations on a symbol, ignoring annotations on components of the type (type * arguments, wildcards, etc.) */ - private static Stream getTypeUseAnnotations( + public static Stream getTypeUseAnnotations( Symbol symbol, Config config) { Stream rawTypeAttributes = symbol.getRawTypeAttributes().stream(); if (symbol instanceof Symbol.MethodSymbol) { @@ -432,6 +433,11 @@ public static boolean isArrayElementNullable(Symbol arraySymbol, Config config) } } } + // For varargs symbols we also consider the elements to be @Nullable if there is a @Nullable + // declaration annotation on the parameter + if ((arraySymbol.flags() & Flags.VARARGS) != 0) { + return Nullness.hasNullableDeclarationAnnotation(arraySymbol, config); + } return false; } } diff --git a/nullaway/src/main/java/com/uber/nullaway/Nullness.java b/nullaway/src/main/java/com/uber/nullaway/Nullness.java index 0ed694b60d..56cf28dcba 100644 --- a/nullaway/src/main/java/com/uber/nullaway/Nullness.java +++ b/nullaway/src/main/java/com/uber/nullaway/Nullness.java @@ -206,9 +206,16 @@ public static boolean hasNullableAnnotation(Symbol symbol, Config config) { return hasNullableAnnotation(NullabilityUtil.getAllAnnotations(symbol, config), config); } + private static boolean hasNullableTypeUseAnnotation(Symbol symbol, Config config) { + return hasNullableAnnotation(NullabilityUtil.getTypeUseAnnotations(symbol, config), config); + } + /** * Does the parameter of {@code symbol} at {@code paramInd} have a {@code @Nullable} declaration * or type-use annotation? This method works for methods defined in either source or class files. + * + *

For a varargs parameter, this method returns true if individual arguments passed in + * the varargs positions can be null. */ public static boolean paramHasNullableAnnotation( Symbol.MethodSymbol symbol, int paramInd, Config config) { @@ -217,8 +224,16 @@ public static boolean paramHasNullableAnnotation( if (isRecordEqualsParam(symbol, paramInd)) { return true; } - return hasNullableAnnotation( - NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config); + if (symbol.isVarArgs() + && paramInd == symbol.getParameters().size() - 1 + && !config.isLegacyAnnotationLocation()) { + // individual arguments passed in the varargs positions can be @Nullable if the array element + // type of the parameter is @Nullable + return NullabilityUtil.isArrayElementNullable(symbol.getParameters().get(paramInd), config); + } else { + return hasNullableAnnotation( + NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config); + } } private static boolean isRecordEqualsParam(Symbol.MethodSymbol symbol, int paramInd) { @@ -251,4 +266,20 @@ public static boolean paramHasNonNullAnnotation( return hasNonNullAnnotation( NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config); } + + /** + * Is the varargs parameter {@code paramSymbol} have a {@code @Nullable} annotation indicating + * that the argument array passed at a call site can be {@code null}? Syntactically, this would be + * written as {@code foo(Object @Nullable... args}} + */ + public static boolean varargsArrayIsNullable(Symbol paramSymbol, Config config) { + return hasNullableTypeUseAnnotation(paramSymbol, config) + || (config.isLegacyAnnotationLocation() + && hasNullableDeclarationAnnotation(paramSymbol, config)); + } + + /** Checks if the symbol has a {@code @Nullable} declaration annotation */ + public static boolean hasNullableDeclarationAnnotation(Symbol symbol, Config config) { + return hasNullableAnnotation(symbol.getRawAttributes().stream(), config); + } } diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java index 0726cd87f5..a10b0829e4 100644 --- a/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java +++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java @@ -7,6 +7,7 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; @@ -64,9 +65,13 @@ private static NullnessStore methodInitialStore( NullnessStore envStore = getEnvNullnessStoreForClass(classTree, context); NullnessStore.Builder result = envStore.toBuilder(); for (LocalVariableNode param : parameters) { - Element element = param.getElement(); - Nullness assumed = - Nullness.hasNullableAnnotation((Symbol) element, config) ? NULLABLE : NONNULL; + Symbol paramSymbol = (Symbol) param.getElement(); + Nullness assumed; + if ((paramSymbol.flags() & Flags.VARARGS) != 0) { + assumed = Nullness.varargsArrayIsNullable(paramSymbol, config) ? NULLABLE : NONNULL; + } else { + assumed = Nullness.hasNullableAnnotation(paramSymbol, config) ? NULLABLE : NONNULL; + } result.setInformation(AccessPath.fromLocal(param), assumed); } result = handler.onDataflowInitialStore(underlyingAST, parameters, result); diff --git a/nullaway/src/test/java/com/uber/nullaway/LegacyVarargsTests.java b/nullaway/src/test/java/com/uber/nullaway/LegacyVarargsTests.java new file mode 100644 index 0000000000..038c37baa8 --- /dev/null +++ b/nullaway/src/test/java/com/uber/nullaway/LegacyVarargsTests.java @@ -0,0 +1,425 @@ +package com.uber.nullaway; + +import com.google.errorprone.CompilationTestHelper; +import java.util.Arrays; +import org.junit.Test; + +/** + * Tests for handling of varargs annotations with legacy annotation locations enabled. Based on + * {@link VarargsTests}, with tests and assertions modified appropriately. + */ +public class LegacyVarargsTests extends NullAwayTestsBase { + + @Test + public void testNonNullVarargs() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Utilities {", + " public static String takesNonNullVarargs(Object o, Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNonNullVarargs(o1, o2, o3);", + " Utilities.takesNonNullVarargs(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Utilities.takesNonNullVarargs(o1, o4);", + " Object[] x = null;", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " Utilities.takesNonNullVarargs(o1, x);", + " }", + "}") + .doTest(); + } + + @Test + public void testNullableVarargs() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargs(Object o, @Nullable Object... others) {", + " // BUG: Diagnostic contains: [NullAway] dereferenced expression others is @Nullable", + " String s = o.toString() + \" \" + others.toString();", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargs(o1, o2, o3, o4);", + " Utilities.takesNullableVarargs(o1);", // Empty var args passed + " Utilities.takesNullableVarargs(o1, o4);", + " Utilities.takesNullableVarargs(o1, (java.lang.Object) null);", + " Object[] x = null;", + " Utilities.takesNullableVarargs(o1, x);", + " }", + "}") + .doTest(); + } + + @Test + public void nullableTypeUseVarargs() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargs(Object o, @Nullable Object... others) {", + " // BUG: Diagnostic contains: [NullAway] dereferenced expression others is @Nullable", + " String s = o.toString() + \" \" + others.toString();", + " for (Object other : others) {", + " // no error since we do not reason about array element nullability", + " s += other.toString();", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargs(o1, o2, o3, o4);", + " Utilities.takesNullableVarargs(o1);", // Empty var args passed + " Utilities.takesNullableVarargs(o1, o4);", + " Utilities.takesNullableVarargs(o1, (java.lang.Object) null);", + " Object[] x = null;", + " Utilities.takesNullableVarargs(o1, x);", + " }", + "}") + .doTest(); + } + + @Test + public void testNonNullVarargsFromHandler() { + String generatedAnnot = + (Double.parseDouble(System.getProperty("java.specification.version")) >= 11) + ? "@javax.annotation.processing.Generated" + : "@javax.annotation.Generated"; + makeTestHelperWithArgs( + 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;", + "import javax.annotation.Nonnull;", + generatedAnnot + "(\"foo\")", + "public class Generated {", + " public static String takesNonNullVarargs(@Nonnull Object o, @Nonnull Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Generated.takesNonNullVarargs(o1, o2, o3);", + " Generated.takesNonNullVarargs(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Generated.takesNonNullVarargs(o1, o4);", + " }", + "}") + .doTest(); + } + + // This is required for compatibility with kotlinc generated jars + // See https://github.com/uber/NullAway/issues/720 + @Test + public void testSkipJetbrainsNotNullOnVarArgsFromThirdPartyJars() { + makeTestHelperWithArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated", + "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true")) + .addSourceLines( + "ThirdParty.java", + "package com.uber.nullaway.lib.unannotated;", + "import org.jetbrains.annotations.NotNull;", + "public class ThirdParty {", + " public static String takesNullableVarargs(@NotNull Object o, @NotNull Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "FirstParty.java", + "package com.uber;", + "import org.jetbrains.annotations.NotNull;", + "public class FirstParty {", + " public static String takesNonNullVarargs(@NotNull Object o, @NotNull Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "import com.uber.nullaway.lib.unannotated.ThirdParty;", + "public class Test {", + " public void testNullableVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " ThirdParty.takesNullableVarargs(o1, o2, o3, o4);", + " ThirdParty.takesNullableVarargs(o1);", // Empty var args passed + " ThirdParty.takesNullableVarargs(o1, o4);", + " ThirdParty.takesNullableVarargs(o1, (Object) null);", + " // BUG: Diagnostic contains: passing @Nullable parameter '(Object[]) null' where @NonNull", + " ThirdParty.takesNullableVarargs(o1, (Object[]) null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " ThirdParty.takesNullableVarargs(o4);", // First arg is not varargs. + " }", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " FirstParty.takesNonNullVarargs(o1, o2, o3);", + " FirstParty.takesNonNullVarargs(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " FirstParty.takesNonNullVarargs(o1, o4);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseNullableVarargsArray() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseNullableVarargsArrayAndElements() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationBeforeDots() { + makeHelper() + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationOnElements() { + makeHelper() + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationOnBoth() { + makeHelper() + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void varargsPassArrayAndOtherArgs() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " static void takesVarargs(Object... args) {}", + " static void test(Object o) {", + " Object[] x = new Object[10];", + " takesVarargs(x, o);", + " }", + " static void takesNullableVarargsArray(Object @Nullable... args) {}", + " static void test2(Object o) {", + " Object[] x = null;", + " takesNullableVarargsArray(x);", + " // in legacy mode the annotation allows for individual arguments to be nullable", + " takesNullableVarargsArray(x, o);", + " }", + "}") + .doTest(); + } + + private CompilationTestHelper makeHelper() { + return makeTestHelperWithArgs( + Arrays.asList( + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:LegacyAnnotationLocations=true")); + } +} diff --git a/nullaway/src/test/java/com/uber/nullaway/VarargsTests.java b/nullaway/src/test/java/com/uber/nullaway/VarargsTests.java index f404668695..c6638a8f3f 100644 --- a/nullaway/src/test/java/com/uber/nullaway/VarargsTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/VarargsTests.java @@ -3,6 +3,7 @@ import java.util.Arrays; import org.junit.Test; +/** Tests for handling of varargs annotations in NullAway's default mode */ public class VarargsTests extends NullAwayTestsBase { @Test @@ -31,6 +32,9 @@ public void testNonNullVarargs() { " Utilities.takesNonNullVarargs(o1);", // Empty var args passed " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", " Utilities.takesNonNullVarargs(o1, o4);", + " Object[] x = null;", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " Utilities.takesNonNullVarargs(o1, x);", " }", "}") .doTest(); @@ -45,14 +49,41 @@ public void testNullableVarargs() { "import javax.annotation.Nullable;", "public class Utilities {", " public static String takesNullableVarargs(Object o, @Nullable Object... others) {", - " String s = o.toString() + \" \";", - " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", // Shouldn't be an error! - " for (Object other : others) {", - " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " String s = o.toString() + \" \" + others.toString();", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargs(o1, o2, o3, o4);", + " Utilities.takesNullableVarargs(o1);", // Empty var args passed + " Utilities.takesNullableVarargs(o1, o4);", + " Utilities.takesNullableVarargs(o1, (java.lang.Object) null);", + " Object[] x = null;", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " Utilities.takesNullableVarargs(o1, x);", " }", - " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", // Shouldn't be an error! + "}") + .doTest(); + } + + @Test + public void nullableTypeUseVarargs() { + defaultCompilationHelper + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargs(Object o, @Nullable Object... others) {", + " String s = o.toString() + \" \" + others.toString();", " for (Object other : others) {", - " s += other.toString();", // SHOULD be an error! + " // no error since we do not reason about array element nullability", + " s += other.toString();", " }", " return s;", " }", @@ -67,8 +98,9 @@ public void testNullableVarargs() { " Utilities.takesNullableVarargs(o1);", // Empty var args passed " Utilities.takesNullableVarargs(o1, o4);", " Utilities.takesNullableVarargs(o1, (java.lang.Object) null);", - // SHOULD be an error! - " Utilities.takesNullableVarargs(o1, (java.lang.Object[]) null);", + " Object[] x = null;", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " Utilities.takesNullableVarargs(o1, x);", " }", "}") .doTest(); @@ -164,7 +196,8 @@ public void testSkipJetbrainsNotNullOnVarArgsFromThirdPartyJars() { " ThirdParty.takesNullableVarargs(o1);", // Empty var args passed " ThirdParty.takesNullableVarargs(o1, o4);", " ThirdParty.takesNullableVarargs(o1, (Object) null);", - " ThirdParty.takesNullableVarargs(o1, (Object[]) null);", // SHOULD be an error! + " // BUG: Diagnostic contains: passing @Nullable parameter '(Object[]) null' where @NonNull", + " ThirdParty.takesNullableVarargs(o1, (Object[]) null);", " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", " ThirdParty.takesNullableVarargs(o4);", // First arg is not varargs. " }", @@ -177,4 +210,211 @@ public void testSkipJetbrainsNotNullOnVarArgsFromThirdPartyJars() { "}") .doTest(); } + + @Test + public void typeUseNullableVarargsArray() { + defaultCompilationHelper + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseNullableVarargsArrayAndElements() { + defaultCompilationHelper + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationBeforeDots() { + defaultCompilationHelper + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationOnElements() { + defaultCompilationHelper + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // BUG: Diagnostic contains: passing @Nullable parameter '(java.lang.Object[]) null' where @NonNull", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationOnBoth() { + defaultCompilationHelper + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void varargsPassArrayAndOtherArgs() { + defaultCompilationHelper + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " static void takesVarargs(Object... args) {}", + " static void test(Object o) {", + " Object[] x = new Object[10];", + " takesVarargs(x, o);", + " }", + " static void takesNullableVarargsArray(Object @Nullable... args) {}", + " static void test2(Object o) {", + " Object[] x = null;", + " // can pass x as the varargs array itself, but not along with other args", + " takesNullableVarargsArray(x);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " takesNullableVarargsArray(x, o);", + " }", + "}") + .doTest(); + } } diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/ArrayTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java similarity index 99% rename from nullaway/src/test/java/com/uber/nullaway/jspecify/ArrayTests.java rename to nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java index eee8b80969..35560acae2 100644 --- a/nullaway/src/test/java/com/uber/nullaway/jspecify/ArrayTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyArrayTests.java @@ -27,7 +27,7 @@ import java.util.Arrays; import org.junit.Test; -public class ArrayTests extends NullAwayTestsBase { +public class JSpecifyArrayTests extends NullAwayTestsBase { @Test public void arrayTopLevelAnnotationDereference() { diff --git a/nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyVarargsTests.java b/nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyVarargsTests.java new file mode 100644 index 0000000000..6ff2f2d754 --- /dev/null +++ b/nullaway/src/test/java/com/uber/nullaway/jspecify/JSpecifyVarargsTests.java @@ -0,0 +1,439 @@ +package com.uber.nullaway.jspecify; + +import com.google.errorprone.CompilationTestHelper; +import com.uber.nullaway.NullAwayTestsBase; +import com.uber.nullaway.VarargsTests; +import java.util.Arrays; +import org.junit.Test; + +/** + * Tests for handling of varargs annotations in JSpecify mode. Based on {@link VarargsTests}, with + * tests and assertions modified appropriately. + */ +public class JSpecifyVarargsTests extends NullAwayTestsBase { + + @Test + public void testNonNullVarargs() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Utilities {", + " public static String takesNonNullVarargs(Object o, Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNonNullVarargs(o1, o2, o3);", + " Utilities.takesNonNullVarargs(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Utilities.takesNonNullVarargs(o1, o4);", + " Object[] x = null;", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " Utilities.takesNonNullVarargs(o1, x);", + " }", + "}") + .doTest(); + } + + @Test + public void testNullableVarargs() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargs(Object o, @Nullable Object... others) {", + " String s = o.toString() + \" \" + others.toString();", + " for (Object other : others) {", + " // BUG: Diagnostic contains: dereferenced expression other is @Nullable", + " s += other.toString();", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargs(o1, o2, o3, o4);", + " Utilities.takesNullableVarargs(o1);", // Empty var args passed + " Utilities.takesNullableVarargs(o1, o4);", + " Utilities.takesNullableVarargs(o1, (java.lang.Object) null);", + " Object[] x = null;", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " Utilities.takesNullableVarargs(o1, x);", + " }", + "}") + .doTest(); + } + + @Test + public void nullableTypeUseVarargs() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargs(Object o, @Nullable Object... others) {", + " String s = o.toString() + \" \" + others.toString();", + " for (Object other : others) {", + " // BUG: Diagnostic contains: dereferenced expression other is @Nullable", + " s += other.toString();", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargs(o1, o2, o3, o4);", + " Utilities.takesNullableVarargs(o1);", // Empty var args passed + " Utilities.takesNullableVarargs(o1, o4);", + " Utilities.takesNullableVarargs(o1, (java.lang.Object) null);", + " Object[] x = null;", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " Utilities.takesNullableVarargs(o1, x);", + " }", + "}") + .doTest(); + } + + @Test + public void testNonNullVarargsFromHandler() { + String generatedAnnot = + (Double.parseDouble(System.getProperty("java.specification.version")) >= 11) + ? "@javax.annotation.processing.Generated" + : "@javax.annotation.Generated"; + makeTestHelperWithArgs( + 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;", + "import javax.annotation.Nonnull;", + generatedAnnot + "(\"foo\")", + "public class Generated {", + " public static String takesNonNullVarargs(@Nonnull Object o, @Nonnull Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "public class Test {", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Generated.takesNonNullVarargs(o1, o2, o3);", + " Generated.takesNonNullVarargs(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Generated.takesNonNullVarargs(o1, o4);", + " }", + "}") + .doTest(); + } + + // This is required for compatibility with kotlinc generated jars + // See https://github.com/uber/NullAway/issues/720 + @Test + public void testSkipJetbrainsNotNullOnVarArgsFromThirdPartyJars() { + makeTestHelperWithArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + "-XepOpt:NullAway:AnnotatedPackages=com.uber", + "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated", + "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true")) + .addSourceLines( + "ThirdParty.java", + "package com.uber.nullaway.lib.unannotated;", + "import org.jetbrains.annotations.NotNull;", + "public class ThirdParty {", + " public static String takesNullableVarargs(@NotNull Object o, @NotNull Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "FirstParty.java", + "package com.uber;", + "import org.jetbrains.annotations.NotNull;", + "public class FirstParty {", + " public static String takesNonNullVarargs(@NotNull Object o, @NotNull Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " s += other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import javax.annotation.Nullable;", + "import com.uber.nullaway.lib.unannotated.ThirdParty;", + "public class Test {", + " public void testNullableVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " ThirdParty.takesNullableVarargs(o1, o2, o3, o4);", + " ThirdParty.takesNullableVarargs(o1);", // Empty var args passed + " ThirdParty.takesNullableVarargs(o1, o4);", + " ThirdParty.takesNullableVarargs(o1, (Object) null);", + " // BUG: Diagnostic contains: passing @Nullable parameter '(Object[]) null' where @NonNull", + " ThirdParty.takesNullableVarargs(o1, (Object[]) null);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " ThirdParty.takesNullableVarargs(o4);", // First arg is not varargs. + " }", + " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " FirstParty.takesNonNullVarargs(o1, o2, o3);", + " FirstParty.takesNonNullVarargs(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " FirstParty.takesNonNullVarargs(o1, o4);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseNullableVarargsArray() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseNullableVarargsArrayAndElements() { + makeHelper() + .addSourceLines( + "Utilities.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " // BUG: Diagnostic contains: dereferenced expression other is @Nullable", + " s += other.toString();", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationBeforeDots() { + makeHelper() + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " s += (other == null) ? \"(null) \" : other.toString() + \" \";", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " // BUG: Diagnostic contains: passing @Nullable parameter", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // this is fine!", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationOnElements() { + makeHelper() + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object... others) {", + " String s = o.toString() + \" \";", + " for (Object other : others) {", + " // BUG: Diagnostic contains: dereferenced expression other is @Nullable", + " s += other.toString();", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " // BUG: Diagnostic contains: passing @Nullable parameter '(java.lang.Object[]) null' where @NonNull", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void typeUseAndDeclarationOnBoth() { + makeHelper() + .addSourceLines( + "Nullable.java", + "package com.uber;", + "import java.lang.annotation.ElementType;", + "import java.lang.annotation.Target;", + "@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})", + "public @interface Nullable {}") + .addSourceLines( + "Utilities.java", + "package com.uber;", + "public class Utilities {", + " public static String takesNullableVarargsArray(Object o, @Nullable Object @Nullable... others) {", + " String s = o.toString() + \" \";", + " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", + " for (Object other : others) {", + " // BUG: Diagnostic contains: dereferenced expression other is @Nullable", + " s += other.toString();", + " }", + " return s;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "public class Test {", + " public void testNullableVarargsArray(Object o1, Object o2, Object o3, @Nullable Object o4) {", + " Utilities.takesNullableVarargsArray(o1, o2, o3, o4);", + " Utilities.takesNullableVarargsArray(o1);", // Empty var args passed + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object) null);", + " Utilities.takesNullableVarargsArray(o1, (java.lang.Object[]) null);", + " }", + "}") + .doTest(); + } + + @Test + public void varargsPassArrayAndOtherArgs() { + makeHelper() + .addSourceLines( + "Test.java", + "package com.uber;", + "import org.checkerframework.checker.nullness.qual.Nullable;", + "public class Test {", + " static void takesVarargs(Object... args) {}", + " static void test(Object o) {", + " Object[] x = new Object[10];", + " takesVarargs(x, o);", + " }", + " static void takesNullableVarargsArray(Object @Nullable... args) {}", + " static void test2(Object o) {", + " Object[] x = null;", + " // can pass x as the varargs array itself, but not along with other args", + " takesNullableVarargsArray(x);", + " // BUG: Diagnostic contains: passing @Nullable parameter 'x'", + " takesNullableVarargsArray(x, o);", + " }", + "}") + .doTest(); + } + + private CompilationTestHelper makeHelper() { + return makeTestHelperWithArgs( + Arrays.asList( + "-XepOpt:NullAway:AnnotatedPackages=com.uber", "-XepOpt:NullAway:JSpecifyMode=true")); + } +}