Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bugs in reading varargs annotations from bytecodes #1055

Merged
merged 22 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 106 additions & 12 deletions nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import java.util.stream.Stream;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import org.checkerframework.nullaway.javacutil.AnnotationUtils;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -301,9 +302,27 @@
* Gets the type use annotations on a symbol, ignoring annotations on components of the type (type
* arguments, wildcards, etc.)
*/
public static Stream<? extends AnnotationMirror> getTypeUseAnnotations(
Symbol symbol, Config config) {
Stream<Attribute.TypeCompound> rawTypeAttributes = symbol.getRawTypeAttributes().stream();
public static Stream<Attribute.TypeCompound> getTypeUseAnnotations(Symbol symbol, Config config) {
return getTypeUseAnnotations(symbol, config, /* onlyDirect= */ true);
}

/**
* Gets the type use annotations on a symbol
*
* @param symbol the symbol
* @param config NullAway configuration
* @param onlyDirect if true, only return annotations that are directly on the type, not on
* components of the type (type arguments, wildcards, array contents, etc.)
* @return the type use annotations on the symbol
*/
private static Stream<Attribute.TypeCompound> getTypeUseAnnotations(
Symbol symbol, Config config, boolean onlyDirect) {
// Adapted from Error Prone's MoreAnnotations class:
// https://github.com/google/error-prone/blob/5f71110374e63f3c35b661f538295fa15b5c1db2/check_api/src/main/java/com/google/errorprone/util/MoreAnnotations.java#L84-L91
Symbol typeAnnotationOwner =
symbol.getKind().equals(ElementKind.PARAMETER) ? symbol.owner : symbol;
Stream<Attribute.TypeCompound> rawTypeAttributes =
typeAnnotationOwner.getRawTypeAttributes().stream();
if (symbol instanceof Symbol.MethodSymbol) {
// for methods, we want annotations on the return type
return rawTypeAttributes.filter(
Expand All @@ -313,7 +332,45 @@
} else {
// filter for annotations directly on the type
return rawTypeAttributes.filter(
t -> NullabilityUtil.isDirectTypeUseAnnotation(t, symbol, config));
t ->
targetTypeMatches(symbol, t.position)
&& (!onlyDirect || NullabilityUtil.isDirectTypeUseAnnotation(t, symbol, config)));
}
}

// Adapted from Error Prone MoreAnnotations:
// https://github.com/google/error-prone/blob/5f71110374e63f3c35b661f538295fa15b5c1db2/check_api/src/main/java/com/google/errorprone/util/MoreAnnotations.java#L128
private static boolean targetTypeMatches(Symbol sym, TypeAnnotationPosition position) {
switch (sym.getKind()) {
case LOCAL_VARIABLE:
return position.type == TargetType.LOCAL_VARIABLE;
case FIELD:
// treated like a field
case ENUM_CONSTANT:
return position.type == TargetType.FIELD;
case CONSTRUCTOR:
case METHOD:
return position.type == TargetType.METHOD_RETURN;
case PARAMETER:
if (position.type.equals(TargetType.METHOD_FORMAL_PARAMETER)) {
int parameterIndex = position.parameter_index;
if (position.onLambda != null) {
com.sun.tools.javac.util.List<JCTree.JCVariableDecl> lambdaParams =
position.onLambda.params;
return parameterIndex < lambdaParams.size()
&& lambdaParams.get(parameterIndex).sym.equals(sym);
} else {
return ((Symbol.MethodSymbol) sym.owner).getParameters().indexOf(sym) == parameterIndex;
}
} else {
return false;
}
case CLASS:
// There are no type annotations on the top-level type of the class being declared, only
// on other types in the signature (e.g. `class Foo extends Bar<@A Baz> {}`).
return false;

Check warning on line 371 in nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java#L371

Added line #L371 was not covered by tests
default:
throw new AssertionError("unsupported element kind " + sym.getKind() + " symbol " + sym);

Check warning on line 373 in nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java#L373

Added line #L373 was not covered by tests
}
}

Expand Down Expand Up @@ -488,12 +545,28 @@
}
// For varargs symbols we also consider the elements to be @Nullable if there is a @Nullable
// declaration annotation on the parameter
// NOTE this flag check does not work for the varargs parameter of a method defined in bytecodes
if ((arraySymbol.flags() & Flags.VARARGS) != 0) {
return Nullness.hasNullableDeclarationAnnotation(arraySymbol, config);
}
return false;
}

/**
* Checks if the given varargs symbol has a {@code @Nullable} annotation for its elements. Works
* for both source and bytecode.
*
* @param varargsSymbol the symbol of the varargs parameter
* @param config NullAway configuration
* @return true if the varargs symbol has a {@code @Nullable} annotation for its elements, false
* otherwise
*/
public static boolean nullableVarargsElementsForSourceOrBytecode(
Symbol varargsSymbol, Config config) {
return isArrayElementNullable(varargsSymbol, config)
|| Nullness.hasNullableDeclarationAnnotation(varargsSymbol, config);
}

/**
* Checks if the given array symbol has a {@code @NonNull} annotation for its elements.
*
Expand All @@ -503,23 +576,44 @@
* otherwise
*/
public static boolean isArrayElementNonNull(Symbol arraySymbol, Config config) {
for (Attribute.TypeCompound t : arraySymbol.getRawTypeAttributes()) {
for (TypeAnnotationPosition.TypePathEntry entry : t.position.location) {
if (entry.tag == TypeAnnotationPosition.TypePathEntryKind.ARRAY) {
if (Nullness.isNonNullAnnotation(t.type.toString(), config)) {
return true;
}
}
}
if (getTypeUseAnnotations(arraySymbol, config, /* onlyDirect= */ false)
.anyMatch(
t -> {
for (TypeAnnotationPosition.TypePathEntry entry : t.position.location) {
if (entry.tag == TypeAnnotationPosition.TypePathEntryKind.ARRAY) {
if (Nullness.isNonNullAnnotation(t.type.toString(), config)) {
return true;
}
}
}

Check warning on line 588 in nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java

View check run for this annotation

Codecov / codecov/patch

nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java#L588

Added line #L588 was not covered by tests
return false;
})) {
return true;
}
// For varargs symbols we also consider the elements to be @NonNull if there is a @NonNull
// declaration annotation on the parameter
// NOTE this flag check does not work for the varargs parameter of a method defined in bytecodes
if ((arraySymbol.flags() & Flags.VARARGS) != 0) {
return Nullness.hasNonNullDeclarationAnnotation(arraySymbol, config);
}
return false;
}

/**
* Checks if the given varargs symbol has a {@code @NonNull} annotation for its elements. Works
* for both source and bytecode.
*
* @param varargsSymbol the symbol of the varargs parameter
* @param config NullAway configuration
* @return true if the varargs symbol has a {@code @NonNull} annotation for its elements, false
* otherwise
*/
public static boolean nonnullVarargsElementsForSourceOrBytecode(
Symbol varargsSymbol, Config config) {
return isArrayElementNonNull(varargsSymbol, config)
|| Nullness.hasNonNullDeclarationAnnotation(varargsSymbol, config);
}

/**
* Does the given symbol have a JetBrains @NotNull declaration annotation? Useful for workarounds
* in light of https://github.com/uber/NullAway/issues/720
Expand Down
10 changes: 4 additions & 6 deletions nullaway/src/main/java/com/uber/nullaway/Nullness.java
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,8 @@ public static boolean paramHasNullableAnnotation(
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);
return NullabilityUtil.nullableVarargsElementsForSourceOrBytecode(
symbol.getParameters().get(paramInd), config);
} else {
return hasNullableAnnotation(
NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config);
Expand Down Expand Up @@ -269,9 +268,8 @@ public static boolean paramHasNonNullAnnotation(
if (symbol.isVarArgs()
&& paramInd == symbol.getParameters().size() - 1
&& !config.isLegacyAnnotationLocation()) {
// individual arguments passed in the varargs positions must be @NonNull if the array element
// type of the parameter is @NonNull
return NullabilityUtil.isArrayElementNonNull(symbol.getParameters().get(paramInd), config);
return NullabilityUtil.nonnullVarargsElementsForSourceOrBytecode(
symbol.getParameters().get(paramInd), config);
} else {
return hasNonNullAnnotation(
NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ private static NullnessStore methodInitialStore(
for (LocalVariableNode param : parameters) {
Symbol paramSymbol = (Symbol) param.getElement();
Nullness assumed;
// Using this flag to check for a varargs parameter works since we know paramSymbol represents
// a parameter defined in source code
if ((paramSymbol.flags() & Flags.VARARGS) != 0) {
assumed = Nullness.varargsArrayIsNullable(paramSymbol, config) ? NULLABLE : NONNULL;
} else {
Expand Down
21 changes: 21 additions & 0 deletions nullaway/src/test/java/com/uber/nullaway/Java8Tests.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,25 @@ public void methodReferenceOnNullableVariable() {
"}")
.doTest();
}

/** test that we can properly read an explicit type-use annotation on a lambda parameter */
@Test
public void testNullableLambdaParamTypeUse() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have the true positive case test somewhere? E.g. something showing that:

NonNullParamFunctionTypeUse n3 = (@Nullable Object x) -> (x == null) ? \"null\" : x.toString();",

Fails if NonNullParamFunctionTypeUse is equivalent to NonNullParamFunctionTypeUse but without @Nullable on takeVal.

Also, how about assigning a lambda that doesn't check for null?

Feel free to ignore if these tests already exist somewhere (they well might), just checking since this test below implies those cases too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think those tests exist here:

https://github.com/uber/NullAway/blob/master/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayJava8NegativeCases.java
https://github.com/uber/NullAway/blob/master/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayJava8PositiveCases.java

The key difference in the new unit test is that @Nullable is a type-use annotation rather than a declaration annotation, and with our new logic we initially weren't reading it out correctly. I added a comment in a59a7a8

defaultCompilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" @FunctionalInterface",
" interface NullableParamFunctionTypeUse<T, U> {",
" U takeVal(@Nullable T x);",
" }",
" static void testParamTypeUse() {",
" NullableParamFunctionTypeUse n3 = (@Nullable Object x) -> (x == null) ? \"null\" : x.toString();",
" NullableParamFunctionTypeUse n4 = (x) -> (x == null) ? \"null\" : x.toString();",
" }",
"}")
.doTest();
}
}
68 changes: 68 additions & 0 deletions nullaway/src/test/java/com/uber/nullaway/VarargsTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,41 @@ public void testNullableVarargs() {
.doTest();
}

/** Test for a @Nullable declaration annotation on a varargs parameter defined in bytecode */
@Test
public void nullableDeclarationVarArgsFromBytecode() {
defaultCompilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.lib.Varargs;",
"public class Test {",
" public void testDeclaration() {",
" String x = null;",
" Varargs s = new Varargs(x);",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add the case passing new Varargs(x, y) where x is non-null but y is @Nullable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

" String y = \"hello\", z = null;",
" Varargs s2 = new Varargs(y, z);",
" }",
"}")
.doTest();
}

@Test
public void nullableTypeUseVarArgsFromBytecode() {
defaultCompilationHelper
.addSourceLines(
"Test.java",
"package com.uber;",
"import com.uber.lib.Varargs;",
"public class Test {",
" public void testTypeUse() {",
" String[] x = null;",
" Varargs.typeUse(x);",
" }",
"}")
.doTest();
}

@Test
public void nullableTypeUseVarargs() {
defaultCompilationHelper
Expand Down Expand Up @@ -515,4 +550,37 @@ public void testVarargsRestrictive() {
"}")
.doTest();
}

/**
* Test for a restrictive @NonNull declaration annotation on a varargs parameter defined in
* bytecode
*/
@Test
public void testVarargsRestrictiveBytecodes() {
makeTestHelperWithArgs(
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 com.uber.lib.unannotated.RestrictivelyAnnotatedVarargs;",
"public class Test {",
" public void testDeclaration() {",
" String x = null;",
" String[] y = null;",
" // BUG: Diagnostic contains: passing @Nullable parameter 'x'",
" RestrictivelyAnnotatedVarargs.test(x);",
" RestrictivelyAnnotatedVarargs.test(y);",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to annotate varargs as non-null array of non-null elements? Do we want a test case for that? (separate from RestrictivelyAnnotatedVarargs.test)

I'd even argue that it is exceedingly rare for the intention of a varargs argument to be "a potentially nullable array of non-null elements", but I guess we should follow JSpecify here either way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this test in f30299a and it exposed a bug which I fixed! We still don't report an error when passing a @Nullable array for this case; that requires a full and proper fix for #1027

" // BUG: Diagnostic contains: passing @Nullable parameter 'x'",
" RestrictivelyAnnotatedVarargs.testTypeUse(x);",
" // TODO should report an error; requires a fuller fix for #1027",
" RestrictivelyAnnotatedVarargs.testTypeUse(y);",
" }",
"}")
.doTest();
}
}
1 change: 1 addition & 0 deletions test-java-lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ plugins {

dependencies {
annotationProcessor project(":nullaway")
implementation deps.test.jsr305Annotations
implementation deps.build.jspecify

compileOnly deps.build.javaxValidation
Expand Down
10 changes: 10 additions & 0 deletions test-java-lib/src/main/java/com/uber/lib/Varargs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.uber.lib;

import javax.annotation.Nullable;

public class Varargs {

public Varargs(@Nullable String... args) {}

public static void typeUse(String @org.jspecify.annotations.Nullable ... args) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.uber.lib.unannotated;

import javax.annotation.Nonnull;

public class RestrictivelyAnnotatedVarargs {

public static void test(@Nonnull String... args) {}

public static void testTypeUse(
@org.jspecify.annotations.NonNull String @org.jspecify.annotations.NonNull ... args) {}
}