Skip to content

Commit 91cf25d

Browse files
authored
Fix bugs in reading varargs annotations from bytecodes (#1055)
For the case of declaration annotations, we were using `Flags.VARARGS` to check if a `Symbol` represented a varargs parameter, but this only works if the method is defined in source code. We add new methods for varargs parameters that may be defined in bytecodes, and always check for declaration annotations in such cases. For type use annotations on parameters, for a method from bytecodes the annotation info is stored on the method's symbol, not the parameter's. We borrow some Error Prone code to handle this case.
1 parent 2a9188b commit 91cf25d

File tree

8 files changed

+223
-18
lines changed

8 files changed

+223
-18
lines changed

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

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.stream.Stream;
5656
import javax.lang.model.element.AnnotationMirror;
5757
import javax.lang.model.element.AnnotationValue;
58+
import javax.lang.model.element.ElementKind;
5859
import javax.lang.model.element.ExecutableElement;
5960
import org.checkerframework.nullaway.javacutil.AnnotationUtils;
6061
import org.jspecify.annotations.Nullable;
@@ -301,9 +302,27 @@ public static Stream<? extends AnnotationMirror> getAllAnnotationsForParameter(
301302
* Gets the type use annotations on a symbol, ignoring annotations on components of the type (type
302303
* arguments, wildcards, etc.)
303304
*/
304-
public static Stream<? extends AnnotationMirror> getTypeUseAnnotations(
305-
Symbol symbol, Config config) {
306-
Stream<Attribute.TypeCompound> rawTypeAttributes = symbol.getRawTypeAttributes().stream();
305+
public static Stream<Attribute.TypeCompound> getTypeUseAnnotations(Symbol symbol, Config config) {
306+
return getTypeUseAnnotations(symbol, config, /* onlyDirect= */ true);
307+
}
308+
309+
/**
310+
* Gets the type use annotations on a symbol
311+
*
312+
* @param symbol the symbol
313+
* @param config NullAway configuration
314+
* @param onlyDirect if true, only return annotations that are directly on the type, not on
315+
* components of the type (type arguments, wildcards, array contents, etc.)
316+
* @return the type use annotations on the symbol
317+
*/
318+
private static Stream<Attribute.TypeCompound> getTypeUseAnnotations(
319+
Symbol symbol, Config config, boolean onlyDirect) {
320+
// Adapted from Error Prone's MoreAnnotations class:
321+
// https://github.com/google/error-prone/blob/5f71110374e63f3c35b661f538295fa15b5c1db2/check_api/src/main/java/com/google/errorprone/util/MoreAnnotations.java#L84-L91
322+
Symbol typeAnnotationOwner =
323+
symbol.getKind().equals(ElementKind.PARAMETER) ? symbol.owner : symbol;
324+
Stream<Attribute.TypeCompound> rawTypeAttributes =
325+
typeAnnotationOwner.getRawTypeAttributes().stream();
307326
if (symbol instanceof Symbol.MethodSymbol) {
308327
// for methods, we want annotations on the return type
309328
return rawTypeAttributes.filter(
@@ -313,7 +332,45 @@ public static Stream<? extends AnnotationMirror> getTypeUseAnnotations(
313332
} else {
314333
// filter for annotations directly on the type
315334
return rawTypeAttributes.filter(
316-
t -> NullabilityUtil.isDirectTypeUseAnnotation(t, symbol, config));
335+
t ->
336+
targetTypeMatches(symbol, t.position)
337+
&& (!onlyDirect || NullabilityUtil.isDirectTypeUseAnnotation(t, symbol, config)));
338+
}
339+
}
340+
341+
// Adapted from Error Prone MoreAnnotations:
342+
// https://github.com/google/error-prone/blob/5f71110374e63f3c35b661f538295fa15b5c1db2/check_api/src/main/java/com/google/errorprone/util/MoreAnnotations.java#L128
343+
private static boolean targetTypeMatches(Symbol sym, TypeAnnotationPosition position) {
344+
switch (sym.getKind()) {
345+
case LOCAL_VARIABLE:
346+
return position.type == TargetType.LOCAL_VARIABLE;
347+
case FIELD:
348+
// treated like a field
349+
case ENUM_CONSTANT:
350+
return position.type == TargetType.FIELD;
351+
case CONSTRUCTOR:
352+
case METHOD:
353+
return position.type == TargetType.METHOD_RETURN;
354+
case PARAMETER:
355+
if (position.type.equals(TargetType.METHOD_FORMAL_PARAMETER)) {
356+
int parameterIndex = position.parameter_index;
357+
if (position.onLambda != null) {
358+
com.sun.tools.javac.util.List<JCTree.JCVariableDecl> lambdaParams =
359+
position.onLambda.params;
360+
return parameterIndex < lambdaParams.size()
361+
&& lambdaParams.get(parameterIndex).sym.equals(sym);
362+
} else {
363+
return ((Symbol.MethodSymbol) sym.owner).getParameters().indexOf(sym) == parameterIndex;
364+
}
365+
} else {
366+
return false;
367+
}
368+
case CLASS:
369+
// There are no type annotations on the top-level type of the class being declared, only
370+
// on other types in the signature (e.g. `class Foo extends Bar<@A Baz> {}`).
371+
return false;
372+
default:
373+
throw new AssertionError("unsupported element kind " + sym.getKind() + " symbol " + sym);
317374
}
318375
}
319376

@@ -488,12 +545,28 @@ public static boolean isArrayElementNullable(Symbol arraySymbol, Config config)
488545
}
489546
// For varargs symbols we also consider the elements to be @Nullable if there is a @Nullable
490547
// declaration annotation on the parameter
548+
// NOTE this flag check does not work for the varargs parameter of a method defined in bytecodes
491549
if ((arraySymbol.flags() & Flags.VARARGS) != 0) {
492550
return Nullness.hasNullableDeclarationAnnotation(arraySymbol, config);
493551
}
494552
return false;
495553
}
496554

555+
/**
556+
* Checks if the given varargs symbol has a {@code @Nullable} annotation for its elements. Works
557+
* for both source and bytecode.
558+
*
559+
* @param varargsSymbol the symbol of the varargs parameter
560+
* @param config NullAway configuration
561+
* @return true if the varargs symbol has a {@code @Nullable} annotation for its elements, false
562+
* otherwise
563+
*/
564+
public static boolean nullableVarargsElementsForSourceOrBytecode(
565+
Symbol varargsSymbol, Config config) {
566+
return isArrayElementNullable(varargsSymbol, config)
567+
|| Nullness.hasNullableDeclarationAnnotation(varargsSymbol, config);
568+
}
569+
497570
/**
498571
* Checks if the given array symbol has a {@code @NonNull} annotation for its elements.
499572
*
@@ -503,23 +576,44 @@ public static boolean isArrayElementNullable(Symbol arraySymbol, Config config)
503576
* otherwise
504577
*/
505578
public static boolean isArrayElementNonNull(Symbol arraySymbol, Config config) {
506-
for (Attribute.TypeCompound t : arraySymbol.getRawTypeAttributes()) {
507-
for (TypeAnnotationPosition.TypePathEntry entry : t.position.location) {
508-
if (entry.tag == TypeAnnotationPosition.TypePathEntryKind.ARRAY) {
509-
if (Nullness.isNonNullAnnotation(t.type.toString(), config)) {
510-
return true;
511-
}
512-
}
513-
}
579+
if (getTypeUseAnnotations(arraySymbol, config, /* onlyDirect= */ false)
580+
.anyMatch(
581+
t -> {
582+
for (TypeAnnotationPosition.TypePathEntry entry : t.position.location) {
583+
if (entry.tag == TypeAnnotationPosition.TypePathEntryKind.ARRAY) {
584+
if (Nullness.isNonNullAnnotation(t.type.toString(), config)) {
585+
return true;
586+
}
587+
}
588+
}
589+
return false;
590+
})) {
591+
return true;
514592
}
515593
// For varargs symbols we also consider the elements to be @NonNull if there is a @NonNull
516594
// declaration annotation on the parameter
595+
// NOTE this flag check does not work for the varargs parameter of a method defined in bytecodes
517596
if ((arraySymbol.flags() & Flags.VARARGS) != 0) {
518597
return Nullness.hasNonNullDeclarationAnnotation(arraySymbol, config);
519598
}
520599
return false;
521600
}
522601

602+
/**
603+
* Checks if the given varargs symbol has a {@code @NonNull} annotation for its elements. Works
604+
* for both source and bytecode.
605+
*
606+
* @param varargsSymbol the symbol of the varargs parameter
607+
* @param config NullAway configuration
608+
* @return true if the varargs symbol has a {@code @NonNull} annotation for its elements, false
609+
* otherwise
610+
*/
611+
public static boolean nonnullVarargsElementsForSourceOrBytecode(
612+
Symbol varargsSymbol, Config config) {
613+
return isArrayElementNonNull(varargsSymbol, config)
614+
|| Nullness.hasNonNullDeclarationAnnotation(varargsSymbol, config);
615+
}
616+
523617
/**
524618
* Does the given symbol have a JetBrains @NotNull declaration annotation? Useful for workarounds
525619
* in light of https://github.com/uber/NullAway/issues/720

nullaway/src/main/java/com/uber/nullaway/Nullness.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,8 @@ public static boolean paramHasNullableAnnotation(
227227
if (symbol.isVarArgs()
228228
&& paramInd == symbol.getParameters().size() - 1
229229
&& !config.isLegacyAnnotationLocation()) {
230-
// individual arguments passed in the varargs positions can be @Nullable if the array element
231-
// type of the parameter is @Nullable
232-
return NullabilityUtil.isArrayElementNullable(symbol.getParameters().get(paramInd), config);
230+
return NullabilityUtil.nullableVarargsElementsForSourceOrBytecode(
231+
symbol.getParameters().get(paramInd), config);
233232
} else {
234233
return hasNullableAnnotation(
235234
NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config);
@@ -269,9 +268,8 @@ public static boolean paramHasNonNullAnnotation(
269268
if (symbol.isVarArgs()
270269
&& paramInd == symbol.getParameters().size() - 1
271270
&& !config.isLegacyAnnotationLocation()) {
272-
// individual arguments passed in the varargs positions must be @NonNull if the array element
273-
// type of the parameter is @NonNull
274-
return NullabilityUtil.isArrayElementNonNull(symbol.getParameters().get(paramInd), config);
271+
return NullabilityUtil.nonnullVarargsElementsForSourceOrBytecode(
272+
symbol.getParameters().get(paramInd), config);
275273
} else {
276274
return hasNonNullAnnotation(
277275
NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config);

nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ private static NullnessStore methodInitialStore(
6767
for (LocalVariableNode param : parameters) {
6868
Symbol paramSymbol = (Symbol) param.getElement();
6969
Nullness assumed;
70+
// Using this flag to check for a varargs parameter works since we know paramSymbol represents
71+
// a parameter defined in source code
7072
if ((paramSymbol.flags() & Flags.VARARGS) != 0) {
7173
assumed = Nullness.varargsArrayIsNullable(paramSymbol, config) ? NULLABLE : NONNULL;
7274
} else {

nullaway/src/test/java/com/uber/nullaway/Java8Tests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,25 @@ public void methodReferenceOnNullableVariable() {
4747
"}")
4848
.doTest();
4949
}
50+
51+
/** test that we can properly read an explicit type-use annotation on a lambda parameter */
52+
@Test
53+
public void testNullableLambdaParamTypeUse() {
54+
defaultCompilationHelper
55+
.addSourceLines(
56+
"Test.java",
57+
"package com.uber;",
58+
"import org.jspecify.annotations.Nullable;",
59+
"class Test {",
60+
" @FunctionalInterface",
61+
" interface NullableParamFunctionTypeUse<T, U> {",
62+
" U takeVal(@Nullable T x);",
63+
" }",
64+
" static void testParamTypeUse() {",
65+
" NullableParamFunctionTypeUse n3 = (@Nullable Object x) -> (x == null) ? \"null\" : x.toString();",
66+
" NullableParamFunctionTypeUse n4 = (x) -> (x == null) ? \"null\" : x.toString();",
67+
" }",
68+
"}")
69+
.doTest();
70+
}
5071
}

nullaway/src/test/java/com/uber/nullaway/VarargsTests.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,41 @@ public void testNullableVarargs() {
7171
.doTest();
7272
}
7373

74+
/** Test for a @Nullable declaration annotation on a varargs parameter defined in bytecode */
75+
@Test
76+
public void nullableDeclarationVarArgsFromBytecode() {
77+
defaultCompilationHelper
78+
.addSourceLines(
79+
"Test.java",
80+
"package com.uber;",
81+
"import com.uber.lib.Varargs;",
82+
"public class Test {",
83+
" public void testDeclaration() {",
84+
" String x = null;",
85+
" Varargs s = new Varargs(x);",
86+
" String y = \"hello\", z = null;",
87+
" Varargs s2 = new Varargs(y, z);",
88+
" }",
89+
"}")
90+
.doTest();
91+
}
92+
93+
@Test
94+
public void nullableTypeUseVarArgsFromBytecode() {
95+
defaultCompilationHelper
96+
.addSourceLines(
97+
"Test.java",
98+
"package com.uber;",
99+
"import com.uber.lib.Varargs;",
100+
"public class Test {",
101+
" public void testTypeUse() {",
102+
" String[] x = null;",
103+
" Varargs.typeUse(x);",
104+
" }",
105+
"}")
106+
.doTest();
107+
}
108+
74109
@Test
75110
public void nullableTypeUseVarargs() {
76111
defaultCompilationHelper
@@ -515,4 +550,37 @@ public void testVarargsRestrictive() {
515550
"}")
516551
.doTest();
517552
}
553+
554+
/**
555+
* Test for a restrictive @NonNull declaration annotation on a varargs parameter defined in
556+
* bytecode
557+
*/
558+
@Test
559+
public void testVarargsRestrictiveBytecodes() {
560+
makeTestHelperWithArgs(
561+
Arrays.asList(
562+
"-d",
563+
temporaryFolder.getRoot().getAbsolutePath(),
564+
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
565+
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
566+
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
567+
.addSourceLines(
568+
"Test.java",
569+
"package com.uber;",
570+
"import com.uber.lib.unannotated.RestrictivelyAnnotatedVarargs;",
571+
"public class Test {",
572+
" public void testDeclaration() {",
573+
" String x = null;",
574+
" String[] y = null;",
575+
" // BUG: Diagnostic contains: passing @Nullable parameter 'x'",
576+
" RestrictivelyAnnotatedVarargs.test(x);",
577+
" RestrictivelyAnnotatedVarargs.test(y);",
578+
" // BUG: Diagnostic contains: passing @Nullable parameter 'x'",
579+
" RestrictivelyAnnotatedVarargs.testTypeUse(x);",
580+
" // TODO should report an error; requires a fuller fix for #1027",
581+
" RestrictivelyAnnotatedVarargs.testTypeUse(y);",
582+
" }",
583+
"}")
584+
.doTest();
585+
}
518586
}

test-java-lib/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ plugins {
2222

2323
dependencies {
2424
annotationProcessor project(":nullaway")
25+
implementation deps.test.jsr305Annotations
2526
implementation deps.build.jspecify
2627

2728
compileOnly deps.build.javaxValidation
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.uber.lib;
2+
3+
import javax.annotation.Nullable;
4+
5+
public class Varargs {
6+
7+
public Varargs(@Nullable String... args) {}
8+
9+
public static void typeUse(String @org.jspecify.annotations.Nullable ... args) {}
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.uber.lib.unannotated;
2+
3+
import javax.annotation.Nonnull;
4+
5+
public class RestrictivelyAnnotatedVarargs {
6+
7+
public static void test(@Nonnull String... args) {}
8+
9+
public static void testTypeUse(
10+
@org.jspecify.annotations.NonNull String @org.jspecify.annotations.NonNull ... args) {}
11+
}

0 commit comments

Comments
 (0)