xref: /aosp_15_r20/external/nullaway/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPath.java (revision f50c306653bc89b8210ce6c9e0b0b44fc134bc03)
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.dataflow;
24 
25 import static com.uber.nullaway.NullabilityUtil.castToNonNull;
26 
27 import com.google.common.base.Preconditions;
28 import com.google.common.collect.ImmutableList;
29 import com.google.common.collect.ImmutableSet;
30 import com.google.errorprone.VisitorState;
31 import com.google.errorprone.util.ASTHelpers;
32 import com.sun.source.tree.LiteralTree;
33 import com.sun.source.tree.MethodInvocationTree;
34 import com.sun.source.tree.Tree;
35 import com.sun.tools.javac.code.Symbol;
36 import com.sun.tools.javac.code.Type;
37 import com.uber.nullaway.NullabilityUtil;
38 import com.uber.nullaway.annotations.JacocoIgnoreGenerated;
39 import java.util.ArrayDeque;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Objects;
43 import java.util.Set;
44 import javax.annotation.Nullable;
45 import javax.lang.model.element.Element;
46 import javax.lang.model.element.ElementKind;
47 import javax.lang.model.element.Modifier;
48 import javax.lang.model.element.VariableElement;
49 import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
50 import org.checkerframework.nullaway.dataflow.cfg.node.IntegerLiteralNode;
51 import org.checkerframework.nullaway.dataflow.cfg.node.LocalVariableNode;
52 import org.checkerframework.nullaway.dataflow.cfg.node.LongLiteralNode;
53 import org.checkerframework.nullaway.dataflow.cfg.node.MethodAccessNode;
54 import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
55 import org.checkerframework.nullaway.dataflow.cfg.node.Node;
56 import org.checkerframework.nullaway.dataflow.cfg.node.StringLiteralNode;
57 import org.checkerframework.nullaway.dataflow.cfg.node.SuperNode;
58 import org.checkerframework.nullaway.dataflow.cfg.node.ThisNode;
59 import org.checkerframework.nullaway.dataflow.cfg.node.TypeCastNode;
60 import org.checkerframework.nullaway.dataflow.cfg.node.VariableDeclarationNode;
61 import org.checkerframework.nullaway.dataflow.cfg.node.WideningConversionNode;
62 import org.checkerframework.nullaway.javacutil.TreeUtils;
63 
64 /**
65  * Represents an extended notion of an access path, which we track for nullness.
66  *
67  * <p>Typically, access paths are of the form x.f.g.h, where x is a variable and f, g, and h are
68  * field names. Here, we also allow no-argument methods to appear in the access path, as well as
69  * method calls passed only statically constant parameters, so an AP can be of the form
70  * x.f().g.h([int_expr|string_expr]) in general.
71  *
72  * <p>We do not allow array accesses in access paths for the moment.
73  */
74 public final class AccessPath implements MapKey {
75 
76   /**
77    * A prefix added for elements appearing in method invocation APs which represent fields that can
78    * be proven to be class-initialization time constants (i.e. static final fields of a type known
79    * to be structurally immutable, such as io.grpc.Metadata.Key).
80    *
81    * <p>This prefix helps avoid collisions between common field names and common strings, e.g.
82    * "KEY_1" and the field KEY_1.
83    */
84   private static final String IMMUTABLE_FIELD_PREFIX = "static final [immutable] field: ";
85 
86   /**
87    * Encode a static final field as a constant argument on a method's AccessPathElement
88    *
89    * <p>The field must be of a type known to be structurally immutable, in addition to being
90    * declared static and final for this encoding to make any sense. We do not verify this here, and
91    * rather operate only on the field's fully qualified name, as this is intended to be a quick
92    * utility method.
93    *
94    * @param fieldFQN the field's Fully Qualified Name
95    * @return a string suitable to be included as part of the constant arguments of an
96    *     AccessPathElement, assuming the field is indeed static final and of an structurally
97    *     immutable type
98    */
immutableFieldNameAsConstantArgument(String fieldFQN)99   public static String immutableFieldNameAsConstantArgument(String fieldFQN) {
100     return IMMUTABLE_FIELD_PREFIX + fieldFQN;
101   }
102 
103   /** Root of the access path. If {@code null}, the root is the receiver argument */
104   @Nullable private final Element root;
105 
106   private final ImmutableList<AccessPathElement> elements;
107 
108   /**
109    * if present, the argument to the map get() method call that is the final element of this path
110    */
111   @Nullable private final MapKey mapGetArg;
112 
AccessPath(@ullable Element root, ImmutableList<AccessPathElement> elements)113   private AccessPath(@Nullable Element root, ImmutableList<AccessPathElement> elements) {
114     this(root, elements, null);
115   }
116 
AccessPath( @ullable Element root, ImmutableList<AccessPathElement> elements, @Nullable MapKey mapGetArg)117   private AccessPath(
118       @Nullable Element root,
119       ImmutableList<AccessPathElement> elements,
120       @Nullable MapKey mapGetArg) {
121     this.root = root;
122     this.elements = elements;
123     this.mapGetArg = mapGetArg;
124   }
125 
126   /**
127    * Construct the access path of a local.
128    *
129    * @param node the local
130    * @return access path representing the local
131    */
fromLocal(LocalVariableNode node)132   public static AccessPath fromLocal(LocalVariableNode node) {
133     return new AccessPath(node.getElement(), ImmutableList.of());
134   }
135 
136   /**
137    * Construct the access path of a variable declaration.
138    *
139    * @param node the variable declaration
140    * @return access path representing the variable declaration
141    */
fromVarDecl(VariableDeclarationNode node)142   static AccessPath fromVarDecl(VariableDeclarationNode node) {
143     Element elem = TreeUtils.elementFromDeclaration(node.getTree());
144     return new AccessPath(elem, ImmutableList.of());
145   }
146 
147   /**
148    * Construct the access path of a field access.
149    *
150    * @param node the field access
151    * @param apContext the current access path context information (see {@link
152    *     AccessPath.AccessPathContext}).
153    * @return access path for the field access, or <code>null</code> if it cannot be represented
154    */
155   @Nullable
fromFieldAccess(FieldAccessNode node, AccessPathContext apContext)156   static AccessPath fromFieldAccess(FieldAccessNode node, AccessPathContext apContext) {
157     return fromNodeAndContext(node, apContext);
158   }
159 
160   @Nullable
fromNodeAndContext(Node node, AccessPathContext apContext)161   private static AccessPath fromNodeAndContext(Node node, AccessPathContext apContext) {
162     return buildAccessPathRecursive(node, new ArrayDeque<>(), apContext, null);
163   }
164 
165   /**
166    * Construct the access path of a method call.
167    *
168    * @param node the method call
169    * @param apContext the current access path context information (see {@link
170    *     AccessPath.AccessPathContext}).
171    * @return access path for the method call, or <code>null</code> if it cannot be represented
172    */
173   @Nullable
fromMethodCall( MethodInvocationNode node, VisitorState state, AccessPathContext apContext)174   static AccessPath fromMethodCall(
175       MethodInvocationNode node, VisitorState state, AccessPathContext apContext) {
176     if (isMapGet(ASTHelpers.getSymbol(node.getTree()), state)) {
177       return fromMapGetCall(node, state, apContext);
178     }
179     return fromVanillaMethodCall(node, apContext);
180   }
181 
182   @Nullable
fromVanillaMethodCall( MethodInvocationNode node, AccessPathContext apContext)183   private static AccessPath fromVanillaMethodCall(
184       MethodInvocationNode node, AccessPathContext apContext) {
185     return fromNodeAndContext(node, apContext);
186   }
187 
188   /**
189    * Returns an access path rooted at {@code newRoot} with the same elements and map-get argument as
190    * {@code origAP}
191    */
switchRoot(AccessPath origAP, Element newRoot)192   static AccessPath switchRoot(AccessPath origAP, Element newRoot) {
193     return new AccessPath(newRoot, origAP.elements, origAP.mapGetArg);
194   }
195 
196   /**
197    * Construct the access path given a {@code base.element} structure.
198    *
199    * @param base the base expression for the access path
200    * @param element the final element of the access path (a field or method)
201    * @param apContext the current access path context information (see {@link
202    *     AccessPath.AccessPathContext}).
203    * @return the {@link AccessPath} {@code base.element}
204    */
205   @Nullable
fromBaseAndElement( Node base, Element element, AccessPathContext apContext)206   public static AccessPath fromBaseAndElement(
207       Node base, Element element, AccessPathContext apContext) {
208     return fromNodeElementAndContext(base, new AccessPathElement(element), apContext);
209   }
210 
211   @Nullable
fromNodeElementAndContext( Node base, AccessPathElement apElement, AccessPathContext apContext)212   private static AccessPath fromNodeElementAndContext(
213       Node base, AccessPathElement apElement, AccessPathContext apContext) {
214     ArrayDeque<AccessPathElement> elements = new ArrayDeque<>();
215     elements.push(apElement);
216     return buildAccessPathRecursive(base, elements, apContext, null);
217   }
218 
219   /**
220    * Construct the access path given a {@code base.method(CONS)} structure.
221    *
222    * <p>IMPORTANT: Be careful with this method, the argument list is not the variable names of the
223    * method arguments, but rather the string representation of primitive-type compile-time constants
224    * or the name of static final fields of structurally immutable types (see {@link
225    * #buildAccessPathRecursive(Node, ArrayDeque, AccessPathContext, MapKey)}).
226    *
227    * <p>This is used by a few specialized Handlers to set nullability around particular paths
228    * involving constants.
229    *
230    * @param base the base expression for the access path
231    * @param method the last method call in the access path
232    * @param constantArguments a list of <b>constant</b> arguments passed to the method call
233    * @param apContext the current access path context information (see {@link
234    *     AccessPath.AccessPathContext}).
235    * @return the {@link AccessPath} {@code base.method(CONS)}
236    */
237   @Nullable
fromBaseMethodAndConstantArgs( Node base, Element method, List<String> constantArguments, AccessPathContext apContext)238   public static AccessPath fromBaseMethodAndConstantArgs(
239       Node base, Element method, List<String> constantArguments, AccessPathContext apContext) {
240     return fromNodeElementAndContext(
241         base, new AccessPathElement(method, constantArguments), apContext);
242   }
243 
244   /**
245    * Construct the access path for <code>map.get(x)</code> from an invocation of <code>put(x)</code>
246    * or <code>containsKey(x)</code>.
247    *
248    * @param node a node invoking containsKey() or put() on a map
249    * @param apContext the current access path context information (see {@link
250    *     AccessPath.AccessPathContext}).
251    * @return an AccessPath representing invoking get() on the same type of map as from node, passing
252    *     the same first argument as is passed in node
253    */
254   @Nullable
getForMapInvocation( MethodInvocationNode node, VisitorState state, AccessPathContext apContext)255   public static AccessPath getForMapInvocation(
256       MethodInvocationNode node, VisitorState state, AccessPathContext apContext) {
257     // For the receiver type for get, use the declared type of the receiver of the containsKey()
258     // call. Note that this may differ from the containing class of the resolved containsKey()
259     // method, which can be in a superclass (e.g., LinkedHashMap does not override containsKey())
260     // assumption: map type will not both override containsKey() and inherit get()
261     return fromMapGetCall(node, state, apContext);
262   }
263 
stripCasts(Node node)264   private static Node stripCasts(Node node) {
265     while (node instanceof TypeCastNode) {
266       node = ((TypeCastNode) node).getOperand();
267     }
268     return node;
269   }
270 
271   @Nullable
argumentToMapKeySpecifier( Node argument, VisitorState state, AccessPathContext apContext)272   private static MapKey argumentToMapKeySpecifier(
273       Node argument, VisitorState state, AccessPathContext apContext) {
274     // Required to have Node type match Tree type in some instances.
275     if (argument instanceof WideningConversionNode) {
276       argument = ((WideningConversionNode) argument).getOperand();
277     }
278     // A switch at the Tree level should be faster than multiple if checks at the Node level.
279     switch (castToNonNull(argument.getTree()).getKind()) {
280       case STRING_LITERAL:
281         return new StringMapKey(((StringLiteralNode) argument).getValue());
282       case INT_LITERAL:
283         return new NumericMapKey(((IntegerLiteralNode) argument).getValue());
284       case LONG_LITERAL:
285         return new NumericMapKey(((LongLiteralNode) argument).getValue());
286       case METHOD_INVOCATION:
287         MethodAccessNode target = ((MethodInvocationNode) argument).getTarget();
288         Node receiver = stripCasts(target.getReceiver());
289         List<Node> arguments = ((MethodInvocationNode) argument).getArguments();
290         // Check for int/long boxing.
291         if (target.getMethod().getSimpleName().toString().equals("valueOf")
292             && arguments.size() == 1
293             && castToNonNull(receiver.getTree()).getKind().equals(Tree.Kind.IDENTIFIER)
294             && (receiver.toString().equals("Integer") || receiver.toString().equals("Long"))) {
295           return argumentToMapKeySpecifier(arguments.get(0), state, apContext);
296         }
297         // Fine to fallthrough:
298       default:
299         // Every other type of expression, including variables, field accesses, new A(...), etc.
300         return getAccessPathForNode(argument, state, apContext); // Every AP is a MapKey too
301     }
302   }
303 
304   @Nullable
fromMapGetCall( MethodInvocationNode node, VisitorState state, AccessPathContext apContext)305   private static AccessPath fromMapGetCall(
306       MethodInvocationNode node, VisitorState state, AccessPathContext apContext) {
307     Node argument = node.getArgument(0);
308     MapKey mapKey = argumentToMapKeySpecifier(argument, state, apContext);
309     if (mapKey == null) {
310       return null;
311     }
312     MethodAccessNode target = node.getTarget();
313     Node receiver = stripCasts(target.getReceiver());
314     return buildAccessPathRecursive(receiver, new ArrayDeque<>(), apContext, mapKey);
315   }
316 
317   /**
318    * Gets corresponding AccessPath for node, if it exists. Handles calls to <code>Map.get()
319    * </code>
320    *
321    * @param node AST node
322    * @param state the visitor state
323    * @param apContext the current access path context information (see {@link
324    *     AccessPath.AccessPathContext}).
325    * @return corresponding AccessPath if it exists; <code>null</code> otherwise
326    */
327   @Nullable
getAccessPathForNode( Node node, VisitorState state, AccessPathContext apContext)328   public static AccessPath getAccessPathForNode(
329       Node node, VisitorState state, AccessPathContext apContext) {
330     if (node instanceof LocalVariableNode) {
331       return fromLocal((LocalVariableNode) node);
332     } else if (node instanceof FieldAccessNode) {
333       return fromFieldAccess((FieldAccessNode) node, apContext);
334     } else if (node instanceof MethodInvocationNode) {
335       return fromMethodCall((MethodInvocationNode) node, state, apContext);
336     } else {
337       return null;
338     }
339   }
340 
341   /**
342    * Constructs an access path ending with the class field element in the argument. The receiver is
343    * the method receiver itself.
344    *
345    * @param element the receiver element.
346    * @return access path representing the class field
347    */
fromFieldElement(VariableElement element)348   public static AccessPath fromFieldElement(VariableElement element) {
349     Preconditions.checkArgument(
350         element.getKind().isField(),
351         "element must be of type: FIELD but received: " + element.getKind());
352     return new AccessPath(null, ImmutableList.of(new AccessPathElement(element)));
353   }
354 
isBoxingMethod(Symbol.MethodSymbol methodSymbol)355   private static boolean isBoxingMethod(Symbol.MethodSymbol methodSymbol) {
356     if (methodSymbol.isStatic() && methodSymbol.getSimpleName().contentEquals("valueOf")) {
357       Symbol.PackageSymbol enclosingPackage = ASTHelpers.enclosingPackage(methodSymbol.enclClass());
358       return enclosingPackage != null && enclosingPackage.fullname.contentEquals("java.lang");
359     }
360     return false;
361   }
362 
363   /**
364    * A helper function that recursively builds an AccessPath from a CFG node.
365    *
366    * @param node the CFG node
367    * @param elements elements to append to the final access path.
368    * @param apContext context information, used to handle cases with constant arguments
369    * @param mapKey map key to be used as the map-get argument, or {@code null} if there is no key
370    * @return the final access path
371    */
372   @Nullable
buildAccessPathRecursive( Node node, ArrayDeque<AccessPathElement> elements, AccessPathContext apContext, @Nullable MapKey mapKey)373   private static AccessPath buildAccessPathRecursive(
374       Node node,
375       ArrayDeque<AccessPathElement> elements,
376       AccessPathContext apContext,
377       @Nullable MapKey mapKey) {
378     AccessPath result;
379     if (node instanceof FieldAccessNode) {
380       FieldAccessNode fieldAccess = (FieldAccessNode) node;
381       if (fieldAccess.isStatic()) {
382         // this is the root
383         result = new AccessPath(fieldAccess.getElement(), ImmutableList.copyOf(elements), mapKey);
384       } else {
385         // instance field access
386         elements.push(new AccessPathElement(fieldAccess.getElement()));
387         result =
388             buildAccessPathRecursive(
389                 stripCasts(fieldAccess.getReceiver()), elements, apContext, mapKey);
390       }
391     } else if (node instanceof MethodInvocationNode) {
392       MethodInvocationNode invocation = (MethodInvocationNode) node;
393       AccessPathElement accessPathElement;
394       MethodAccessNode accessNode = invocation.getTarget();
395       if (invocation.getArguments().size() == 0) {
396         Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(invocation.getTree());
397         if (symbol.isStatic()) {
398           // a zero-argument static method call can be the root of an access path
399           return new AccessPath(symbol, ImmutableList.copyOf(elements), mapKey);
400         } else {
401           accessPathElement = new AccessPathElement(accessNode.getMethod());
402         }
403       } else {
404         List<String> constantArgumentValues = new ArrayList<>();
405         for (Node argumentNode : invocation.getArguments()) {
406           Tree tree = argumentNode.getTree();
407           if (tree == null) {
408             return null; // Not an AP
409           } else if (tree.getKind().equals(Tree.Kind.METHOD_INVOCATION)) {
410             // Check for boxing call
411             MethodInvocationTree methodInvocationTree = (MethodInvocationTree) tree;
412             if (methodInvocationTree.getArguments().size() == 1
413                 && isBoxingMethod(ASTHelpers.getSymbol(methodInvocationTree))) {
414               tree = methodInvocationTree.getArguments().get(0);
415             }
416           }
417           switch (tree.getKind()) {
418             case BOOLEAN_LITERAL:
419             case CHAR_LITERAL:
420             case DOUBLE_LITERAL:
421             case FLOAT_LITERAL:
422             case INT_LITERAL:
423             case LONG_LITERAL:
424             case STRING_LITERAL:
425               constantArgumentValues.add(((LiteralTree) tree).getValue().toString());
426               break;
427             case NULL_LITERAL:
428               // Um, probably not? Return null for now.
429               return null; // Not an AP
430             case MEMBER_SELECT: // check for Foo.CONST
431             case IDENTIFIER: // check for CONST
432               // Check for a constant field (static final)
433               Symbol symbol = ASTHelpers.getSymbol(tree);
434               if (symbol instanceof Symbol.VarSymbol
435                   && symbol.getKind().equals(ElementKind.FIELD)) {
436                 Symbol.VarSymbol varSymbol = (Symbol.VarSymbol) symbol;
437                 // From docs: getConstantValue() returns the value of this variable if this is a
438                 // static final field initialized to a compile-time constant. Returns null
439                 // otherwise.
440                 // This means that foo(FOUR) will match foo(4) iff FOUR=4 is a compile time
441                 // constant :)
442                 Object constantValue = varSymbol.getConstantValue();
443                 if (constantValue != null) {
444                   constantArgumentValues.add(constantValue.toString());
445                   break;
446                 }
447                 // The above will not work for static final fields of reference type, since they are
448                 // initialized at class-initialization time, not compile time. Properly handling
449                 // such fields would further require proving deep immutability for the object type
450                 // itself. We use a handler-augment list of safe types:
451                 Set<Modifier> modifiersSet = varSymbol.getModifiers();
452                 if (modifiersSet.contains(Modifier.STATIC)
453                     && modifiersSet.contains(Modifier.FINAL)
454                     && apContext.isStructurallyImmutableType(varSymbol.type)) {
455                   String immutableFieldFQN =
456                       varSymbol.enclClass().flatName().toString()
457                           + "."
458                           + varSymbol.flatName().toString();
459                   constantArgumentValues.add(
460                       immutableFieldNameAsConstantArgument(immutableFieldFQN));
461                   break;
462                 }
463               }
464               // Cascade to default, symbol is not a constant field
465               // fall through
466             default:
467               return null; // Not an AP
468           }
469         }
470         accessPathElement = new AccessPathElement(accessNode.getMethod(), constantArgumentValues);
471       }
472       elements.push(accessPathElement);
473       result =
474           buildAccessPathRecursive(
475               stripCasts(accessNode.getReceiver()), elements, apContext, mapKey);
476     } else if (node instanceof LocalVariableNode) {
477       result =
478           new AccessPath(
479               ((LocalVariableNode) node).getElement(), ImmutableList.copyOf(elements), mapKey);
480     } else if (node instanceof ThisNode || node instanceof SuperNode) {
481       result = new AccessPath(null, ImmutableList.copyOf(elements), mapKey);
482     } else {
483       // don't handle any other cases
484       result = null;
485     }
486     return result;
487   }
488 
489   /**
490    * Creates an access path representing a Map get call, where the key is obtained by calling {@code
491    * next()} on some {@code Iterator}. Used to support reasoning about iteration over a map's key
492    * set using an enhanced-for loop.
493    *
494    * @param mapNode Node representing the map
495    * @param iterVar local variable holding the iterator
496    * @param apContext access path context
497    * @return access path representing the get call, or {@code null} if the map node cannot be
498    *     represented with an access path
499    */
500   @Nullable
mapWithIteratorContentsKey( Node mapNode, LocalVariableNode iterVar, AccessPathContext apContext)501   public static AccessPath mapWithIteratorContentsKey(
502       Node mapNode, LocalVariableNode iterVar, AccessPathContext apContext) {
503     IteratorContentsKey iterContentsKey =
504         new IteratorContentsKey((VariableElement) iterVar.getElement());
505     return buildAccessPathRecursive(mapNode, new ArrayDeque<>(), apContext, iterContentsKey);
506   }
507 
508   /**
509    * Creates an access path identical to {@code accessPath} (which must represent a map get), but
510    * replacing its map {@code get()} argument with {@code mapKey}
511    */
replaceMapKey(AccessPath accessPath, MapKey mapKey)512   public static AccessPath replaceMapKey(AccessPath accessPath, MapKey mapKey) {
513     return new AccessPath(accessPath.getRoot(), accessPath.getElements(), mapKey);
514   }
515 
516   @Override
equals(Object o)517   public boolean equals(Object o) {
518     if (this == o) {
519       return true;
520     }
521     if (o == null || getClass() != o.getClass()) {
522       return false;
523     }
524     AccessPath that = (AccessPath) o;
525     return Objects.equals(root, that.root)
526         && elements.equals(that.elements)
527         && Objects.equals(mapGetArg, that.mapGetArg);
528   }
529 
530   @Override
hashCode()531   public int hashCode() {
532     int result = 1;
533     result = 31 * result + (root != null ? root.hashCode() : 0);
534     result = 31 * result + elements.hashCode();
535     result = 31 * result + (mapGetArg != null ? mapGetArg.hashCode() : 0);
536     return result;
537   }
538 
539   /**
540    * Returns the root element of the access path. If the root is the receiver argument, returns
541    * {@code null}.
542    */
543   @Nullable
getRoot()544   public Element getRoot() {
545     return root;
546   }
547 
getElements()548   public ImmutableList<AccessPathElement> getElements() {
549     return elements;
550   }
551 
552   @Nullable
getMapGetArg()553   public MapKey getMapGetArg() {
554     return mapGetArg;
555   }
556 
557   @Override
toString()558   public String toString() {
559     return "AccessPath{"
560         + "root="
561         + (root == null ? "this" : root)
562         + ", elements="
563         + elements
564         + ", mapGetArg="
565         + mapGetArg
566         + '}';
567   }
568 
isMapGet(Symbol.MethodSymbol symbol, VisitorState state)569   private static boolean isMapGet(Symbol.MethodSymbol symbol, VisitorState state) {
570     return NullabilityUtil.isMapMethod(symbol, state, "get", 1);
571   }
572 
isContainsKey(Symbol.MethodSymbol symbol, VisitorState state)573   public static boolean isContainsKey(Symbol.MethodSymbol symbol, VisitorState state) {
574     return NullabilityUtil.isMapMethod(symbol, state, "containsKey", 1);
575   }
576 
isMapPut(Symbol.MethodSymbol symbol, VisitorState state)577   public static boolean isMapPut(Symbol.MethodSymbol symbol, VisitorState state) {
578     return NullabilityUtil.isMapMethod(symbol, state, "put", 2)
579         || NullabilityUtil.isMapMethod(symbol, state, "putIfAbsent", 2);
580   }
581 
isMapComputeIfAbsent(Symbol.MethodSymbol symbol, VisitorState state)582   public static boolean isMapComputeIfAbsent(Symbol.MethodSymbol symbol, VisitorState state) {
583     return NullabilityUtil.isMapMethod(symbol, state, "computeIfAbsent", 2);
584   }
585 
586   private static final class StringMapKey implements MapKey {
587 
588     private final String key;
589 
StringMapKey(String key)590     public StringMapKey(String key) {
591       this.key = key;
592     }
593 
594     @Override
hashCode()595     public int hashCode() {
596       return this.key.hashCode();
597     }
598 
599     @Override
equals(Object obj)600     public boolean equals(Object obj) {
601       if (obj instanceof StringMapKey) {
602         return this.key.equals(((StringMapKey) obj).key);
603       }
604       return false;
605     }
606   }
607 
608   private static final class NumericMapKey implements MapKey {
609 
610     private final long key;
611 
NumericMapKey(long key)612     public NumericMapKey(long key) {
613       this.key = key;
614     }
615 
616     @Override
hashCode()617     public int hashCode() {
618       return Long.hashCode(this.key);
619     }
620 
621     /**
622      * We ignore this method for code coverage since there is non-determinism somewhere deep in a
623      * Map implementation such that, depending on how AccessPaths get bucketed in the Map (which
624      * depends on non-deterministic hash codes), sometimes this method is called and sometimes it is
625      * not.
626      */
627     @Override
628     @JacocoIgnoreGenerated
equals(Object obj)629     public boolean equals(Object obj) {
630       if (obj instanceof NumericMapKey) {
631         return this.key == ((NumericMapKey) obj).key;
632       }
633       return false;
634     }
635   }
636 
637   /**
638    * Represents all possible values that could be returned by calling {@code next()} on an {@code
639    * Iterator} variable
640    */
641   public static final class IteratorContentsKey implements MapKey {
642 
643     /**
644      * Element for the local variable holding the {@code Iterator}. We only support locals for now,
645      * as this class is designed specifically for reasoning about iterating over map keys using an
646      * enhanced-for loop over a {@code keySet()}, and for such cases the iterator is always stored
647      * locally
648      */
649     private final VariableElement iteratorVarElement;
650 
IteratorContentsKey(VariableElement iteratorVarElement)651     IteratorContentsKey(VariableElement iteratorVarElement) {
652       this.iteratorVarElement = iteratorVarElement;
653     }
654 
getIteratorVarElement()655     public VariableElement getIteratorVarElement() {
656       return iteratorVarElement;
657     }
658 
659     /**
660      * We ignore this method for code coverage since there is non-determinism somewhere deep in a
661      * Map implementation such that, depending on how AccessPaths get bucketed in the Map (which
662      * depends on non-deterministic hash codes), sometimes this method is called and sometimes it is
663      * not.
664      */
665     @Override
666     @JacocoIgnoreGenerated
equals(Object o)667     public boolean equals(Object o) {
668       if (this == o) {
669         return true;
670       }
671       if (o == null || getClass() != o.getClass()) {
672         return false;
673       }
674       IteratorContentsKey that = (IteratorContentsKey) o;
675       return iteratorVarElement.equals(that.iteratorVarElement);
676     }
677 
678     @Override
hashCode()679     public int hashCode() {
680       return iteratorVarElement.hashCode();
681     }
682   }
683 
684   /**
685    * Represents a per-javac instance of an AccessPath context options.
686    *
687    * <p>This includes, for example, data on known structurally immutable types.
688    */
689   public static final class AccessPathContext {
690 
691     private final ImmutableSet<String> immutableTypes;
692 
AccessPathContext(ImmutableSet<String> immutableTypes)693     private AccessPathContext(ImmutableSet<String> immutableTypes) {
694       this.immutableTypes = immutableTypes;
695     }
696 
isStructurallyImmutableType(Type type)697     public boolean isStructurallyImmutableType(Type type) {
698       return immutableTypes.contains(type.tsym.toString());
699     }
700 
builder()701     public static Builder builder() {
702       return new AccessPathContext.Builder();
703     }
704 
705     /** class for building up instances of the AccessPathContext. */
706     public static final class Builder {
707 
708       @Nullable private ImmutableSet<String> immutableTypes;
709 
Builder()710       Builder() {}
711 
712       /**
713        * Passes the set of structurally immutable types registered into this AccessPathContext.
714        *
715        * <p>See {@link com.uber.nullaway.handlers.Handler#onRegisterImmutableTypes} for more info.
716        *
717        * @param immutableTypes the immutable types known to our dataflow analysis.
718        */
setImmutableTypes(ImmutableSet<String> immutableTypes)719       public Builder setImmutableTypes(ImmutableSet<String> immutableTypes) {
720         this.immutableTypes = immutableTypes;
721         return this;
722       }
723 
724       /**
725        * Construct the immutable AccessPathContext instance.
726        *
727        * @return an access path context constructed from everything added to the builder
728        */
build()729       public AccessPathContext build() {
730         if (immutableTypes == null) {
731           throw new IllegalStateException("must set immutable types before building");
732         }
733         return new AccessPathContext(immutableTypes);
734       }
735     }
736   }
737 }
738