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