1 /* 2 * Copyright 2014 Google Inc. All Rights Reserved. 3 * 4 * Modifications copyright (C) 2017 Uber Technologies, Inc. 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 * in compliance with the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software distributed under the License 12 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 * or implied. See the License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.uber.nullaway.dataflow; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static com.google.common.collect.Sets.intersection; 21 22 import com.google.common.collect.ImmutableMap; 23 import com.google.errorprone.VisitorState; 24 import com.uber.nullaway.Nullness; 25 import com.uber.nullaway.dataflow.AccessPath.IteratorContentsKey; 26 import java.util.HashMap; 27 import java.util.LinkedHashSet; 28 import java.util.Map; 29 import java.util.Set; 30 import java.util.function.Predicate; 31 import java.util.stream.Collectors; 32 import javax.annotation.Nullable; 33 import javax.lang.model.element.Element; 34 import org.checkerframework.nullaway.dataflow.analysis.Store; 35 import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode; 36 import org.checkerframework.nullaway.dataflow.cfg.node.LocalVariableNode; 37 import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode; 38 import org.checkerframework.nullaway.dataflow.cfg.visualize.CFGVisualizer; 39 import org.checkerframework.nullaway.dataflow.expression.JavaExpression; 40 41 /** 42 * Highly based on {@link com.google.errorprone.dataflow.LocalStore}, but for {@link AccessPath}s. 43 */ 44 public class NullnessStore implements Store<NullnessStore> { 45 46 private static final NullnessStore EMPTY = new NullnessStore(ImmutableMap.of()); 47 48 private final ImmutableMap<AccessPath, Nullness> contents; 49 NullnessStore(Map<AccessPath, Nullness> contents)50 private NullnessStore(Map<AccessPath, Nullness> contents) { 51 this.contents = ImmutableMap.copyOf(contents); 52 } 53 54 /** 55 * Produce an empty store. 56 * 57 * @return an empty store 58 */ empty()59 public static NullnessStore empty() { 60 return EMPTY; 61 } 62 63 /** 64 * Get the nullness for a local variable. 65 * 66 * @param node node representing local variable 67 * @param defaultValue default value if we have no fact 68 * @return fact associated with local 69 */ valueOfLocalVariable(LocalVariableNode node, Nullness defaultValue)70 public Nullness valueOfLocalVariable(LocalVariableNode node, Nullness defaultValue) { 71 return contents.getOrDefault(AccessPath.fromLocal(node), defaultValue); 72 } 73 74 /** 75 * Get the nullness of a field. 76 * 77 * @param node node representing field access 78 * @param defaultValue default value if we have no fact 79 * @return fact associated with field access 80 */ valueOfField( FieldAccessNode node, Nullness defaultValue, AccessPath.AccessPathContext apContext)81 public Nullness valueOfField( 82 FieldAccessNode node, Nullness defaultValue, AccessPath.AccessPathContext apContext) { 83 AccessPath path = AccessPath.fromFieldAccess(node, apContext); 84 if (path == null) { 85 return defaultValue; 86 } 87 return contents.getOrDefault(path, defaultValue); 88 } 89 90 /** 91 * Get the nullness of a method call. 92 * 93 * @param node node representing method invocation 94 * @param defaultValue default value if we have no fact 95 * @return fact associated with method invocation 96 */ valueOfMethodCall( MethodInvocationNode node, VisitorState state, Nullness defaultValue, AccessPath.AccessPathContext apContext)97 public Nullness valueOfMethodCall( 98 MethodInvocationNode node, 99 VisitorState state, 100 Nullness defaultValue, 101 AccessPath.AccessPathContext apContext) { 102 AccessPath accessPath = AccessPath.fromMethodCall(node, state, apContext); 103 if (accessPath == null) { 104 return defaultValue; 105 } 106 return contents.getOrDefault(accessPath, defaultValue); 107 } 108 109 /** 110 * Get all access paths in this store with a particular nullness value. 111 * 112 * @param value a nullness value 113 * @return all access paths in this store that have the given nullness value 114 */ getAccessPathsWithValue(Nullness value)115 public Set<AccessPath> getAccessPathsWithValue(Nullness value) { 116 Set<AccessPath> result = new LinkedHashSet<>(); 117 for (Map.Entry<AccessPath, Nullness> entry : contents.entrySet()) { 118 if (value.equals(entry.getValue())) { 119 result.add(entry.getKey()); 120 } 121 } 122 return result; 123 } 124 125 /** 126 * If this store maps an access path {@code p} whose map-get argument is an {@link 127 * IteratorContentsKey} whose variable is {@code iteratorVar}, returns {@code p}. Otherwise, 128 * returns {@code null}. 129 */ 130 @Nullable getMapGetIteratorContentsAccessPath(LocalVariableNode iteratorVar)131 public AccessPath getMapGetIteratorContentsAccessPath(LocalVariableNode iteratorVar) { 132 for (AccessPath accessPath : contents.keySet()) { 133 MapKey mapGetArg = accessPath.getMapGetArg(); 134 if (mapGetArg instanceof IteratorContentsKey) { 135 IteratorContentsKey iteratorContentsKey = (IteratorContentsKey) mapGetArg; 136 if (iteratorContentsKey.getIteratorVarElement().equals(iteratorVar.getElement())) { 137 return accessPath; 138 } 139 } 140 } 141 return null; 142 } 143 144 /** 145 * Gets the {@link Nullness} value of an access path. 146 * 147 * @param accessPath The access path. 148 * @return The {@link Nullness} value of the access path. 149 */ getNullnessOfAccessPath(AccessPath accessPath)150 public Nullness getNullnessOfAccessPath(AccessPath accessPath) { 151 if (contents == null) { 152 return Nullness.NULLABLE; 153 } 154 return contents.getOrDefault(accessPath, Nullness.NULLABLE); 155 } 156 toBuilder()157 public Builder toBuilder() { 158 return new Builder(this); 159 } 160 161 @Override copy()162 public NullnessStore copy() { 163 return this; 164 } 165 166 @Override leastUpperBound(NullnessStore other)167 public NullnessStore leastUpperBound(NullnessStore other) { 168 NullnessStore.Builder result = NullnessStore.empty().toBuilder(); 169 for (AccessPath ap : intersection(contents.keySet(), other.contents.keySet())) { 170 Nullness apContents = contents.get(ap); 171 if (apContents == null) { 172 throw new RuntimeException("null contents for " + ap); 173 } 174 Nullness otherAPContents = other.contents.get(ap); 175 if (otherAPContents == null) { 176 throw new RuntimeException("null other contents for " + ap); 177 } 178 result.contents.put(ap, apContents.leastUpperBound(otherAPContents)); 179 } 180 return result.build(); 181 } 182 183 @Override widenedUpperBound(NullnessStore vNullnessStore)184 public NullnessStore widenedUpperBound(NullnessStore vNullnessStore) { 185 return leastUpperBound(vNullnessStore); 186 } 187 188 @Override equals(@ullable Object o)189 public boolean equals(@Nullable Object o) { 190 if (!(o instanceof NullnessStore)) { 191 return false; 192 } 193 NullnessStore other = (NullnessStore) o; 194 return contents.equals(other.contents); 195 } 196 197 @Override hashCode()198 public int hashCode() { 199 return contents.hashCode(); 200 } 201 202 @Override toString()203 public String toString() { 204 return contents.toString(); 205 } 206 207 @Override canAlias(JavaExpression a, JavaExpression b)208 public boolean canAlias(JavaExpression a, JavaExpression b) { 209 return true; 210 } 211 212 @Override visualize(CFGVisualizer<?, NullnessStore, ?> viz)213 public String visualize(CFGVisualizer<?, NullnessStore, ?> viz) { 214 throw new UnsupportedOperationException(); 215 } 216 217 /** 218 * Takes the Access Paths rooted at specific locals in this NullnessStore and translates them to 219 * paths rooted at different/renamed local variables. 220 * 221 * <p>This method is used to patch-around the paths inter-procedurally when handling certain 222 * libraries. For example, by {@code handlers.RxNullabilityPropagator} to translate access paths 223 * relative to the argument of a filter(...) method, to paths relative to the argument of a 224 * map(...) method in a filter(...).map(...) chain pattern. 225 * 226 * @param localVarTranslations A map from local variable nodes to local variable nodes, indicating 227 * the desired re-rooting / re-naming. 228 * @return A store containing only those access paths in {@code this} which are relative to 229 * variables in the domain of {@code localVarTranslations}, with each access path re-rooted to 230 * be relative to the corresponding local variable in the co-domain of the map. 231 */ uprootAccessPaths( Map<LocalVariableNode, LocalVariableNode> localVarTranslations)232 public NullnessStore uprootAccessPaths( 233 Map<LocalVariableNode, LocalVariableNode> localVarTranslations) { 234 NullnessStore.Builder nullnessBuilder = NullnessStore.empty().toBuilder(); 235 for (AccessPath ap : contents.keySet()) { 236 Element element = ap.getRoot(); 237 if (element == null) { 238 // Access path is rooted at the receiver, so we don't need to uproot it 239 continue; 240 } 241 for (LocalVariableNode fromVar : localVarTranslations.keySet()) { 242 if (element.equals(fromVar.getElement())) { 243 LocalVariableNode toVar = localVarTranslations.get(fromVar); 244 AccessPath newAP = AccessPath.switchRoot(ap, toVar.getElement()); 245 nullnessBuilder.setInformation(newAP, contents.get(ap)); 246 } 247 } 248 } 249 return nullnessBuilder.build(); 250 } 251 252 /** 253 * Get access paths matching a predicate. 254 * 255 * @param pred predicate over {@link AccessPath}s 256 * @return NullnessStore containing only AccessPaths that pass the predicate 257 */ filterAccessPaths(Predicate<AccessPath> pred)258 public NullnessStore filterAccessPaths(Predicate<AccessPath> pred) { 259 return new NullnessStore( 260 contents.entrySet().stream() 261 .filter(e -> pred.test(e.getKey())) 262 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); 263 } 264 265 /** class for building up instances of the store. */ 266 public static final class Builder { 267 private final Map<AccessPath, Nullness> contents; 268 Builder(NullnessStore prototype)269 Builder(NullnessStore prototype) { 270 271 contents = new HashMap<>(prototype.contents); 272 } 273 274 /** 275 * Sets the value for the given variable. {@code element} must come from a call to {@link 276 * LocalVariableNode#getElement()} or {@link 277 * org.checkerframework.nullaway.javacutil.TreeUtils#elementFromDeclaration} ({@link 278 * org.checkerframework.nullaway.dataflow.cfg.node.VariableDeclarationNode#getTree()}). 279 * 280 * @param ap relevant access path 281 * @param value fact for access path 282 * @return the new builder 283 */ setInformation(AccessPath ap, Nullness value)284 public NullnessStore.Builder setInformation(AccessPath ap, Nullness value) { 285 contents.put(checkNotNull(ap), checkNotNull(value)); 286 return this; 287 } 288 289 /** 290 * Construct the immutable NullnessStore instance. 291 * 292 * @return a store constructed from everything added to the builder 293 */ build()294 public NullnessStore build() { 295 return new NullnessStore(contents); 296 } 297 } 298 } 299