diff --git a/nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java b/nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java index 3e1d093137..849d3e938d 100644 --- a/nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java +++ b/nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java @@ -111,6 +111,22 @@ public boolean isGenerated(Symbol symbol, Config config) { return ASTHelpers.hasDirectAnnotationWithSimpleName(outermostClassSymbol, "Generated"); } + /** + * Check if the symbol represents the .class field of a primitive type. + * + *

e.g. int.class, boolean.class, void.class, etc. + * + * @param symbol symbol for entity + * @return true iff this symbol represents t.class for a primitive type t. + */ + private static boolean isClassFieldOfPrimitiveType(Symbol symbol) { + return symbol.name.contentEquals("class") + && symbol.owner != null + && symbol.owner.getKind().equals(ElementKind.CLASS) + && symbol.owner.getQualifiedName().equals(symbol.owner.getSimpleName()) + && symbol.owner.enclClass() == null; + } + /** * Check if a symbol comes from unannotated code. * @@ -123,6 +139,13 @@ public boolean isSymbolUnannotated(Symbol symbol, Config config) { Symbol.ClassSymbol classSymbol; if (symbol instanceof Symbol.ClassSymbol) { classSymbol = (Symbol.ClassSymbol) symbol; + } else if (isClassFieldOfPrimitiveType(symbol)) { + // As a special case, int.class, boolean.class, etc, cause ASTHelpers.enclosingClass(...) to + // return null, even though int/boolean/etc. are technically ClassSymbols. We consider this + // class "field" of primitive types to be always unannotated. (In the future, we could check + // here for whether java.lang is in the annotated packages, but if it is, I suspect we will + // have weirder problems than this) + return true; } else { classSymbol = castToNonNull(ASTHelpers.enclosingClass(symbol)); } diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java index 7a66e18d1e..166da7262e 100644 --- a/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java +++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java @@ -986,4 +986,99 @@ public void nullUnmarkedAndAcknowledgeRestrictiveAnnotations() { "}") .doTest(); } + + @Test + public void nullMarkedStaticImports() { + makeTestHelperWithArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + // Flag is required for now, but might no longer be need with @NullMarked! + "-XepOpt:NullAway:AnnotatedPackages=com.uber.dontcare", + "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true")) + .addSourceLines( + "StaticMethods.java", + "package com.uber;", + "import com.example.jspecify.future.annotations.NullMarked;", + "import org.jspecify.nullness.Nullable;", + "public final class StaticMethods {", + " private StaticMethods() {}", + " @NullMarked", + " public static Object nonNullCallee(Object o) {", + " return o;", + " }", + " @NullMarked", + " @Nullable", + " public static Object nullableCallee(@Nullable Object o) {", + " return o;", + " }", + " public static Object unmarkedCallee(@Nullable Object o) {", + " // no error, because unmarked", + " return o;", + " }", + " @Nullable", + " public static Object unmarkedNullableCallee(@Nullable Object o) {", + " return o;", + " }", + "}") + .addSourceLines( + "Test.java", + "package com.uber;", + "import static com.uber.StaticMethods.nonNullCallee;", + "import static com.uber.StaticMethods.nullableCallee;", + "import static com.uber.StaticMethods.unmarkedCallee;", + "import static com.uber.StaticMethods.unmarkedNullableCallee;", + "import com.example.jspecify.future.annotations.NullMarked;", + "import org.jspecify.nullness.Nullable;", + "@NullMarked", + "public class Test {", + " public Object getNewObject() {", + " return new Object();", + " }", + " public void test() {", + " Object o = getNewObject();", + " nonNullCallee(o).toString();", + " // BUG: Diagnostic contains: dereferenced expression nullableCallee(o) is @Nullable", + " nullableCallee(o).toString();", + " unmarkedCallee(o).toString();", + " // BUG: Diagnostic contains: dereferenced expression unmarkedNullableCallee(o) is @Nullable", + " unmarkedNullableCallee(o).toString();", + " }", + "}") + .doTest(); + } + + @Test + public void dotClassSanityTest() { + // Check that we do not crash while determining the nullmarked-ness of primitive.class (e.g. + // int.class) + makeTestHelperWithArgs( + Arrays.asList( + "-d", + temporaryFolder.getRoot().getAbsolutePath(), + // Flag is required for now, but might no longer be need with @NullMarked! + "-XepOpt:NullAway:AnnotatedPackages=com.uber.dontcare", + "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true")) + .addSourceLines( + "Test.java", + "package com.uber;", + "import com.example.jspecify.future.annotations.NullMarked;", + "import org.jspecify.nullness.Nullable;", + "@NullMarked", + "public class Test {", + " public void takesClass(Class c) {", + " }", + " public Object test(boolean flag) {", + " takesClass(Test.class);", + " takesClass(String.class);", + " takesClass(int.class);", + " takesClass(boolean.class);", + " takesClass(float.class);", + " takesClass(void.class);", + " // NEEDED TO TRIGGER DATAFLOW:", + " return flag ? Test.class : new Object();", + " }", + "}") + .doTest(); + } }