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.Types; 45 import com.sun.tools.javac.tree.JCTree; 46 import com.sun.tools.javac.util.JCDiagnostic; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Set; 50 import java.util.stream.Collectors; 51 import java.util.stream.Stream; 52 import javax.annotation.Nullable; 53 import javax.lang.model.element.AnnotationMirror; 54 import javax.lang.model.element.AnnotationValue; 55 import javax.lang.model.element.ExecutableElement; 56 import org.checkerframework.nullaway.javacutil.AnnotationUtils; 57 58 /** Helpful utility methods for nullability analysis. */ 59 public class NullabilityUtil { 60 61 private static final Supplier<Type> MAP_TYPE_SUPPLIER = Suppliers.typeFromString("java.util.Map"); 62 NullabilityUtil()63 private NullabilityUtil() {} 64 65 /** 66 * finds the corresponding functional interface method for a lambda expression or method reference 67 * 68 * @param tree the lambda expression or method reference 69 * @return the functional interface method 70 */ getFunctionalInterfaceMethod(ExpressionTree tree, Types types)71 public static Symbol.MethodSymbol getFunctionalInterfaceMethod(ExpressionTree tree, Types types) { 72 Preconditions.checkArgument( 73 (tree instanceof LambdaExpressionTree) || (tree instanceof MemberReferenceTree)); 74 Type funcInterfaceType = ((JCTree.JCFunctionalExpression) tree).type; 75 return (Symbol.MethodSymbol) types.findDescriptorSymbol(funcInterfaceType.tsym); 76 } 77 78 /** 79 * determines whether a lambda parameter is missing an explicit type declaration 80 * 81 * @param lambdaParameter the parameter 82 * @return true if there is no type declaration, false otherwise 83 */ lambdaParamIsImplicitlyTyped(VariableTree lambdaParameter)84 public static boolean lambdaParamIsImplicitlyTyped(VariableTree lambdaParameter) { 85 // kind of a hack; the "preferred position" seems to be the position 86 // of the variable name. if this differs from the start position, it 87 // means there is an explicit type declaration 88 JCDiagnostic.DiagnosticPosition diagnosticPosition = 89 (JCDiagnostic.DiagnosticPosition) lambdaParameter; 90 return diagnosticPosition.getStartPosition() == diagnosticPosition.getPreferredPosition(); 91 } 92 93 /** 94 * find the closest ancestor method in a superclass or superinterface that method overrides 95 * 96 * @param method the subclass method 97 * @param types the types data structure from javac 98 * @return closest overridden ancestor method, or <code>null</code> if method does not override 99 * anything 100 */ 101 @Nullable getClosestOverriddenMethod( Symbol.MethodSymbol method, Types types)102 public static Symbol.MethodSymbol getClosestOverriddenMethod( 103 Symbol.MethodSymbol method, Types types) { 104 // taken from Error Prone MethodOverrides check 105 Symbol.ClassSymbol owner = method.enclClass(); 106 for (Type s : types.closure(owner.type)) { 107 if (types.isSameType(s, owner.type)) { 108 continue; 109 } 110 for (Symbol m : s.tsym.members().getSymbolsByName(method.name)) { 111 if (!(m instanceof Symbol.MethodSymbol)) { 112 continue; 113 } 114 Symbol.MethodSymbol msym = (Symbol.MethodSymbol) m; 115 if (msym.isStatic()) { 116 continue; 117 } 118 if (method.overrides(msym, owner, types, /*checkReturn*/ false)) { 119 return msym; 120 } 121 } 122 } 123 return null; 124 } 125 /** 126 * finds the symbol for the top-level class containing the given symbol 127 * 128 * @param symbol the given symbol 129 * @return symbol for the non-nested enclosing class 130 */ getOutermostClassSymbol(Symbol symbol)131 public static Symbol.ClassSymbol getOutermostClassSymbol(Symbol symbol) { 132 // get the symbol for the outermost enclosing class. this handles 133 // the case of anonymous classes 134 Symbol.ClassSymbol outermostClassSymbol = ASTHelpers.enclosingClass(symbol); 135 while (outermostClassSymbol.getNestingKind().isNested()) { 136 Symbol.ClassSymbol enclosingSymbol = ASTHelpers.enclosingClass(outermostClassSymbol.owner); 137 if (enclosingSymbol != null) { 138 outermostClassSymbol = enclosingSymbol; 139 } else { 140 // enclosingSymbol can be null in weird cases like for array methods 141 break; 142 } 143 } 144 return outermostClassSymbol; 145 } 146 147 /** 148 * find the enclosing method, lambda expression or initializer block for the leaf of some tree 149 * path 150 * 151 * @param path the tree path 152 * @param others also stop and return in case of any of these tree kinds 153 * @return the closest enclosing method / lambda 154 */ 155 @Nullable findEnclosingMethodOrLambdaOrInitializer( TreePath path, ImmutableSet<Tree.Kind> others)156 public static TreePath findEnclosingMethodOrLambdaOrInitializer( 157 TreePath path, ImmutableSet<Tree.Kind> others) { 158 TreePath curPath = path.getParentPath(); 159 while (curPath != null) { 160 if (curPath.getLeaf() instanceof MethodTree 161 || curPath.getLeaf() instanceof LambdaExpressionTree 162 || others.contains(curPath.getLeaf().getKind())) { 163 return curPath; 164 } 165 TreePath parent = curPath.getParentPath(); 166 if (parent != null && parent.getLeaf() instanceof ClassTree) { 167 if (curPath.getLeaf() instanceof BlockTree) { 168 // found initializer block 169 return curPath; 170 } 171 if (curPath.getLeaf() instanceof VariableTree 172 && ((VariableTree) curPath.getLeaf()).getInitializer() != null) { 173 // found field with an inline initializer 174 return curPath; 175 } 176 } 177 curPath = parent; 178 } 179 return null; 180 } 181 182 /** 183 * find the enclosing method, lambda expression or initializer block for the leaf of some tree 184 * path 185 * 186 * @param path the tree path 187 * @return the closest enclosing method / lambda 188 */ 189 @Nullable findEnclosingMethodOrLambdaOrInitializer(TreePath path)190 public static TreePath findEnclosingMethodOrLambdaOrInitializer(TreePath path) { 191 return findEnclosingMethodOrLambdaOrInitializer(path, ImmutableSet.of()); 192 } 193 194 /** 195 * NOTE: this method does not work for getting all annotations of parameters of methods from class 196 * files. For that case, use {@link #getAllAnnotationsForParameter(Symbol.MethodSymbol, int)} 197 * 198 * @param symbol the symbol 199 * @return all annotations on the symbol and on the type of the symbol 200 */ getAllAnnotations(Symbol symbol)201 public static Stream<? extends AnnotationMirror> getAllAnnotations(Symbol symbol) { 202 // for methods, we care about annotations on the return type, not on the method type itself 203 Stream<? extends AnnotationMirror> typeUseAnnotations = getTypeUseAnnotations(symbol); 204 return Stream.concat(symbol.getAnnotationMirrors().stream(), typeUseAnnotations); 205 } 206 207 /** 208 * Retrieve the {@code value} attribute of a method annotation of some type. 209 * 210 * @param methodSymbol A method to check for the annotation. 211 * @param annotName The qualified name of the annotation. 212 * @return The {@code value} attribute of the annotation, or {@code null} if the annotation is not 213 * present. 214 */ getAnnotationValue( Symbol.MethodSymbol methodSymbol, String annotName)215 public static @Nullable String getAnnotationValue( 216 Symbol.MethodSymbol methodSymbol, String annotName) { 217 AnnotationMirror annot = 218 AnnotationUtils.getAnnotationByName(methodSymbol.getAnnotationMirrors(), annotName); 219 if (annot == null) { 220 return null; 221 } 222 223 Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = 224 annot.getElementValues(); 225 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : 226 elementValues.entrySet()) { 227 ExecutableElement elem = entry.getKey(); 228 if (elem.getSimpleName().contentEquals("value")) { 229 return (String) entry.getValue().getValue(); 230 } 231 } 232 // not found 233 return null; 234 } 235 236 /** 237 * Retrieve the {@code value} attribute of a method annotation of some type where the {@code 238 * value} is an array. 239 * 240 * @param methodSymbol A method to check for the annotation. 241 * @param annotName The qualified name or simple name of the annotation depending on the value of 242 * {@code exactMatch}. 243 * @param exactMatch If true, the annotation name must match the full qualified name given in 244 * {@code annotName}, otherwise, simple names will be checked. 245 * @return The {@code value} attribute of the annotation as a {@code Set}, or {@code null} if the 246 * annotation is not present. 247 */ getAnnotationValueArray( Symbol.MethodSymbol methodSymbol, String annotName, boolean exactMatch)248 public static @Nullable Set<String> getAnnotationValueArray( 249 Symbol.MethodSymbol methodSymbol, String annotName, boolean exactMatch) { 250 AnnotationMirror annot = null; 251 for (AnnotationMirror annotationMirror : methodSymbol.getAnnotationMirrors()) { 252 String name = AnnotationUtils.annotationName(annotationMirror); 253 if ((exactMatch && name.equals(annotName)) || (!exactMatch && name.endsWith(annotName))) { 254 annot = annotationMirror; 255 break; 256 } 257 } 258 if (annot == null) { 259 return null; 260 } 261 Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = 262 annot.getElementValues(); 263 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : 264 elementValues.entrySet()) { 265 ExecutableElement elem = entry.getKey(); 266 if (elem.getSimpleName().contentEquals("value")) { 267 @SuppressWarnings("unchecked") 268 List<AnnotationValue> values = (List<AnnotationValue>) entry.getValue().getValue(); 269 return values.stream().map((av) -> ((String) av.getValue())).collect(Collectors.toSet()); 270 } 271 } 272 return null; 273 } 274 275 /** 276 * Works for method parameters defined either in source or in class files 277 * 278 * @param symbol the method symbol 279 * @param paramInd index of the parameter 280 * @return all declaration and type-use annotations for the parameter 281 */ getAllAnnotationsForParameter( Symbol.MethodSymbol symbol, int paramInd)282 public static Stream<? extends AnnotationMirror> getAllAnnotationsForParameter( 283 Symbol.MethodSymbol symbol, int paramInd) { 284 Symbol.VarSymbol varSymbol = symbol.getParameters().get(paramInd); 285 return Stream.concat( 286 varSymbol.getAnnotationMirrors().stream(), 287 symbol 288 .getRawTypeAttributes() 289 .stream() 290 .filter( 291 t -> 292 t.position.type.equals(TargetType.METHOD_FORMAL_PARAMETER) 293 && t.position.parameter_index == paramInd)); 294 } 295 getTypeUseAnnotations(Symbol symbol)296 private static Stream<? extends AnnotationMirror> getTypeUseAnnotations(Symbol symbol) { 297 Stream<Attribute.TypeCompound> rawTypeAttributes = symbol.getRawTypeAttributes().stream(); 298 if (symbol instanceof Symbol.MethodSymbol) { 299 // for methods, we want the type-use annotations on the return type 300 return rawTypeAttributes.filter((t) -> t.position.type.equals(TargetType.METHOD_RETURN)); 301 } 302 return rawTypeAttributes; 303 } 304 305 /** 306 * Check if a field might be null, based on the type. 307 * 308 * @param symbol symbol for field; must be non-null 309 * @param config NullAway config 310 * @return true if based on the type, package, and name of the field, the analysis should assume 311 * the field might be null; false otherwise 312 * @throws NullPointerException if {@code symbol} is null 313 */ mayBeNullFieldFromType(Symbol symbol, Config config)314 public static boolean mayBeNullFieldFromType(Symbol symbol, Config config) { 315 Preconditions.checkNotNull( 316 symbol, "mayBeNullFieldFromType should never be called with a null symbol"); 317 return !(symbol.getSimpleName().toString().equals("class") 318 || symbol.isEnum() 319 || isUnannotated(symbol, config)) 320 && Nullness.hasNullableAnnotation(symbol, config); 321 } 322 323 /** 324 * Converts a {@link Nullness} to a {@code bool} value. 325 * 326 * @param nullness The nullness value. 327 * @return true if the nullness value represents a {@code Nullable} value. To be more specific, it 328 * returns true if the nullness value is either {@link Nullness#NULL} or {@link 329 * Nullness#NULLABLE}. 330 */ nullnessToBool(Nullness nullness)331 public static boolean nullnessToBool(Nullness nullness) { 332 switch (nullness) { 333 case BOTTOM: 334 case NONNULL: 335 return false; 336 case NULL: 337 case NULLABLE: 338 return true; 339 default: 340 throw new AssertionError("Impossible: " + nullness); 341 } 342 } 343 344 /** 345 * Check if a symbol comes from unannotated code. 346 * 347 * @param symbol symbol for entity 348 * @param config NullAway config 349 * @return true if symbol represents an entity from a class that is unannotated; false otherwise 350 */ isUnannotated(Symbol symbol, Config config)351 public static boolean isUnannotated(Symbol symbol, Config config) { 352 Symbol.ClassSymbol outermostClassSymbol = getOutermostClassSymbol(symbol); 353 return !config.fromAnnotatedPackage(outermostClassSymbol) 354 || config.isUnannotatedClass(outermostClassSymbol); 355 } 356 357 /** 358 * Check if a symbol comes from generated code. 359 * 360 * @param symbol symbol for entity 361 * @return true if symbol represents an entity from a class annotated with {@code @Generated}; 362 * false otherwise 363 */ isGenerated(Symbol symbol)364 public static boolean isGenerated(Symbol symbol) { 365 Symbol.ClassSymbol outermostClassSymbol = getOutermostClassSymbol(symbol); 366 return ASTHelpers.hasDirectAnnotationWithSimpleName(outermostClassSymbol, "Generated"); 367 } 368 369 /** 370 * Checks if {@code symbol} is a method on {@code java.util.Map} (or a subtype) with name {@code 371 * methodName} and {@code numParams} parameters 372 */ isMapMethod( Symbol.MethodSymbol symbol, VisitorState state, String methodName, int numParams)373 public static boolean isMapMethod( 374 Symbol.MethodSymbol symbol, VisitorState state, String methodName, int numParams) { 375 if (!symbol.getSimpleName().toString().equals(methodName)) { 376 return false; 377 } 378 if (symbol.getParameters().size() != numParams) { 379 return false; 380 } 381 Symbol owner = symbol.owner; 382 return ASTHelpers.isSubtype(owner.type, MAP_TYPE_SUPPLIER.get(state), state); 383 } 384 } 385