Skip to content

Commit cc5ef65

Browse files
armughan11msridhar
andauthored
Enforce correct type-use annotation locations for nested types (#1045)
Enforce correct type-use annotation locations for nested types as per JSpecify norms. We enforce type-use annotations to be on the inner class and raise an error if they are not. For annotations that are both type use and declaration, we raise an error at an invalid location. **Current behavior** ``` // All three are ok @nullable A.B.C foo1 = null; A.@nullable B.C foo2 = null; A.B.@nullable C foo3 = null; ``` **New behavior** ``` // BUG: Diagnostic contains: Type-use nullability annotations should be applied on inner class @nullable A.B.C foo1 = null; // BUG: Diagnostic contains: Type-use nullability annotations should be applied on inner class A.@nullable B.C foo2 = null; A.B.@nullable C foo3 = null; ``` For annotations which are both declaration and annotation and type-use, only `foo2` throws an error since the location isn't apt for either scenarios --------- Co-authored-by: Manu Sridharan <[email protected]>
1 parent 9eea2be commit cc5ef65

File tree

4 files changed

+298
-26
lines changed

4 files changed

+298
-26
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public enum MessageTypes {
5959
WRONG_OVERRIDE_RETURN_GENERIC,
6060
WRONG_OVERRIDE_PARAM_GENERIC,
6161
ASSIGN_NULLABLE_TO_NONNULL_ARRAY,
62+
NULLABLE_ON_WRONG_NESTED_CLASS_LEVEL
6263
}
6364

6465
public String getMessage() {

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
import static com.uber.nullaway.ErrorBuilder.errMsgForInitializer;
3434
import static com.uber.nullaway.NullabilityUtil.castToNonNull;
3535
import static com.uber.nullaway.NullabilityUtil.isArrayElementNullable;
36+
import static com.uber.nullaway.Nullness.isNullableAnnotation;
37+
import static java.lang.annotation.ElementType.TYPE_PARAMETER;
38+
import static java.lang.annotation.ElementType.TYPE_USE;
3639

3740
import com.google.auto.service.AutoService;
3841
import com.google.auto.value.AutoValue;
@@ -53,6 +56,8 @@
5356
import com.google.errorprone.matchers.Matchers;
5457
import com.google.errorprone.suppliers.Suppliers;
5558
import com.google.errorprone.util.ASTHelpers;
59+
import com.sun.source.tree.AnnotatedTypeTree;
60+
import com.sun.source.tree.AnnotationTree;
5661
import com.sun.source.tree.ArrayAccessTree;
5762
import com.sun.source.tree.AssignmentTree;
5863
import com.sun.source.tree.BinaryTree;
@@ -98,6 +103,8 @@
98103
import com.uber.nullaway.handlers.Handler;
99104
import com.uber.nullaway.handlers.Handlers;
100105
import com.uber.nullaway.handlers.MethodAnalysisContext;
106+
import java.lang.annotation.ElementType;
107+
import java.lang.annotation.Target;
101108
import java.util.ArrayList;
102109
import java.util.LinkedHashMap;
103110
import java.util.LinkedHashSet;
@@ -187,6 +194,8 @@ public class NullAway extends BugChecker
187194
static final String CORE_CHECK_NAME = "NullAway.<core>";
188195

189196
private static final Matcher<ExpressionTree> THIS_MATCHER = NullAway::isThisIdentifierMatcher;
197+
private static final ImmutableSet<ElementType> TYPE_USE_OR_TYPE_PARAMETER =
198+
ImmutableSet.of(TYPE_USE, TYPE_PARAMETER);
190199

191200
private final Predicate<MethodInvocationNode> nonAnnotatedMethod;
192201

@@ -570,6 +579,11 @@ public Description matchMemberSelect(MemberSelectTree tree, VisitorState state)
570579
|| symbol instanceof ModuleElement) {
571580
return Description.NO_MATCH;
572581
}
582+
if ((tree.getExpression() instanceof AnnotatedTypeTree)
583+
&& !config.isLegacyAnnotationLocation()) {
584+
checkNullableAnnotationPositionInType(
585+
((AnnotatedTypeTree) tree.getExpression()).getAnnotations(), tree, state);
586+
}
573587

574588
Description badDeref = matchDereference(tree.getExpression(), tree, state);
575589
if (!badDeref.equals(Description.NO_MATCH)) {
@@ -638,6 +652,10 @@ public Description matchMethod(MethodTree tree, VisitorState state) {
638652
if (!withinAnnotatedCode(state)) {
639653
return Description.NO_MATCH;
640654
}
655+
if (!config.isLegacyAnnotationLocation()) {
656+
checkNullableAnnotationPositionInType(
657+
tree.getModifiers().getAnnotations(), tree.getReturnType(), state);
658+
}
641659
// if the method is overriding some other method,
642660
// check that nullability annotations are consistent with
643661
// overridden method (if overridden method is in an annotated
@@ -1464,6 +1482,10 @@ public Description matchVariable(VariableTree tree, VisitorState state) {
14641482
if (tree.getInitializer() != null && config.isJSpecifyMode()) {
14651483
GenericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state);
14661484
}
1485+
if (!config.isLegacyAnnotationLocation()) {
1486+
checkNullableAnnotationPositionInType(
1487+
tree.getModifiers().getAnnotations(), tree.getType(), state);
1488+
}
14671489

14681490
if (symbol.type.isPrimitive() && tree.getInitializer() != null) {
14691491
doUnboxingCheck(state, tree.getInitializer());
@@ -1487,6 +1509,85 @@ public Description matchVariable(VariableTree tree, VisitorState state) {
14871509
return Description.NO_MATCH;
14881510
}
14891511

1512+
/**
1513+
* returns true if {@code anno} is a type use annotation; it may also be a declaration annotation
1514+
*/
1515+
private static boolean isTypeUseAnnotation(Symbol anno) {
1516+
Target target = anno.getAnnotation(Target.class);
1517+
ImmutableSet<ElementType> elementTypes =
1518+
target == null ? ImmutableSet.of() : ImmutableSet.copyOf(target.value());
1519+
return elementTypes.contains(TYPE_USE);
1520+
}
1521+
1522+
/**
1523+
* returns true if {@code anno} is a declaration annotation; it may also be a type use annotation
1524+
*/
1525+
private static boolean isDeclarationAnnotation(Symbol anno) {
1526+
Target target = anno.getAnnotation(Target.class);
1527+
if (target == null) {
1528+
return true;
1529+
}
1530+
ImmutableSet<ElementType> elementTypes = ImmutableSet.copyOf(target.value());
1531+
// Return true for any annotation that is not exclusively a type-use annotation
1532+
return !(elementTypes.equals(ImmutableSet.of(ElementType.TYPE_USE))
1533+
|| TYPE_USE_OR_TYPE_PARAMETER.containsAll(elementTypes));
1534+
}
1535+
1536+
/**
1537+
* Checks whether any {@code @Nullable} annotation is at the right location for nested types.
1538+
* Raises an error iff the type is a field access expression (for an inner class type), the
1539+
* annotation is type use, and the annotation is not applied on the innermost type.
1540+
*
1541+
* @param annotations The annotations to check
1542+
* @param type The tree representing the type structure
1543+
* @param state The visitor state
1544+
*/
1545+
private void checkNullableAnnotationPositionInType(
1546+
List<? extends AnnotationTree> annotations, Tree type, VisitorState state) {
1547+
1548+
// Early return if the type is not a nested or inner class reference.
1549+
if (!(type instanceof MemberSelectTree)) {
1550+
return;
1551+
}
1552+
1553+
// Get the end position of the outer type expression. Any nullable annotation before this
1554+
// position is considered to be on the outer type, which is incorrect.
1555+
int endOfOuterType = state.getEndPosition(((MemberSelectTree) type).getExpression());
1556+
int startOfType = ((JCTree) type).getStartPosition();
1557+
1558+
for (AnnotationTree annotation : annotations) {
1559+
Symbol sym = ASTHelpers.getSymbol(annotation);
1560+
if (sym == null) {
1561+
continue;
1562+
}
1563+
1564+
String qualifiedName = sym.getQualifiedName().toString();
1565+
if (!isNullableAnnotation(qualifiedName, config)) {
1566+
continue;
1567+
}
1568+
1569+
if (!isTypeUseAnnotation(sym)) {
1570+
continue;
1571+
}
1572+
// If an annotation is declaration ALSO, we check if it is at the correct location. If it is,
1573+
// we treat it as declaration and skip the checks.
1574+
if (isDeclarationAnnotation(sym) && state.getEndPosition(annotation) <= startOfType) {
1575+
continue;
1576+
}
1577+
1578+
if (state.getEndPosition(annotation) < endOfOuterType) {
1579+
// annotation is not on the inner-most type
1580+
ErrorMessage errorMessage =
1581+
new ErrorMessage(
1582+
MessageTypes.NULLABLE_ON_WRONG_NESTED_CLASS_LEVEL,
1583+
"Type-use nullability annotations should be applied on inner class");
1584+
1585+
state.reportMatch(
1586+
errorBuilder.createErrorDescription(errorMessage, buildDescription(type), state, null));
1587+
}
1588+
}
1589+
}
1590+
14901591
/**
14911592
* Check if an inner class's annotation means this Compilation Unit is partially annotated.
14921593
*

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

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import com.sun.tools.javac.code.Type;
4545
import com.sun.tools.javac.code.TypeAnnotationPosition;
4646
import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry;
47+
import com.sun.tools.javac.code.TypeTag;
4748
import com.sun.tools.javac.code.Types;
4849
import com.sun.tools.javac.tree.JCTree;
4950
import com.sun.tools.javac.util.JCDiagnostic;
@@ -293,7 +294,7 @@ public static Stream<? extends AnnotationMirror> getAllAnnotationsForParameter(
293294
t ->
294295
t.position.type.equals(TargetType.METHOD_FORMAL_PARAMETER)
295296
&& t.position.parameter_index == paramInd
296-
&& NullabilityUtil.isDirectTypeUseAnnotation(t, config)));
297+
&& NullabilityUtil.isDirectTypeUseAnnotation(t, symbol, config)));
297298
}
298299

299300
/**
@@ -308,10 +309,11 @@ public static Stream<? extends AnnotationMirror> getTypeUseAnnotations(
308309
return rawTypeAttributes.filter(
309310
(t) ->
310311
t.position.type.equals(TargetType.METHOD_RETURN)
311-
&& isDirectTypeUseAnnotation(t, config));
312+
&& isDirectTypeUseAnnotation(t, symbol, config));
312313
} else {
313314
// filter for annotations directly on the type
314-
return rawTypeAttributes.filter(t -> NullabilityUtil.isDirectTypeUseAnnotation(t, config));
315+
return rawTypeAttributes.filter(
316+
t -> NullabilityUtil.isDirectTypeUseAnnotation(t, symbol, config));
315317
}
316318
}
317319

@@ -323,11 +325,13 @@ public static Stream<? extends AnnotationMirror> getTypeUseAnnotations(
323325
* but {@code List<@Nullable T> lst} is not.
324326
*
325327
* @param t the annotation and its position in the type
328+
* @param symbol the symbol for the annotated element
326329
* @param config NullAway configuration
327330
* @return {@code true} if the annotation should be treated as applying directly to the top-level
328331
* type, false otherwise
329332
*/
330-
private static boolean isDirectTypeUseAnnotation(Attribute.TypeCompound t, Config config) {
333+
private static boolean isDirectTypeUseAnnotation(
334+
Attribute.TypeCompound t, Symbol symbol, Config config) {
331335
// location is a list of TypePathEntry objects, indicating whether the annotation is
332336
// on an array, inner type, wildcard, or type argument. If it's empty, then the
333337
// annotation is directly on the type.
@@ -340,6 +344,9 @@ private static boolean isDirectTypeUseAnnotation(Attribute.TypeCompound t, Confi
340344
// In JSpecify mode and without the LegacyAnnotationLocations flag, annotations on array
341345
// dimensions are *not* treated as applying to the top-level type, consistent with the JSpecify
342346
// spec.
347+
// Annotations which are *not* on the inner type are not treated as being applied to the inner
348+
// type. This can be bypassed the LegacyAnnotationLocations flag, in which
349+
// annotations on all locations are treated as applying to the inner type.
343350
// We don't allow mixing of inner types and array dimensions in the same location
344351
// (i.e. `Foo.@Nullable Bar []` is meaningless).
345352
// These aren't correct semantics for type use annotations, but a series of hacky
@@ -349,10 +356,13 @@ private static boolean isDirectTypeUseAnnotation(Attribute.TypeCompound t, Confi
349356
// See https://github.com/uber/NullAway/issues/708
350357
boolean locationHasInnerTypes = false;
351358
boolean locationHasArray = false;
359+
int innerTypeCount = 0;
360+
int nestingDepth = getNestingDepth(symbol.type);
352361
for (TypePathEntry entry : t.position.location) {
353362
switch (entry.tag) {
354363
case INNER_TYPE:
355364
locationHasInnerTypes = true;
365+
innerTypeCount++;
356366
break;
357367
case ARRAY:
358368
if (config.isJSpecifyMode() || !config.isLegacyAnnotationLocation()) {
@@ -367,8 +377,30 @@ private static boolean isDirectTypeUseAnnotation(Attribute.TypeCompound t, Confi
367377
return false;
368378
}
369379
}
370-
// Make sure it's not a mix of inner types and arrays for this annotation's location
371-
return !(locationHasInnerTypes && locationHasArray);
380+
if (config.isLegacyAnnotationLocation()) {
381+
// Make sure it's not a mix of inner types and arrays for this annotation's location
382+
return !(locationHasInnerTypes && locationHasArray);
383+
}
384+
// For non-nested classes annotations apply to the innermost type.
385+
if (!isTypeOfNestedClass(symbol.type)) {
386+
return true;
387+
}
388+
// For nested classes the annotation is only valid if it is on the innermost type.
389+
return innerTypeCount == nestingDepth - 1;
390+
}
391+
392+
private static int getNestingDepth(Type type) {
393+
int depth = 0;
394+
for (Type curr = type;
395+
curr != null && !curr.hasTag(TypeTag.NONE);
396+
curr = curr.getEnclosingType()) {
397+
depth++;
398+
}
399+
return depth;
400+
}
401+
402+
private static boolean isTypeOfNestedClass(Type type) {
403+
return type.tsym != null && type.tsym.owner instanceof Symbol.ClassSymbol;
372404
}
373405

374406
/**

0 commit comments

Comments
 (0)