• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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