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 * Produce an empty store. 55 * 56 * @return an empty store 57 */ empty()58 public static NullnessStore empty() { 59 return EMPTY; 60 } 61 62 /** 63 * Get the nullness for a local variable. 64 * 65 * @param node node representing local variable 66 * @param defaultValue default value if we have no fact 67 * @return fact associated with local 68 */ valueOfLocalVariable(LocalVariableNode node, Nullness defaultValue)69 public Nullness valueOfLocalVariable(LocalVariableNode node, Nullness defaultValue) { 70 Nullness result = contents.get(AccessPath.fromLocal(node)); 71 return result != null ? result : 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 Nullness result = contents.get(path); 88 return result != null ? result : defaultValue; 89 } 90 91 /** 92 * Get the nullness of a method call. 93 * 94 * @param node node representing method invocation 95 * @param defaultValue default value if we have no fact 96 * @return fact associated with method invocation 97 */ valueOfMethodCall( MethodInvocationNode node, VisitorState state, Nullness defaultValue, AccessPath.AccessPathContext apContext)98 public Nullness valueOfMethodCall( 99 MethodInvocationNode node, 100 VisitorState state, 101 Nullness defaultValue, 102 AccessPath.AccessPathContext apContext) { 103 AccessPath accessPath = AccessPath.fromMethodCall(node, state, apContext); 104 if (accessPath == null) { 105 return defaultValue; 106 } 107 Nullness result = contents.get(accessPath); 108 return result != null ? result : defaultValue; 109 } 110 111 /** 112 * Get all access paths in this store with a particular nullness value. 113 * 114 * @param value a nullness value 115 * @return all access paths in this store that have the given nullness value 116 */ getAccessPathsWithValue(Nullness value)117 public Set<AccessPath> getAccessPathsWithValue(Nullness value) { 118 Set<AccessPath> result = new LinkedHashSet<>(); 119 for (Map.Entry<AccessPath, Nullness> entry : contents.entrySet()) { 120 if (value.equals(entry.getValue())) { 121 result.add(entry.getKey()); 122 } 123 } 124 return result; 125 } 126 127 /** 128 * If this store maps an access path {@code p} whose map-get argument is an {@link 129 * IteratorContentsKey} whose variable is {@code iteratorVar}, returns {@code p}. Otherwise, 130 * returns {@code null}. 131 */ 132 @Nullable getMapGetIteratorContentsAccessPath(LocalVariableNode iteratorVar)133 public AccessPath getMapGetIteratorContentsAccessPath(LocalVariableNode iteratorVar) { 134 for (AccessPath accessPath : contents.keySet()) { 135 MapKey mapGetArg = accessPath.getMapGetArg(); 136 if (mapGetArg instanceof IteratorContentsKey) { 137 IteratorContentsKey iteratorContentsKey = (IteratorContentsKey) mapGetArg; 138 if (iteratorContentsKey.getIteratorVarElement().equals(iteratorVar.getElement())) { 139 return accessPath; 140 } 141 } 142 } 143 return null; 144 } 145 /** 146 * Gets the {@link Nullness} value of an access path. 147 * 148 * @param accessPath The access path. 149 * @return The {@link Nullness} value of the access path. 150 */ getNullnessOfAccessPath(AccessPath accessPath)151 public Nullness getNullnessOfAccessPath(AccessPath accessPath) { 152 if (contents == null) { 153 return Nullness.NULLABLE; 154 } 155 Nullness nullness = contents.get(accessPath); 156 return (nullness == null) ? Nullness.NULLABLE : nullness; 157 } 158 toBuilder()159 public Builder toBuilder() { 160 return new Builder(this); 161 } 162 163 @Override copy()164 public NullnessStore copy() { 165 return this; 166 } 167 168 @Override leastUpperBound(NullnessStore other)169 public NullnessStore leastUpperBound(NullnessStore other) { 170 NullnessStore.Builder result = NullnessStore.empty().toBuilder(); 171 for (AccessPath ap : intersection(contents.keySet(), other.contents.keySet())) { 172 Nullness apContents = contents.get(ap); 173 if (apContents == null) { 174 throw new RuntimeException("null contents for " + ap); 175 } 176 Nullness otherAPContents = other.contents.get(ap); 177 if (otherAPContents == null) { 178 throw new RuntimeException("null other contents for " + ap); 179 } 180 result.contents.put(ap, apContents.leastUpperBound(otherAPContents)); 181 } 182 return result.build(); 183 } 184 185 @Override widenedUpperBound(NullnessStore vNullnessStore)186 public NullnessStore widenedUpperBound(NullnessStore vNullnessStore) { 187 return leastUpperBound(vNullnessStore); 188 } 189 190 @Override equals(Object o)191 public boolean equals(Object o) { 192 if (!(o instanceof NullnessStore)) { 193 return false; 194 } 195 NullnessStore other = (NullnessStore) o; 196 return contents.equals(other.contents); 197 } 198 199 @Override hashCode()200 public int hashCode() { 201 return contents.hashCode(); 202 } 203 204 @Override toString()205 public String toString() { 206 return contents.toString(); 207 } 208 209 @Override canAlias(JavaExpression a, JavaExpression b)210 public boolean canAlias(JavaExpression a, JavaExpression b) { 211 return true; 212 } 213 214 @Override visualize(CFGVisualizer<?, NullnessStore, ?> viz)215 public String visualize(CFGVisualizer<?, NullnessStore, ?> viz) { 216 throw new UnsupportedOperationException(); 217 } 218 219 /** 220 * Takes the Access Paths rooted at specific locals in this NullnessStore and translates them to 221 * paths rooted at different/renamed local variables. 222 * 223 * <p>This method is used to patch-around the paths inter-procedurally when handling certain 224 * libraries. For example, by {@code handlers.RxNullabilityPropagator} to translate access paths 225 * relative to the argument of a filter(...) method, to paths relative to the argument of a 226 * map(...) method in a filter(...).map(...) chain pattern. 227 * 228 * @param localVarTranslations A map from local variable nodes to local variable nodes, indicating 229 * the desired re-rooting / re-naming. 230 * @return A store containing only those access paths in {@code this} which are relative to 231 * variables in the domain of {@code localVarTranslations}, with each access path re-rooted to 232 * be relative to the corresponding local variable in the co-domain of the map. 233 */ uprootAccessPaths( Map<LocalVariableNode, LocalVariableNode> localVarTranslations)234 public NullnessStore uprootAccessPaths( 235 Map<LocalVariableNode, LocalVariableNode> localVarTranslations) { 236 NullnessStore.Builder nullnessBuilder = NullnessStore.empty().toBuilder(); 237 for (AccessPath ap : contents.keySet()) { 238 if (ap.getRoot().isReceiver()) { 239 continue; 240 } 241 Element varElement = ap.getRoot().getVarElement(); 242 for (LocalVariableNode fromVar : localVarTranslations.keySet()) { 243 if (varElement.equals(fromVar.getElement())) { 244 LocalVariableNode toVar = localVarTranslations.get(fromVar); 245 AccessPath newAP = 246 new AccessPath(new AccessPath.Root(toVar.getElement()), ap.getElements()); 247 nullnessBuilder.setInformation(newAP, contents.get(ap)); 248 } 249 } 250 } 251 return nullnessBuilder.build(); 252 } 253 254 /** 255 * Get access paths matching a predicate. 256 * 257 * @param pred predicate over {@link AccessPath}s 258 * @return NullnessStore containing only AccessPaths that pass the predicate 259 */ filterAccessPaths(Predicate<AccessPath> pred)260 public NullnessStore filterAccessPaths(Predicate<AccessPath> pred) { 261 return new NullnessStore( 262 contents 263 .entrySet() 264 .stream() 265 .filter(e -> pred.test(e.getKey())) 266 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); 267 } 268 269 /** class for building up instances of the store. */ 270 public static final class Builder { 271 private final Map<AccessPath, Nullness> contents; 272 Builder(NullnessStore prototype)273 Builder(NullnessStore prototype) { 274 275 contents = new HashMap<>(prototype.contents); 276 } 277 278 /** 279 * Sets the value for the given variable. {@code element} must come from a call to {@link 280 * LocalVariableNode#getElement()} or {@link 281 * org.checkerframework.nullaway.javacutil.TreeUtils#elementFromDeclaration} ({@link 282 * org.checkerframework.nullaway.dataflow.cfg.node.VariableDeclarationNode#getTree()}). 283 * 284 * @param ap relevant access path 285 * @param value fact for access path 286 * @return the new builder 287 */ setInformation(AccessPath ap, Nullness value)288 public NullnessStore.Builder setInformation(AccessPath ap, Nullness value) { 289 contents.put(checkNotNull(ap), checkNotNull(value)); 290 return this; 291 } 292 293 /** 294 * Construct the immutable NullnessStore instance. 295 * 296 * @return a store constructed from everything added to the builder 297 */ build()298 public NullnessStore build() { 299 return new NullnessStore(contents); 300 } 301 } 302 } 303