From 5fbee1ff8682762cd2f291070491fe3e476787cf Mon Sep 17 00:00:00 2001 From: Manu Sridharan Date: Wed, 6 Dec 2023 05:18:48 -0800 Subject: [PATCH] Model Lombok-generated equals methods as having a @Nullable parameter (#874) See the new test in `UsesDTO`. Without this change, NullAway reports an incorrect warning for this code that passes a `@Nullable` parameter to a Lombok-generated `equals()` method. The real logic change in this PR is very small. Most of the changes are required to make a `VisitorState` object available in a `Handler` method. --- .../main/java/com/uber/nullaway/NullAway.java | 7 ++---- .../AccessPathNullnessPropagation.java | 2 +- .../CoreNullnessStoreInitializer.java | 16 +++++++------- .../dataflow/NullnessStoreInitializer.java | 8 +++---- .../nullaway/handlers/BaseNoOpHandler.java | 2 +- .../nullaway/handlers/CompositeHandler.java | 4 ++-- .../com/uber/nullaway/handlers/Handler.java | 4 ++-- .../handlers/InferredJARModelsHandler.java | 3 +-- .../handlers/LibraryModelsHandler.java | 4 ++-- .../uber/nullaway/handlers/LombokHandler.java | 22 +++++++++++++++++++ .../RestrictiveAnnotationHandler.java | 2 +- .../ContractNullnessStoreInitializer.java | 8 +++---- .../main/java/com/uber/lombok/UsesDTO.java | 5 +++++ 13 files changed, 53 insertions(+), 34 deletions(-) diff --git a/nullaway/src/main/java/com/uber/nullaway/NullAway.java b/nullaway/src/main/java/com/uber/nullaway/NullAway.java index 744bdddc42..f7ee780e5b 100644 --- a/nullaway/src/main/java/com/uber/nullaway/NullAway.java +++ b/nullaway/src/main/java/com/uber/nullaway/NullAway.java @@ -760,10 +760,7 @@ private Description checkParamOverriding( // Check handlers for any further/overriding nullness information overriddenMethodArgNullnessMap = handler.onOverrideMethodInvocationParametersNullability( - state.context, - overriddenMethod, - isOverriddenMethodAnnotated, - overriddenMethodArgNullnessMap); + state, overriddenMethod, isOverriddenMethodAnnotated, overriddenMethodArgNullnessMap); // If we have an unbound method reference, the first parameter of the overridden method must be // @NonNull, as this parameter will be used as a method receiver inside the generated lambda. @@ -1714,7 +1711,7 @@ private Description handleInvocation( // Allow handlers to override the list of non-null argument positions argumentPositionNullness = handler.onOverrideMethodInvocationParametersNullability( - state.context, methodSymbol, isMethodAnnotated, argumentPositionNullness); + state, methodSymbol, isMethodAnnotated, argumentPositionNullness); // now actually check the arguments // NOTE: the case of an invocation on a possibly-null reference diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java index b68665ad0b..fb18368afe 100644 --- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java +++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java @@ -206,7 +206,7 @@ private static Node unwrapAssignExpr(Node node) { public NullnessStore initialStore( UnderlyingAST underlyingAST, List parameters) { return nullnessStoreInitializer.getInitialStore( - underlyingAST, parameters, handler, state.context, state.getTypes(), config); + underlyingAST, parameters, handler, state, config); } @Override diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java index 2dfce9fc74..170ccb1a7d 100644 --- a/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java +++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java @@ -3,6 +3,7 @@ import static com.uber.nullaway.Nullness.NONNULL; import static com.uber.nullaway.Nullness.NULLABLE; +import com.google.errorprone.VisitorState; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.tree.LambdaExpressionTree; @@ -30,9 +31,9 @@ public NullnessStore getInitialStore( UnderlyingAST underlyingAST, List parameters, Handler handler, - Context context, - Types types, + VisitorState state, Config config) { + Context context = state.context; if (underlyingAST.getKind().equals(UnderlyingAST.Kind.ARBITRARY_CODE)) { // not a method or a lambda; an initializer expression or block UnderlyingAST.CFGStatement ast = (UnderlyingAST.CFGStatement) underlyingAST; @@ -44,8 +45,7 @@ public NullnessStore getInitialStore( (UnderlyingAST.CFGLambda) underlyingAST, parameters, handler, - context, - types, + state, config, getCodeAnnotationInfo(context)); } else { @@ -77,13 +77,12 @@ private static NullnessStore lambdaInitialStore( UnderlyingAST.CFGLambda underlyingAST, List parameters, Handler handler, - Context context, - Types types, + VisitorState state, Config config, CodeAnnotationInfo codeAnnotationInfo) { // include nullness info for locals from enclosing environment EnclosingEnvironmentNullness environmentNullness = - EnclosingEnvironmentNullness.instance(context); + EnclosingEnvironmentNullness.instance(state.context); NullnessStore environmentMapping = Objects.requireNonNull( environmentNullness.getEnvironmentMapping(underlyingAST.getLambdaTree()), @@ -91,6 +90,7 @@ private static NullnessStore lambdaInitialStore( NullnessStore.Builder result = environmentMapping.toBuilder(); LambdaExpressionTree code = underlyingAST.getLambdaTree(); // need to check annotation for i'th parameter of functional interface declaration + Types types = state.getTypes(); Symbol.MethodSymbol fiMethodSymbol = NullabilityUtil.getFunctionalInterfaceMethod(code, types); com.sun.tools.javac.util.List fiMethodParameters = fiMethodSymbol.getParameters(); @@ -119,7 +119,7 @@ private static NullnessStore lambdaInitialStore( } fiArgumentPositionNullness = handler.onOverrideMethodInvocationParametersNullability( - context, fiMethodSymbol, isFIAnnotated, fiArgumentPositionNullness); + state, fiMethodSymbol, isFIAnnotated, fiArgumentPositionNullness); for (int i = 0; i < parameters.size(); i++) { LocalVariableNode param = parameters.get(i); diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStoreInitializer.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStoreInitializer.java index c654621933..37c68d3a61 100644 --- a/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStoreInitializer.java +++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStoreInitializer.java @@ -1,10 +1,10 @@ package com.uber.nullaway.dataflow; +import com.google.errorprone.VisitorState; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.util.Trees; import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.code.Types; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.util.Context; import com.uber.nullaway.Config; @@ -30,8 +30,7 @@ public abstract class NullnessStoreInitializer { * @param underlyingAST The AST node being matched. * @param parameters list of local variable nodes. * @param handler reference to the handler invoked. - * @param context context. - * @param types types. + * @param state the visitor state. * @param config config for analysis. * @return Initial Nullness store. */ @@ -39,8 +38,7 @@ public abstract NullnessStore getInitialStore( UnderlyingAST underlyingAST, List parameters, Handler handler, - Context context, - Types types, + VisitorState state, Config config); /** diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java index 242a96a1dc..4fc0c0c8f2 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java @@ -120,7 +120,7 @@ public Nullness onOverrideMethodReturnNullability( @Override public Nullness[] onOverrideMethodInvocationParametersNullability( - Context context, + VisitorState state, Symbol.MethodSymbol methodSymbol, boolean isAnnotated, Nullness[] argumentPositionNullness) { diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java index 31617da68b..3602a5ff03 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java @@ -137,14 +137,14 @@ public Nullness onOverrideMethodReturnNullability( @Override public Nullness[] onOverrideMethodInvocationParametersNullability( - Context context, + VisitorState state, Symbol.MethodSymbol methodSymbol, boolean isAnnotated, Nullness[] argumentPositionNullness) { for (Handler h : handlers) { argumentPositionNullness = h.onOverrideMethodInvocationParametersNullability( - context, methodSymbol, isAnnotated, argumentPositionNullness); + state, methodSymbol, isAnnotated, argumentPositionNullness); } return argumentPositionNullness; } diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java index 835c01fbfd..d509d7bdda 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java @@ -182,7 +182,7 @@ Nullness onOverrideMethodReturnNullability( * considered isAnnotated or not. We use a mutable map for performance, but it should not outlive * the chain of handler invocations. * - * @param context The current context. + * @param state The current visitor state. * @param methodSymbol The method symbol for the method in question. * @param isAnnotated A boolean flag indicating whether the called method is considered to be * within isAnnotated or unannotated code, used to avoid querying for this information @@ -195,7 +195,7 @@ Nullness onOverrideMethodReturnNullability( * handler. */ Nullness[] onOverrideMethodInvocationParametersNullability( - Context context, + VisitorState state, Symbol.MethodSymbol methodSymbol, boolean isAnnotated, Nullness[] argumentPositionNullness); diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java index bd70059580..71dbd92b62 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java @@ -28,7 +28,6 @@ import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; -import com.sun.tools.javac.util.Context; import com.uber.nullaway.Config; import com.uber.nullaway.NullAway; import com.uber.nullaway.Nullness; @@ -122,7 +121,7 @@ private void loadStubxFiles() { @Override public Nullness[] onOverrideMethodInvocationParametersNullability( - Context context, + VisitorState state, Symbol.MethodSymbol methodSymbol, boolean isAnnotated, Nullness[] argumentPositionNullness) { diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java index e000c4fdcf..e5e943c553 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java @@ -82,11 +82,11 @@ public LibraryModelsHandler(Config config) { @Override public Nullness[] onOverrideMethodInvocationParametersNullability( - Context context, + VisitorState state, Symbol.MethodSymbol methodSymbol, boolean isAnnotated, Nullness[] argumentPositionNullness) { - OptimizedLibraryModels optimizedLibraryModels = getOptLibraryModels(context); + OptimizedLibraryModels optimizedLibraryModels = getOptLibraryModels(state.context); ImmutableSet nullableParamsFromModel = optimizedLibraryModels.explicitlyNullableParameters(methodSymbol); ImmutableSet nonNullParamsFromModel = diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java index 7069497800..7d76611b9d 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java @@ -86,4 +86,26 @@ public Nullness onOverrideMethodReturnNullability( } return returnNullness; } + + /** + * Mark the first argument of Lombok-generated {@code equals} methods as {@code @Nullable}, since + * Lombok does not generate the annotation. + */ + @Override + public Nullness[] onOverrideMethodInvocationParametersNullability( + VisitorState state, + Symbol.MethodSymbol methodSymbol, + boolean isAnnotated, + Nullness[] argumentPositionNullness) { + if (ASTHelpers.hasAnnotation(methodSymbol, LOMBOK_GENERATED_ANNOTATION_NAME, state)) { + // We assume that Lombok-generated equals methods with a single argument override + // Object.equals and are not an overload. + if (methodSymbol.getSimpleName().contentEquals("equals") + && methodSymbol.params().size() == 1) { + // The parameter is not annotated with @Nullable, but it should be. + argumentPositionNullness[0] = Nullness.NULLABLE; + } + } + return argumentPositionNullness; + } } diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java index 561ac6ebae..7c5c9baf8f 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java @@ -99,7 +99,7 @@ private CodeAnnotationInfo getCodeAnnotationInfo(Context context) { @Override public Nullness[] onOverrideMethodInvocationParametersNullability( - Context context, + VisitorState state, Symbol.MethodSymbol methodSymbol, boolean isAnnotated, Nullness[] argumentPositionNullness) { diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java index 41b25d525d..155bd900ca 100644 --- a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java +++ b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java @@ -3,12 +3,11 @@ import static com.uber.nullaway.Nullness.NONNULL; import static com.uber.nullaway.Nullness.NULLABLE; +import com.google.errorprone.VisitorState; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.code.Types; -import com.sun.tools.javac.util.Context; import com.uber.nullaway.Config; import com.uber.nullaway.Nullness; import com.uber.nullaway.dataflow.AccessPath; @@ -31,8 +30,7 @@ public NullnessStore getInitialStore( UnderlyingAST underlyingAST, List parameters, Handler handler, - Context context, - Types types, + VisitorState state, Config config) { assert underlyingAST.getKind() == UnderlyingAST.Kind.METHOD; @@ -49,7 +47,7 @@ public NullnessStore getInitialStore( String[] parts = clauses[0].split("->"); String[] antecedent = parts[0].split(","); - NullnessStore envStore = getEnvNullnessStoreForClass(classTree, context); + NullnessStore envStore = getEnvNullnessStoreForClass(classTree, state.context); NullnessStore.Builder result = envStore.toBuilder(); for (int i = 0; i < antecedent.length; ++i) { diff --git a/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesDTO.java b/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesDTO.java index f236a65056..bef6f7898f 100644 --- a/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesDTO.java +++ b/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesDTO.java @@ -38,4 +38,9 @@ public static String foo(LombokDTO ldto) { s += (ldto.getNullableField() == null ? "" : ldto.getNullableField().toString()); return s; } + + public static boolean callEquals(LombokDTO ldto, @Nullable Object o) { + // No error should be reported since equals() parameter should be treated as @Nullable + return ldto.equals(o); + } }