1 /* 2 * Copyright (c) 2017 Uber Technologies, Inc. 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 * THE SOFTWARE. 21 */ 22 23 package com.uber.nullaway; 24 25 import com.google.common.base.Preconditions; 26 import com.google.common.collect.ImmutableSet; 27 import com.google.errorprone.VisitorState; 28 import com.google.errorprone.suppliers.Supplier; 29 import com.google.errorprone.suppliers.Suppliers; 30 import com.google.errorprone.util.ASTHelpers; 31 import com.sun.source.tree.BlockTree; 32 import com.sun.source.tree.ClassTree; 33 import com.sun.source.tree.ExpressionTree; 34 import com.sun.source.tree.LambdaExpressionTree; 35 import com.sun.source.tree.MemberReferenceTree; 36 import com.sun.source.tree.MethodTree; 37 import com.sun.source.tree.Tree; 38 import com.sun.source.tree.VariableTree; 39 import com.sun.source.util.TreePath; 40 import com.sun.tools.javac.code.Attribute; 41 import com.sun.tools.javac.code.Symbol; 42 import com.sun.tools.javac.code.TargetType; 43 import com.sun.tools.javac.code.Type; 44 import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry; 45 import com.sun.tools.javac.code.Types; 46 import com.sun.tools.javac.tree.JCTree; 47 import com.sun.tools.javac.util.JCDiagnostic; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Set; 51 import java.util.stream.Collectors; 52 import java.util.stream.Stream; 53 import javax.annotation.Nullable; 54 import javax.lang.model.element.AnnotationMirror; 55 import javax.lang.model.element.AnnotationValue; 56 import javax.lang.model.element.ExecutableElement; 57 import org.checkerframework.nullaway.javacutil.AnnotationUtils; 58 59 /** Helpful utility methods for nullability analysis. */ 60 public class NullabilityUtil { 61 public static final String NULLMARKED_SIMPLE_NAME = "NullMarked"; 62 public static final String NULLUNMARKED_SIMPLE_NAME = "NullUnmarked"; 63 64 private static final Supplier<Type> MAP_TYPE_SUPPLIER = Suppliers.typeFromString("java.util.Map"); 65 NullabilityUtil()66 private NullabilityUtil() {} 67 68 /** 69 * finds the corresponding functional interface method for a lambda expression or method reference 70 * 71 * @param tree the lambda expression or method reference 72 * @return the functional interface method 73 */ getFunctionalInterfaceMethod(ExpressionTree tree, Types types)74 public static Symbol.MethodSymbol getFunctionalInterfaceMethod(ExpressionTree tree, Types types) { 75 Preconditions.checkArgument( 76 (tree instanceof LambdaExpressionTree) || (tree instanceof MemberReferenceTree)); 77 Type funcInterfaceType = ((JCTree.JCFunctionalExpression) tree).type; 78 return (Symbol.MethodSymbol) types.findDescriptorSymbol(funcInterfaceType.tsym); 79 } 80 81 /** 82 * determines whether a lambda parameter is missing an explicit type declaration 83 * 84 * @param lambdaParameter the parameter 85 * @return true if there is no type declaration, false otherwise 86 */ lambdaParamIsImplicitlyTyped(VariableTree lambdaParameter)87 public static boolean lambdaParamIsImplicitlyTyped(VariableTree lambdaParameter) { 88 // kind of a hack; the "preferred position" seems to be the position 89 // of the variable name. if this differs from the start position, it 90 // means there is an explicit type declaration 91 JCDiagnostic.DiagnosticPosition diagnosticPosition = 92 (JCDiagnostic.DiagnosticPosition) lambdaParameter; 93 return diagnosticPosition.getStartPosition() == diagnosticPosition.getPreferredPosition(); 94 } 95 96 /** 97 * find the closest ancestor method in a superclass or superinterface that method overrides 98 * 99 * @param method the subclass method 100 * @param types the types data structure from javac 101 * @return closest overridden ancestor method, or <code>null</code> if method does not override 102 * anything 103 */ 104 @Nullable getClosestOverriddenMethod( Symbol.MethodSymbol method, Types types)105 public static Symbol.MethodSymbol getClosestOverriddenMethod( 106 Symbol.MethodSymbol method, Types types) { 107 // taken from Error Prone MethodOverrides check 108 Symbol.ClassSymbol owner = method.enclClass(); 109 for (Type s : types.closure(owner.type)) { 110 if (types.isSameType(s, owner.type)) { 111 continue; 112 } 113 for (Symbol m : s.tsym.members().getSymbolsByName(method.name)) { 114 if (!(m instanceof Symbol.MethodSymbol)) { 115 continue; 116 } 117 Symbol.MethodSymbol msym = (Symbol.MethodSymbol) m; 118 if (msym.isStatic()) { 119 continue; 120 } 121 if (method.overrides(msym, owner, types, /*checkReturn*/ false)) { 122 return msym; 123 } 124 } 125 } 126 return null; 127 } 128 129 /** 130 * find the enclosing method, lambda expression or initializer block for the leaf of some tree 131 * path 132 * 133 * @param path the tree path 134 * @param others also stop and return in case of any of these tree kinds 135 * @return the closest enclosing method / lambda 136 */ 137 @Nullable findEnclosingMethodOrLambdaOrInitializer( TreePath path, ImmutableSet<Tree.Kind> others)138 public static TreePath findEnclosingMethodOrLambdaOrInitializer( 139 TreePath path, ImmutableSet<Tree.Kind> others) { 140 TreePath curPath = path.getParentPath(); 141 while (curPath != null) { 142 if (curPath.getLeaf() instanceof MethodTree 143 || curPath.getLeaf() instanceof LambdaExpressionTree 144 || others.contains(curPath.getLeaf().getKind())) { 145 return curPath; 146 } 147 TreePath parent = curPath.getParentPath(); 148 if (parent != null && parent.getLeaf() instanceof ClassTree) { 149 if (curPath.getLeaf() instanceof BlockTree) { 150 // found initializer block 151 return curPath; 152 } 153 if (curPath.getLeaf() instanceof VariableTree 154 && ((VariableTree) curPath.getLeaf()).getInitializer() != null) { 155 // found field with an inline initializer 156 return curPath; 157 } 158 } 159 curPath = parent; 160 } 161 return null; 162 } 163 164 /** 165 * find the enclosing method, lambda expression or initializer block for the leaf of some tree 166 * path 167 * 168 * @param path the tree path 169 * @return the closest enclosing method / lambda 170 */ 171 @Nullable findEnclosingMethodOrLambdaOrInitializer(TreePath path)172 public static TreePath findEnclosingMethodOrLambdaOrInitializer(TreePath path) { 173 return findEnclosingMethodOrLambdaOrInitializer(path, ImmutableSet.of()); 174 } 175 176 /** 177 * NOTE: this method does not work for getting all annotations of parameters of methods from class 178 * files. For that case, use {@link #getAllAnnotationsForParameter(Symbol.MethodSymbol, int, 179 * Config)} 180 * 181 * @param symbol the symbol 182 * @return all annotations on the symbol and on the type of the symbol 183 */ getAllAnnotations(Symbol symbol, Config config)184 public static Stream<? extends AnnotationMirror> getAllAnnotations(Symbol symbol, Config config) { 185 // for methods, we care about annotations on the return type, not on the method type itself 186 Stream<? extends AnnotationMirror> typeUseAnnotations = getTypeUseAnnotations(symbol, config); 187 return Stream.concat(symbol.getAnnotationMirrors().stream(), typeUseAnnotations); 188 } 189 190 /** 191 * Retrieve the {@code value} attribute of a method annotation of some type. 192 * 193 * @param methodSymbol A method to check for the annotation. 194 * @param annotName The qualified name of the annotation. 195 * @return The {@code value} attribute of the annotation, or {@code null} if the annotation is not 196 * present. 197 */ getAnnotationValue( Symbol.MethodSymbol methodSymbol, String annotName)198 public static @Nullable String getAnnotationValue( 199 Symbol.MethodSymbol methodSymbol, String annotName) { 200 AnnotationMirror annot = 201 AnnotationUtils.getAnnotationByName(methodSymbol.getAnnotationMirrors(), annotName); 202 if (annot == null) { 203 return null; 204 } 205 206 Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = 207 annot.getElementValues(); 208 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : 209 elementValues.entrySet()) { 210 ExecutableElement elem = entry.getKey(); 211 if (elem.getSimpleName().contentEquals("value")) { 212 return (String) entry.getValue().getValue(); 213 } 214 } 215 // not found 216 return null; 217 } 218 219 /** 220 * Retrieve the {@code value} attribute of a method annotation of some type where the {@code 221 * value} is an array. 222 * 223 * @param methodSymbol A method to check for the annotation. 224 * @param annotName The qualified name or simple name of the annotation depending on the value of 225 * {@code exactMatch}. 226 * @param exactMatch If true, the annotation name must match the full qualified name given in 227 * {@code annotName}, otherwise, simple names will be checked. 228 * @return The {@code value} attribute of the annotation as a {@code Set}, or {@code null} if the 229 * annotation is not present. 230 */ getAnnotationValueArray( Symbol.MethodSymbol methodSymbol, String annotName, boolean exactMatch)231 public static @Nullable Set<String> getAnnotationValueArray( 232 Symbol.MethodSymbol methodSymbol, String annotName, boolean exactMatch) { 233 AnnotationMirror annot = null; 234 for (AnnotationMirror annotationMirror : methodSymbol.getAnnotationMirrors()) { 235 String name = AnnotationUtils.annotationName(annotationMirror); 236 if ((exactMatch && name.equals(annotName)) || (!exactMatch && name.endsWith(annotName))) { 237 annot = annotationMirror; 238 break; 239 } 240 } 241 if (annot == null) { 242 return null; 243 } 244 Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = 245 annot.getElementValues(); 246 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : 247 elementValues.entrySet()) { 248 ExecutableElement elem = entry.getKey(); 249 if (elem.getSimpleName().contentEquals("value")) { 250 @SuppressWarnings("unchecked") 251 List<AnnotationValue> values = (List<AnnotationValue>) entry.getValue().getValue(); 252 return values.stream().map((av) -> ((String) av.getValue())).collect(Collectors.toSet()); 253 } 254 } 255 return null; 256 } 257 258 /** 259 * Works for method parameters defined either in source or in class files 260 * 261 * @param symbol the method symbol 262 * @param paramInd index of the parameter 263 * @param config NullAway configuration 264 * @return all declaration and type-use annotations for the parameter 265 */ getAllAnnotationsForParameter( Symbol.MethodSymbol symbol, int paramInd, Config config)266 public static Stream<? extends AnnotationMirror> getAllAnnotationsForParameter( 267 Symbol.MethodSymbol symbol, int paramInd, Config config) { 268 Symbol.VarSymbol varSymbol = symbol.getParameters().get(paramInd); 269 return Stream.concat( 270 varSymbol.getAnnotationMirrors().stream(), 271 symbol.getRawTypeAttributes().stream() 272 .filter( 273 t -> 274 t.position.type.equals(TargetType.METHOD_FORMAL_PARAMETER) 275 && t.position.parameter_index == paramInd 276 && NullabilityUtil.isDirectTypeUseAnnotation(t, config))); 277 } 278 279 /** 280 * Gets the type use annotations on a symbol, ignoring annotations on components of the type (type 281 * arguments, wildcards, etc.) 282 */ getTypeUseAnnotations( Symbol symbol, Config config)283 private static Stream<? extends AnnotationMirror> getTypeUseAnnotations( 284 Symbol symbol, Config config) { 285 Stream<Attribute.TypeCompound> rawTypeAttributes = symbol.getRawTypeAttributes().stream(); 286 if (symbol instanceof Symbol.MethodSymbol) { 287 // for methods, we want annotations on the return type 288 return rawTypeAttributes.filter( 289 (t) -> 290 t.position.type.equals(TargetType.METHOD_RETURN) 291 && isDirectTypeUseAnnotation(t, config)); 292 } else { 293 // filter for annotations directly on the type 294 return rawTypeAttributes.filter(t -> NullabilityUtil.isDirectTypeUseAnnotation(t, config)); 295 } 296 } 297 298 /** 299 * Check whether a type-use annotation should be treated as applying directly to the top-level 300 * type 301 * 302 * <p>For example {@code @Nullable List<T> lst} is a direct type use annotation of {@code lst}, 303 * but {@code List<@Nullable T> lst} is not. 304 * 305 * @param t the annotation and its position in the type 306 * @param config NullAway configuration 307 * @return {@code true} if the annotation should be treated as applying directly to the top-level 308 * type, false otherwise 309 */ isDirectTypeUseAnnotation(Attribute.TypeCompound t, Config config)310 private static boolean isDirectTypeUseAnnotation(Attribute.TypeCompound t, Config config) { 311 // location is a list of TypePathEntry objects, indicating whether the annotation is 312 // on an array, inner type, wildcard, or type argument. If it's empty, then the 313 // annotation is directly on the type. 314 // We care about both annotations directly on the outer type and also those directly 315 // on an inner type or array dimension, but wish to discard annotations on wildcards, 316 // or type arguments. 317 // For arrays, outside JSpecify mode, we treat annotations on the outer type and on any 318 // dimension of the array as applying to the nullability of the array itself, not the elements. 319 // In JSpecify mode, annotations on array dimensions are *not* treated as applying to the 320 // top-level type, consistent with the JSpecify spec. 321 // We don't allow mixing of inner types and array dimensions in the same location 322 // (i.e. `Foo.@Nullable Bar []` is meaningless). 323 // These aren't correct semantics for type use annotations, but a series of hacky 324 // compromises to keep some semblance of backwards compatibility until we can do a 325 // proper deprecation of the incorrect behaviors for type use annotations when their 326 // semantics don't match those of a declaration annotation in the same position. 327 // See https://github.com/uber/NullAway/issues/708 328 boolean locationHasInnerTypes = false; 329 boolean locationHasArray = false; 330 for (TypePathEntry entry : t.position.location) { 331 switch (entry.tag) { 332 case INNER_TYPE: 333 locationHasInnerTypes = true; 334 break; 335 case ARRAY: 336 if (config.isJSpecifyMode()) { 337 // In JSpecify mode, annotations on array element types do not apply to the top-level 338 // type 339 return false; 340 } 341 locationHasArray = true; 342 break; 343 default: 344 // Wildcard or type argument! 345 return false; 346 } 347 } 348 // Make sure it's not a mix of inner types and arrays for this annotation's location 349 return !(locationHasInnerTypes && locationHasArray); 350 } 351 352 /** 353 * Check if a field might be null, based on the type. 354 * 355 * @param symbol symbol for field 356 * @param config NullAway config 357 * @return true if based on the type, package, and name of the field, the analysis should assume 358 * the field might be null; false otherwise 359 */ mayBeNullFieldFromType( Symbol symbol, Config config, CodeAnnotationInfo codeAnnotationInfo)360 public static boolean mayBeNullFieldFromType( 361 Symbol symbol, Config config, CodeAnnotationInfo codeAnnotationInfo) { 362 return !(symbol.getSimpleName().toString().equals("class") 363 || symbol.isEnum() 364 || codeAnnotationInfo.isSymbolUnannotated(symbol, config)) 365 && Nullness.hasNullableAnnotation(symbol, config); 366 } 367 368 /** 369 * Converts a {@link Nullness} to a {@code bool} value. 370 * 371 * @param nullness The nullness value. 372 * @return true if the nullness value represents a {@code Nullable} value. To be more specific, it 373 * returns true if the nullness value is either {@link Nullness#NULL} or {@link 374 * Nullness#NULLABLE}. 375 */ nullnessToBool(Nullness nullness)376 public static boolean nullnessToBool(Nullness nullness) { 377 switch (nullness) { 378 case BOTTOM: 379 case NONNULL: 380 return false; 381 case NULL: 382 case NULLABLE: 383 return true; 384 default: 385 throw new AssertionError("Impossible: " + nullness); 386 } 387 } 388 389 /** 390 * Checks if {@code symbol} is a method on {@code java.util.Map} (or a subtype) with name {@code 391 * methodName} and {@code numParams} parameters 392 */ isMapMethod( Symbol.MethodSymbol symbol, VisitorState state, String methodName, int numParams)393 public static boolean isMapMethod( 394 Symbol.MethodSymbol symbol, VisitorState state, String methodName, int numParams) { 395 if (!symbol.getSimpleName().toString().equals(methodName)) { 396 return false; 397 } 398 if (symbol.getParameters().size() != numParams) { 399 return false; 400 } 401 Symbol owner = symbol.owner; 402 return ASTHelpers.isSubtype(owner.type, MAP_TYPE_SUPPLIER.get(state), state); 403 } 404 405 /** 406 * Downcasts a {@code @Nullable} argument to {@code NonNull}, returning the argument 407 * 408 * @throws NullPointerException if argument is {@code null} 409 */ castToNonNull(@ullable T obj)410 public static <T> T castToNonNull(@Nullable T obj) { 411 if (obj == null) { 412 throw new NullPointerException("castToNonNull failed!"); 413 } 414 return obj; 415 } 416 } 417