1 /* 2 * Copyright 2014 Google Inc. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 // Based on com.google.errorprone.dataflow.nullnesspropagation.* 18 19 package com.uber.nullaway; 20 21 import com.sun.tools.javac.code.Symbol; 22 import com.sun.tools.javac.code.Type; 23 import com.sun.tools.javac.util.List; 24 import java.util.stream.Stream; 25 import javax.lang.model.element.AnnotationMirror; 26 import org.checkerframework.nullaway.dataflow.analysis.AbstractValue; 27 28 /** 29 * Represents one of the possible nullness values in our nullness analysis. 30 * 31 * @author deminguyen@google.com (Demi Nguyen) 32 */ 33 public enum Nullness implements AbstractValue<Nullness> { 34 35 /** 36 * The lattice for nullness looks like: 37 * 38 * <pre> 39 * Nullable 40 * / \ 41 * Null Non-null 42 * \ / 43 * Bottom 44 * </pre> 45 */ 46 NULLABLE("Nullable"), 47 NULL("Null"), 48 NONNULL("Non-null"), 49 BOTTOM("Bottom"); 50 51 private final String displayName; 52 Nullness(String displayName)53 Nullness(String displayName) { 54 this.displayName = displayName; 55 } 56 57 // The following leastUpperBound and greatestLowerBound methods were created by handwriting a 58 // truth table and then encoding the values into these functions. A better approach would be to 59 // represent the lattice directly and compute these functions from the lattice. 60 61 @Override leastUpperBound(Nullness other)62 public Nullness leastUpperBound(Nullness other) { 63 if (this == other) { 64 return this; 65 } 66 // Bottom loses. 67 if (this == BOTTOM) { 68 return other; 69 } 70 if (other == BOTTOM) { 71 return this; 72 } 73 // They disagree, and neither is bottom. 74 return NULLABLE; 75 } 76 greatestLowerBound(Nullness other)77 public Nullness greatestLowerBound(Nullness other) { 78 if (this == other) { 79 return this; 80 } 81 // Nullable loses. 82 if (this == NULLABLE) { 83 return other; 84 } 85 if (other == NULLABLE) { 86 return this; 87 } 88 // They disagree, and neither is nullable. 89 return BOTTOM; 90 } 91 92 /** 93 * Returns the {@code Nullness} that corresponds to what you can deduce by knowing that some 94 * expression is not equal to another expression with this {@code Nullness}. 95 * 96 * <p>A {@code Nullness} represents a set of possible values for a expression. Suppose you have 97 * two variables {@code var1} and {@code var2}. If {@code var1 != var2}, then {@code var1} must be 98 * an element of the complement of the singleton set containing the value of {@code var2}. If you 99 * union these complement sets over all possible values of {@code var2}, the set that results is 100 * what this method returns, assuming that {@code this} is the {@code Nullness} of {@code var2}. 101 * 102 * <p>Example 1: Suppose {@code nv2 == NULL}. Then {@code var2} can have exactly one value, {@code 103 * null}, and {@code var1} must have a value in the set of all values except {@code null}. That 104 * set is exactly {@code NONNULL}. 105 * 106 * <p>Example 2: Suppose {@code nv2 == NONNULL}. Then {@code var2} can have any value except 107 * {@code null}. Suppose {@code var2} has value {@code "foo"}. Then {@code var1} must have a value 108 * in the set of all values except {@code "foo"}. Now suppose {@code var2} has value {@code "bar"} 109 * . Then {@code var1} must have a value in set of all values except {@code "bar"}. Since we don't 110 * know which value in the set {@code NONNULL var2} has, we union all possible complement sets to 111 * get the set of all values, or {@code NULLABLE}. 112 */ deducedValueWhenNotEqual()113 public Nullness deducedValueWhenNotEqual() { 114 switch (this) { 115 case NULLABLE: 116 return NULLABLE; 117 case NONNULL: 118 return NULLABLE; 119 case NULL: 120 return NONNULL; 121 case BOTTOM: 122 return BOTTOM; 123 default: 124 throw new AssertionError("Inverse of " + this + " not defined"); 125 } 126 } 127 128 @Override toString()129 public String toString() { 130 return displayName; 131 } 132 hasNullableAnnotation( Stream<? extends AnnotationMirror> annotations, Config config)133 public static boolean hasNullableAnnotation( 134 Stream<? extends AnnotationMirror> annotations, Config config) { 135 return annotations 136 .map(anno -> anno.getAnnotationType().toString()) 137 .anyMatch(anno -> isNullableAnnotation(anno, config)); 138 } 139 hasNonNullAnnotation( Stream<? extends AnnotationMirror> annotations, Config config)140 public static boolean hasNonNullAnnotation( 141 Stream<? extends AnnotationMirror> annotations, Config config) { 142 return annotations 143 .map(anno -> anno.getAnnotationType().toString()) 144 .anyMatch(anno -> isNonNullAnnotation(anno, config)); 145 } 146 147 /** 148 * Check whether an annotation should be treated as equivalent to <code>@Nullable</code>. 149 * 150 * @param annotName annotation name 151 * @return true if we treat annotName as a <code>@Nullable</code> annotation, false otherwise 152 */ isNullableAnnotation(String annotName, Config config)153 public static boolean isNullableAnnotation(String annotName, Config config) { 154 return annotName.endsWith(".Nullable") 155 // endsWith and not equals and no `org.`, because gradle's shadow plug in rewrites strings 156 // and will replace `org.checkerframework` with `shadow.checkerframework`. Yes, really... 157 // I assume it's something to handle reflection. 158 || annotName.endsWith(".checkerframework.checker.nullness.compatqual.NullableDecl") 159 // matches javax.annotation.CheckForNull and edu.umd.cs.findbugs.annotations.CheckForNull 160 || annotName.endsWith(".CheckForNull") 161 // matches any of the multiple @ParametricNullness annotations used within Guava 162 // (see https://github.com/google/guava/issues/6126) 163 // We check the simple name first and the package prefix second for boolean short 164 // circuiting, as Guava includes 165 // many annotations 166 || (annotName.endsWith(".ParametricNullness") && annotName.startsWith("com.google.common.")) 167 || (config.acknowledgeAndroidRecent() 168 && annotName.equals("androidx.annotation.RecentlyNullable")) 169 || config.isCustomNullableAnnotation(annotName); 170 } 171 172 /** 173 * Check whether an annotation should be treated as equivalent to <code>@NonNull</code>. 174 * 175 * @param annotName annotation name 176 * @return true if we treat annotName as a <code>@NonNull</code> annotation, false otherwise 177 */ isNonNullAnnotation(String annotName, Config config)178 private static boolean isNonNullAnnotation(String annotName, Config config) { 179 return annotName.endsWith(".NonNull") 180 || annotName.endsWith(".NotNull") 181 || annotName.endsWith(".Nonnull") 182 || (config.acknowledgeAndroidRecent() 183 && annotName.equals("androidx.annotation.RecentlyNonNull")) 184 || config.isCustomNonnullAnnotation(annotName); 185 } 186 187 /** 188 * Does the symbol have a {@code @NonNull} declaration or type-use annotation? 189 * 190 * <p>NOTE: this method does not work for checking all annotations of parameters of methods from 191 * class files. For that case, use {@link #paramHasNullableAnnotation(Symbol.MethodSymbol, int, 192 * Config)} 193 */ hasNonNullAnnotation(Symbol symbol, Config config)194 public static boolean hasNonNullAnnotation(Symbol symbol, Config config) { 195 return hasNonNullAnnotation(NullabilityUtil.getAllAnnotations(symbol, config), config); 196 } 197 198 /** 199 * Does the symbol have a {@code @Nullable} declaration or type-use annotation? 200 * 201 * <p>NOTE: this method does not work for checking all annotations of parameters of methods from 202 * class files. For that case, use {@link #paramHasNullableAnnotation(Symbol.MethodSymbol, int, 203 * Config)} 204 */ hasNullableAnnotation(Symbol symbol, Config config)205 public static boolean hasNullableAnnotation(Symbol symbol, Config config) { 206 return hasNullableAnnotation(NullabilityUtil.getAllAnnotations(symbol, config), config); 207 } 208 209 /** 210 * Does the parameter of {@code symbol} at {@code paramInd} have a {@code @Nullable} declaration 211 * or type-use annotation? This method works for methods defined in either source or class files. 212 */ paramHasNullableAnnotation( Symbol.MethodSymbol symbol, int paramInd, Config config)213 public static boolean paramHasNullableAnnotation( 214 Symbol.MethodSymbol symbol, int paramInd, Config config) { 215 // We treat the (generated) equals() method of record types to have a @Nullable parameter, as 216 // the generated implementation handles null (as required by the contract of Object.equals()) 217 if (isRecordEqualsParam(symbol, paramInd)) { 218 return true; 219 } 220 return hasNullableAnnotation( 221 NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config); 222 } 223 isRecordEqualsParam(Symbol.MethodSymbol symbol, int paramInd)224 private static boolean isRecordEqualsParam(Symbol.MethodSymbol symbol, int paramInd) { 225 // Here we compare with toString() to preserve compatibility with JDK 11 (records only 226 // introduced in JDK 16) 227 if (!symbol.owner.getKind().toString().equals("RECORD")) { 228 return false; 229 } 230 if (!symbol.getSimpleName().contentEquals("equals")) { 231 return false; 232 } 233 // Check for a boolean return type and a single parameter of type java.lang.Object 234 Type type = symbol.type; 235 List<Type> parameterTypes = type.getParameterTypes(); 236 if (!(type.getReturnType().toString().equals("boolean") 237 && parameterTypes != null 238 && parameterTypes.size() == 1 239 && parameterTypes.get(0).toString().equals("java.lang.Object"))) { 240 return false; 241 } 242 return paramInd == 0; 243 } 244 245 /** 246 * Does the parameter of {@code symbol} at {@code paramInd} have a {@code @NonNull} declaration or 247 * type-use annotation? This method works for methods defined in either source or class files. 248 */ paramHasNonNullAnnotation( Symbol.MethodSymbol symbol, int paramInd, Config config)249 public static boolean paramHasNonNullAnnotation( 250 Symbol.MethodSymbol symbol, int paramInd, Config config) { 251 return hasNonNullAnnotation( 252 NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd, config), config); 253 } 254 } 255