1 /* 2 * Copyright (c) 2017-2022 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 static com.uber.nullaway.ASTHelpersBackports.hasDirectAnnotationWithSimpleName; 26 import static com.uber.nullaway.NullabilityUtil.castToNonNull; 27 28 import com.google.common.base.Preconditions; 29 import com.google.common.cache.Cache; 30 import com.google.common.cache.CacheBuilder; 31 import com.google.common.collect.ImmutableSet; 32 import com.google.errorprone.util.ASTHelpers; 33 import com.sun.tools.javac.code.Symbol; 34 import com.sun.tools.javac.util.Context; 35 import java.util.HashMap; 36 import java.util.Map; 37 import javax.lang.model.element.ElementKind; 38 39 /** 40 * Provides APIs for querying whether code is annotated for nullness checking, and for related 41 * queries on what annotations are present on a class/method and/or on relevant enclosing scopes 42 * (i.e. enclosing classes or methods). Makes use of caching internally for performance. 43 */ 44 public final class CodeAnnotationInfo { 45 46 private static final Context.Key<CodeAnnotationInfo> ANNOTATION_INFO_KEY = new Context.Key<>(); 47 48 private static final int MAX_CLASS_CACHE_SIZE = 200; 49 50 private final Cache<Symbol.ClassSymbol, ClassCacheRecord> classCache = 51 CacheBuilder.newBuilder().maximumSize(MAX_CLASS_CACHE_SIZE).build(); 52 CodeAnnotationInfo()53 private CodeAnnotationInfo() {} 54 55 /** 56 * Get the CodeAnnotationInfo for the given javac context. We ensure there is one instance per 57 * context (as opposed to using static fields) to avoid memory leaks. 58 */ instance(Context context)59 public static CodeAnnotationInfo instance(Context context) { 60 CodeAnnotationInfo annotationInfo = context.get(ANNOTATION_INFO_KEY); 61 if (annotationInfo == null) { 62 annotationInfo = new CodeAnnotationInfo(); 63 context.put(ANNOTATION_INFO_KEY, annotationInfo); 64 } 65 return annotationInfo; 66 } 67 68 /** 69 * Checks if a symbol comes from an annotated package, as determined by either configuration flags 70 * (e.g. {@code -XepOpt:NullAway::AnnotatedPackages}) or package level annotations (e.g. {@code 71 * org.jspecify.annotations.NullMarked}). 72 * 73 * @param outermostClassSymbol symbol for class (must be an outermost class) 74 * @param config NullAway config 75 * @return true if the class is from a package that should be treated as properly annotated 76 * according to our convention (every possibly null parameter / return / field 77 * annotated @Nullable), false otherwise 78 */ fromAnnotatedPackage( Symbol.ClassSymbol outermostClassSymbol, Config config)79 private static boolean fromAnnotatedPackage( 80 Symbol.ClassSymbol outermostClassSymbol, Config config) { 81 final String className = outermostClassSymbol.getQualifiedName().toString(); 82 Symbol.PackageSymbol enclosingPackage = ASTHelpers.enclosingPackage(outermostClassSymbol); 83 if (!config.fromExplicitlyAnnotatedPackage(className) 84 && !(enclosingPackage != null 85 && hasDirectAnnotationWithSimpleName( 86 enclosingPackage, NullabilityUtil.NULLMARKED_SIMPLE_NAME))) { 87 // By default, unknown code is unannotated unless @NullMarked or configured as annotated by 88 // package name 89 return false; 90 } 91 if (config.fromExplicitlyUnannotatedPackage(className) 92 || (enclosingPackage != null 93 && hasDirectAnnotationWithSimpleName( 94 enclosingPackage, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME))) { 95 // Any code explicitly marked as unannotated in our configuration is unannotated, no matter 96 // what. Similarly, any package annotated as @NullUnmarked is unannotated, even if 97 // explicitly passed to -XepOpt:NullAway::AnnotatedPackages 98 return false; 99 } 100 // Finally, if we are here, the code was marked as annotated (either by configuration or 101 // @NullMarked) and nothing overrides it. 102 return true; 103 } 104 105 /** 106 * Check if a symbol comes from generated code. 107 * 108 * @param symbol symbol for entity 109 * @return true if symbol represents an entity contained in a class annotated with 110 * {@code @Generated}; false otherwise 111 */ isGenerated(Symbol symbol, Config config)112 public boolean isGenerated(Symbol symbol, Config config) { 113 Symbol.ClassSymbol classSymbol = ASTHelpers.enclosingClass(symbol); 114 if (classSymbol == null) { 115 Preconditions.checkArgument( 116 isClassFieldOfPrimitiveType( 117 symbol), // One known case where this can happen: int.class, void.class, etc. 118 String.format( 119 "Unexpected symbol passed to CodeAnnotationInfo.isGenerated(...) with null enclosing class: %s", 120 symbol)); 121 return false; 122 } 123 Symbol.ClassSymbol outermostClassSymbol = get(classSymbol, config).outermostClassSymbol; 124 return hasDirectAnnotationWithSimpleName(outermostClassSymbol, "Generated"); 125 } 126 127 /** 128 * Check if the symbol represents the .class field of a primitive type. 129 * 130 * <p>e.g. int.class, boolean.class, void.class, etc. 131 * 132 * @param symbol symbol for entity 133 * @return true iff this symbol represents t.class for a primitive type t. 134 */ isClassFieldOfPrimitiveType(Symbol symbol)135 private static boolean isClassFieldOfPrimitiveType(Symbol symbol) { 136 return symbol.name.contentEquals("class") 137 && symbol.owner != null 138 && symbol.owner.getKind().equals(ElementKind.CLASS) 139 && symbol.owner.getQualifiedName().equals(symbol.owner.getSimpleName()) 140 && symbol.owner.enclClass() == null; 141 } 142 143 /** 144 * Check if a symbol comes from unannotated code. 145 * 146 * @param symbol symbol for entity 147 * @param config NullAway config 148 * @return true if symbol represents an entity contained in a class that is unannotated; false 149 * otherwise 150 */ isSymbolUnannotated(Symbol symbol, Config config)151 public boolean isSymbolUnannotated(Symbol symbol, Config config) { 152 Symbol.ClassSymbol classSymbol; 153 if (symbol instanceof Symbol.ClassSymbol) { 154 classSymbol = (Symbol.ClassSymbol) symbol; 155 } else if (isClassFieldOfPrimitiveType(symbol)) { 156 // As a special case, int.class, boolean.class, etc, cause ASTHelpers.enclosingClass(...) to 157 // return null, even though int/boolean/etc. are technically ClassSymbols. We consider this 158 // class "field" of primitive types to be always unannotated. (In the future, we could check 159 // here for whether java.lang is in the annotated packages, but if it is, I suspect we will 160 // have weirder problems than this) 161 return true; 162 } else { 163 classSymbol = castToNonNull(ASTHelpers.enclosingClass(symbol)); 164 } 165 final ClassCacheRecord classCacheRecord = get(classSymbol, config); 166 boolean inAnnotatedClass = classCacheRecord.isNullnessAnnotated; 167 if (symbol.getKind().equals(ElementKind.METHOD) 168 || symbol.getKind().equals(ElementKind.CONSTRUCTOR)) { 169 return !classCacheRecord.isMethodNullnessAnnotated((Symbol.MethodSymbol) symbol); 170 } else { 171 return !inAnnotatedClass; 172 } 173 } 174 175 /** 176 * Check whether a class should be treated as nullness-annotated. 177 * 178 * @param classSymbol The symbol for the class to be checked 179 * @return Whether this class should be treated as null-annotated, taking into account annotations 180 * on enclosing classes, the containing package, and other NullAway configuration like 181 * annotated packages 182 */ isClassNullAnnotated(Symbol.ClassSymbol classSymbol, Config config)183 public boolean isClassNullAnnotated(Symbol.ClassSymbol classSymbol, Config config) { 184 return get(classSymbol, config).isNullnessAnnotated; 185 } 186 187 /** 188 * Retrieve the (outermostClass, isNullMarked) record for a given class symbol. 189 * 190 * <p>This method is recursive, using the cache on the way up and populating it on the way down. 191 * 192 * @param classSymbol The class to query, possibly an inner class 193 * @return A record including the outermost class in which the given class is nested, as well as 194 * boolean flag noting whether it should be treated as nullness-annotated, taking into account 195 * annotations on enclosing classes, the containing package, and other NullAway configuration 196 * like annotated packages 197 */ get(Symbol.ClassSymbol classSymbol, Config config)198 private ClassCacheRecord get(Symbol.ClassSymbol classSymbol, Config config) { 199 ClassCacheRecord record = classCache.getIfPresent(classSymbol); 200 if (record != null) { 201 return record; 202 } 203 if (classSymbol.getNestingKind().isNested()) { 204 Symbol owner = classSymbol.owner; 205 Preconditions.checkNotNull(owner, "Symbol.owner should only be null for modules!"); 206 Symbol.MethodSymbol enclosingMethod = null; 207 if (owner.getKind().equals(ElementKind.METHOD) 208 || owner.getKind().equals(ElementKind.CONSTRUCTOR)) { 209 enclosingMethod = (Symbol.MethodSymbol) owner; 210 } 211 Symbol.ClassSymbol enclosingClass = ASTHelpers.enclosingClass(classSymbol); 212 // enclosingClass can be null in weird cases like for array methods 213 if (enclosingClass != null) { 214 ClassCacheRecord recordForEnclosing = get(enclosingClass, config); 215 // Check if this class is annotated, recall that enclosed scopes override enclosing scopes 216 boolean isAnnotated = recordForEnclosing.isNullnessAnnotated; 217 if (enclosingMethod != null) { 218 isAnnotated = recordForEnclosing.isMethodNullnessAnnotated(enclosingMethod); 219 } 220 if (hasDirectAnnotationWithSimpleName( 221 classSymbol, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) { 222 isAnnotated = false; 223 } else if (hasDirectAnnotationWithSimpleName( 224 classSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME)) { 225 isAnnotated = true; 226 } 227 if (shouldTreatAsUnannotated(classSymbol, config)) { 228 isAnnotated = false; 229 } 230 record = new ClassCacheRecord(recordForEnclosing.outermostClassSymbol, isAnnotated); 231 } 232 } 233 if (record == null) { 234 // We are already at the outermost class (we can find), so let's create a record for it 235 record = new ClassCacheRecord(classSymbol, isAnnotatedTopLevelClass(classSymbol, config)); 236 } 237 classCache.put(classSymbol, record); 238 return record; 239 } 240 shouldTreatAsUnannotated(Symbol.ClassSymbol classSymbol, Config config)241 private boolean shouldTreatAsUnannotated(Symbol.ClassSymbol classSymbol, Config config) { 242 if (config.isUnannotatedClass(classSymbol)) { 243 return true; 244 } else if (config.treatGeneratedAsUnannotated()) { 245 // Generated code is or isn't excluded, depending on configuration 246 // Note: In the future, we might want finer grain controls to distinguish code that is 247 // generated with nullability info and without. 248 if (hasDirectAnnotationWithSimpleName(classSymbol, "Generated")) { 249 return true; 250 } 251 ImmutableSet<String> generatedCodeAnnotations = config.getGeneratedCodeAnnotations(); 252 if (classSymbol.getAnnotationMirrors().stream() 253 .map(anno -> anno.getAnnotationType().toString()) 254 .anyMatch(generatedCodeAnnotations::contains)) { 255 return true; 256 } 257 } 258 return false; 259 } 260 isAnnotatedTopLevelClass(Symbol.ClassSymbol classSymbol, Config config)261 private boolean isAnnotatedTopLevelClass(Symbol.ClassSymbol classSymbol, Config config) { 262 // First, check for an explicitly @NullUnmarked top level class 263 if (hasDirectAnnotationWithSimpleName(classSymbol, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) { 264 return false; 265 } 266 // Then, check if the class has a @NullMarked annotation or comes from an annotated package 267 if ((hasDirectAnnotationWithSimpleName(classSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME) 268 || fromAnnotatedPackage(classSymbol, config))) { 269 // make sure it's not explicitly configured as unannotated 270 return !shouldTreatAsUnannotated(classSymbol, config); 271 } 272 return false; 273 } 274 275 /** 276 * Immutable record holding the outermost class symbol and the nullness-annotated state for a 277 * given (possibly inner) class. 278 * 279 * <p>The class being referenced by the record is not represented by this object, but rather the 280 * key used to retrieve it. 281 */ 282 private static final class ClassCacheRecord { 283 public final Symbol.ClassSymbol outermostClassSymbol; 284 public final boolean isNullnessAnnotated; 285 public final Map<Symbol.MethodSymbol, Boolean> methodNullnessCache; 286 ClassCacheRecord(Symbol.ClassSymbol outermostClassSymbol, boolean isAnnotated)287 public ClassCacheRecord(Symbol.ClassSymbol outermostClassSymbol, boolean isAnnotated) { 288 this.outermostClassSymbol = outermostClassSymbol; 289 this.isNullnessAnnotated = isAnnotated; 290 this.methodNullnessCache = new HashMap<>(); 291 } 292 isMethodNullnessAnnotated(Symbol.MethodSymbol methodSymbol)293 public boolean isMethodNullnessAnnotated(Symbol.MethodSymbol methodSymbol) { 294 return methodNullnessCache.computeIfAbsent( 295 methodSymbol, 296 m -> { 297 if (hasDirectAnnotationWithSimpleName(m, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) { 298 return false; 299 } else if (this.isNullnessAnnotated) { 300 return true; 301 } else { 302 return hasDirectAnnotationWithSimpleName(m, NullabilityUtil.NULLMARKED_SIMPLE_NAME); 303 } 304 }); 305 } 306 } 307 } 308