1 package com.uber.nullaway.handlers.temporary; 2 3 import com.google.errorprone.VisitorState; 4 import com.google.errorprone.util.ASTHelpers; 5 import com.sun.source.tree.LambdaExpressionTree; 6 import com.sun.source.tree.MethodInvocationTree; 7 import com.sun.tools.javac.code.Symbol; 8 import com.uber.nullaway.NullabilityUtil; 9 import com.uber.nullaway.Nullness; 10 import com.uber.nullaway.handlers.BaseNoOpHandler; 11 import java.util.Arrays; 12 import javax.lang.model.element.Name; 13 14 /** 15 * This handler provides a temporary workaround due to our lack of support for generics, which 16 * allows natural usage of Futures/FluentFuture Guava APIs. It can potentially introduce false 17 * negatives, however, and should be deprecated as soon as full generic support is available. 18 * 19 * <p>This works by special casing the return nullability of {@link com.google.common.base.Function} 20 * and {@link com.google.common.util.concurrent.AsyncFunction} to be e.g. {@code Function<@Nullable 21 * T>} whenever these functional interfaces are implemented as a lambda expression passed to a list 22 * of specific methods of {@link com.google.common.util.concurrent.FluentFuture} or {@link 23 * com.google.common.util.concurrent.Futures}. We cannot currently check that {@code T} for {@code 24 * FluentFuture<T>} is a {@code @Nullable} type, so this is unsound. However, we have found many 25 * cases in practice where these lambdas include {@code null} returns, which were already being 26 * ignored (due to a bug) before PR #765. This handler offers the best possible support for these 27 * cases, at least until our generics support is mature enough to handle them. 28 * 29 * <p>Note: Package {@code com.uber.nullaway.handlers.temporary} is meant for this sort of temporary 30 * workaround handler, to be removed as future NullAway features make them unnecessary. This is a 31 * hack, but the best of a bunch of bad options. 32 */ 33 public class FluentFutureHandler extends BaseNoOpHandler { 34 35 private static final String GUAVA_FUNCTION_CLASS_NAME = "com.google.common.base.Function"; 36 private static final String GUAVA_ASYNC_FUNCTION_CLASS_NAME = 37 "com.google.common.util.concurrent.AsyncFunction"; 38 private static final String FLUENT_FUTURE_CLASS_NAME = 39 "com.google.common.util.concurrent.FluentFuture"; 40 private static final String FUTURES_CLASS_NAME = "com.google.common.util.concurrent.Futures"; 41 private static final String FUNCTION_APPLY_METHOD_NAME = "apply"; 42 private static final String[] FLUENT_FUTURE_INCLUDE_LIST_METHODS = { 43 "catching", "catchingAsync", "transform", "transformAsync" 44 }; 45 isGuavaFunctionDotApply(Symbol.MethodSymbol methodSymbol)46 private static boolean isGuavaFunctionDotApply(Symbol.MethodSymbol methodSymbol) { 47 Name className = methodSymbol.enclClass().flatName(); 48 return (className.contentEquals(GUAVA_FUNCTION_CLASS_NAME) 49 || className.contentEquals(GUAVA_ASYNC_FUNCTION_CLASS_NAME)) 50 && methodSymbol.name.contentEquals(FUNCTION_APPLY_METHOD_NAME); 51 } 52 isFluentFutureIncludeListMethod(Symbol.MethodSymbol methodSymbol)53 private static boolean isFluentFutureIncludeListMethod(Symbol.MethodSymbol methodSymbol) { 54 Name className = methodSymbol.enclClass().flatName(); 55 return (className.contentEquals(FLUENT_FUTURE_CLASS_NAME) 56 || className.contentEquals(FUTURES_CLASS_NAME)) 57 && Arrays.stream(FLUENT_FUTURE_INCLUDE_LIST_METHODS) 58 .anyMatch(s -> methodSymbol.name.contentEquals(s)); 59 } 60 61 @Override onOverrideMethodReturnNullability( Symbol.MethodSymbol methodSymbol, VisitorState state, boolean isAnnotated, Nullness returnNullness)62 public Nullness onOverrideMethodReturnNullability( 63 Symbol.MethodSymbol methodSymbol, 64 VisitorState state, 65 boolean isAnnotated, 66 Nullness returnNullness) { 67 // We only care about lambda's implementing Guava's Function 68 if (!isGuavaFunctionDotApply(methodSymbol)) { 69 return returnNullness; 70 } 71 // Check if we are inside a lambda passed as an argument to a method call: 72 LambdaExpressionTree enclosingLambda = 73 ASTHelpers.findEnclosingNode(state.getPath(), LambdaExpressionTree.class); 74 if (enclosingLambda == null 75 || !NullabilityUtil.getFunctionalInterfaceMethod(enclosingLambda, state.getTypes()) 76 .equals(methodSymbol)) { 77 return returnNullness; 78 } 79 MethodInvocationTree methodInvocation = 80 ASTHelpers.findEnclosingNode(state.getPath(), MethodInvocationTree.class); 81 if (methodInvocation == null || !methodInvocation.getArguments().contains(enclosingLambda)) { 82 return returnNullness; 83 } 84 // Check if that method call is one of the FluentFuture APIs we care about 85 Symbol.MethodSymbol lambdaConsumerMethodSymbol = ASTHelpers.getSymbol(methodInvocation); 86 if (!isFluentFutureIncludeListMethod(lambdaConsumerMethodSymbol)) { 87 return returnNullness; 88 } 89 return Nullness.NULLABLE; 90 } 91 } 92