Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom path to Optional class for Optional emptiness handler #288

Merged
merged 5 commits into from Mar 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java
Expand Up @@ -70,6 +70,8 @@ public abstract class AbstractConfig implements Config {

protected boolean checkOptionalEmptiness;

protected Set<String> optionalClassPaths;

protected boolean assertsEnabled;

/**
Expand Down Expand Up @@ -189,6 +191,11 @@ public boolean checkOptionalEmptiness() {
return checkOptionalEmptiness;
}

@Override
public Set<String> getOptionalClassPaths() {
return optionalClassPaths;
}

@Override
public boolean assertsEnabled() {
return assertsEnabled;
Expand Down
7 changes: 7 additions & 0 deletions nullaway/src/main/java/com/uber/nullaway/Config.java
Expand Up @@ -24,6 +24,7 @@

import com.google.common.collect.ImmutableSet;
import com.sun.tools.javac.code.Symbol;
import java.util.Set;
import javax.annotation.Nullable;

/** Provides configuration parameters for the nullability checker. */
Expand Down Expand Up @@ -115,6 +116,12 @@ public interface Config {
*/
boolean checkOptionalEmptiness();

/**
* @return the paths for Optional class. The list always contains the path of {@link
* java.util.Optional}.
*/
Set<String> getOptionalClassPaths();

/**
* @return the fully qualified name of a method which will take a @Nullable version of a value and
* return an @NonNull copy (likely through an unsafe downcast, but performing runtime checking
Expand Down
Expand Up @@ -27,6 +27,7 @@

import com.google.common.collect.ImmutableSet;
import com.sun.tools.javac.code.Symbol;
import java.util.Set;
import javax.annotation.Nullable;

/**
Expand Down Expand Up @@ -117,6 +118,11 @@ public boolean checkOptionalEmptiness() {
throw new IllegalStateException(error_msg);
}

@Override
public Set<String> getOptionalClassPaths() {
throw new IllegalStateException(error_msg);
}

@Override
@Nullable
public String getCastToNonNullMethod() {
Expand Down
Expand Up @@ -56,6 +56,8 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
static final String FL_ACKNOWLEDGE_RESTRICTIVE =
EP_FL_NAMESPACE + ":AcknowledgeRestrictiveAnnotations";
static final String FL_CHECK_OPTIONAL_EMPTINESS = EP_FL_NAMESPACE + ":CheckOptionalEmptiness";
static final String FL_OPTIONAL_CLASS_PATHS =
EP_FL_NAMESPACE + ":CheckOptionalEmptinessCustomClasses";
static final String FL_SUPPRESS_COMMENT = EP_FL_NAMESPACE + ":AutoFixSuppressionComment";
/** --- JarInfer configs --- */
static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";
Expand Down Expand Up @@ -141,6 +143,11 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
getFlagStringSet(flags, FL_EXCLUDED_FIELD_ANNOT, DEFAULT_EXCLUDED_FIELD_ANNOT));
castToNonNullMethod = flags.get(FL_CTNN_METHOD).orElse(null);
autofixSuppressionComment = flags.get(FL_SUPPRESS_COMMENT).orElse("");
optionalClassPaths =
new ImmutableSet.Builder<String>()
.addAll(getFlagStringSet(flags, FL_OPTIONAL_CLASS_PATHS))
.add("java.util.Optional")
.build();
if (autofixSuppressionComment.contains("\n")) {
throw new IllegalStateException(
"Invalid -XepOpt" + FL_SUPPRESS_COMMENT + " value. Comment must be single line.");
Expand Down
Expand Up @@ -51,7 +51,7 @@ public static Handler buildDefault(Config config) {
handlerListBuilder.add(new ContractHandler());
handlerListBuilder.add(new ApacheThriftIsSetHandler());
if (config.checkOptionalEmptiness()) {
handlerListBuilder.add(new OptionalEmptinessHandler());
handlerListBuilder.add(new OptionalEmptinessHandler(config));
}
return new CompositeHandler(handlerListBuilder.build());
}
Expand Down
Expand Up @@ -21,7 +21,7 @@
*/
package com.uber.nullaway.handlers;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
Expand All @@ -31,12 +31,13 @@
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.Config;
import com.uber.nullaway.ErrorMessage;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import java.util.Optional;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
Expand All @@ -49,9 +50,12 @@
*/
public class OptionalEmptinessHandler extends BaseNoOpHandler {

private static String OPTIONAL_PATH = "java.util.Optional";
@Nullable private ImmutableSet<Type> optionalTypes;
private final Config config;

@Nullable private Optional<Type> optionalType;
OptionalEmptinessHandler(Config config) {
this.config = config;
}

@Override
public boolean onOverrideMayBeNullExpr(
Expand All @@ -66,11 +70,14 @@ && optionalIsGetCall((Symbol.MethodSymbol) ASTHelpers.getSymbol(expr), state.get
@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
if (optionalType == null) {
optionalType =
Optional.ofNullable(state.getTypeFromString(OPTIONAL_PATH))
.map(state.getTypes()::erasure);
}
optionalTypes =
config
.getOptionalClassPaths()
.stream()
.map(state::getTypeFromString)
.filter(Objects::nonNull)
.map(state.getTypes()::erasure)
.collect(ImmutableSet.toImmutableSet());
}

@Override
Expand Down Expand Up @@ -141,20 +148,20 @@ private void updateNonNullAPsForElement(
}

private boolean optionalIsPresentCall(Symbol.MethodSymbol symbol, Types types) {
Preconditions.checkNotNull(optionalType);
// noinspection ConstantConditions
return optionalType.isPresent()
&& symbol.getSimpleName().toString().equals("isPresent")
&& symbol.getParameters().length() == 0
&& types.isSubtype(symbol.owner.type, optionalType.get());
for (Type optionalType : optionalTypes) {
if (symbol.getSimpleName().toString().equals("isPresent")
&& symbol.getParameters().length() == 0
&& types.isSubtype(symbol.owner.type, optionalType)) return true;
}
return false;
}

private boolean optionalIsGetCall(Symbol.MethodSymbol symbol, Types types) {
Preconditions.checkNotNull(optionalType);
// noinspection ConstantConditions
return optionalType.isPresent()
&& symbol.getSimpleName().toString().equals("get")
&& symbol.getParameters().length() == 0
&& types.isSubtype(symbol.owner.type, optionalType.get());
for (Type optionalType : optionalTypes) {
if (symbol.getSimpleName().toString().equals("get")
&& symbol.getParameters().length() == 0
&& types.isSubtype(symbol.owner.type, optionalType)) return true;
}
return false;
}
}
Expand Up @@ -203,7 +203,7 @@ class RxNullabilityPropagator extends BaseNoOpHandler {
private final Map<ReturnTree, Tree> returnToEnclosingMethodOrLambda =
new LinkedHashMap<ReturnTree, Tree>();

// Similar to above, but mapping espression-bodies to their enclosing lambdas
// Similar to above, but mapping expression-bodies to their enclosing lambdas
private final Map<ExpressionTree, LambdaExpressionTree> expressionBodyToFilterLambda =
new LinkedHashMap<ExpressionTree, LambdaExpressionTree>();

Expand Down
160 changes: 160 additions & 0 deletions nullaway/src/test/java/com/uber/nullaway/NullAwayTest.java
Expand Up @@ -1233,6 +1233,166 @@ public void OptionalEmptinessHandlerTest() {
.doTest();
}

@Test
public void OptionalEmptinessHandlerWithSingleCustomPathTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
"-XepOpt:NullAway:CheckOptionalEmptiness=true",
"-XepOpt:NullAway:CheckOptionalEmptinessCustomClasses=com.google.common.base.Optional"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add one more (smaller) test, that checks that java.util.Optional is still recognized when this argument is set, as per our discussion. Tests are a good way to document that sort of design edge-case, after all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done already 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(True, I missed this one, my bad 😅 )

.addSourceLines(
"TestNegative.java",
"package com.uber;",
"import com.google.common.base.Optional;",
"import javax.annotation.Nullable;",
"import com.google.common.base.Function;",
"public class TestNegative {",
" void foo() {",
" Optional<Object> a = Optional.absent();",
" // no error since a.isPresent() is called",
" if(a.isPresent()){",
" a.get().toString();",
" }",
" }",
" public void lambdaConsumer(Function a){",
" return;",
" }",
" void bar() {",
" Optional<Object> b = Optional.absent();",
" if(b.isPresent()){",
" lambdaConsumer(v -> b.get().toString());",
" }",
" }",
"}")
.addSourceLines(
"TestPositive.java",
"package com.uber;",
"import java.util.Optional;",
"import javax.annotation.Nullable;",
"import com.google.common.base.Function;",
"public class TestPositive {",
" void foo() {",
" Optional<Object> a = Optional.empty();",
" // BUG: Diagnostic contains: Optional a can be empty",
" a.get().toString();",
" }",
" public void lambdaConsumer(Function a){",
" return;",
" }",
" void bar() {",
" Optional<Object> b = Optional.empty();",
" // BUG: Diagnostic contains: Optional b can be empty",
" lambdaConsumer(v -> b.get().toString());",
" }",
"}")
.addSourceLines(
"TestPositive2.java",
"package com.uber;",
"import com.google.common.base.Optional;",
"import javax.annotation.Nullable;",
"import com.google.common.base.Function;",
"public class TestPositive2 {",
" void foo() {",
" Optional<Object> a = Optional.absent();",
" // BUG: Diagnostic contains: Optional a can be empty",
" a.get().toString();",
" }",
" public void lambdaConsumer(Function a){",
" return;",
" }",
" void bar() {",
" Optional<Object> b = Optional.absent();",
" // BUG: Diagnostic contains: Optional b can be empty",
" lambdaConsumer(v -> b.get().toString());",
" }",
"}")
.doTest();
}

@Test
public void OptionalEmptinessHandlerWithTwoCustomPathsTest() {
compilationHelper
.setArgs(
Arrays.asList(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
"-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
"-XepOpt:NullAway:CheckOptionalEmptiness=true",
"-XepOpt:NullAway:CheckOptionalEmptinessCustomClasses=does.not.matter.Optional,com.google.common.base.Optional"))
.addSourceLines(
"TestNegative.java",
"package com.uber;",
"import com.google.common.base.Optional;",
"import javax.annotation.Nullable;",
"import com.google.common.base.Function;",
"public class TestNegative {",
" void foo() {",
" Optional<Object> a = Optional.absent();",
" // no error since a.isPresent() is called",
" if(a.isPresent()){",
" a.get().toString();",
" }",
" }",
" public void lambdaConsumer(Function a){",
" return;",
" }",
" void bar() {",
" Optional<Object> b = Optional.absent();",
" if(b.isPresent()){",
" lambdaConsumer(v -> b.get().toString());",
" }",
" }",
"}")
.addSourceLines(
"TestPositive.java",
"package com.uber;",
"import java.util.Optional;",
"import javax.annotation.Nullable;",
"import com.google.common.base.Function;",
"public class TestPositive {",
" void foo() {",
" Optional<Object> a = Optional.empty();",
" // BUG: Diagnostic contains: Optional a can be empty",
" a.get().toString();",
" }",
" public void lambdaConsumer(Function a){",
" return;",
" }",
" void bar() {",
" Optional<Object> b = Optional.empty();",
" // BUG: Diagnostic contains: Optional b can be empty",
" lambdaConsumer(v -> b.get().toString());",
" }",
"}")
.addSourceLines(
"TestPositive2.java",
"package com.uber;",
"import com.google.common.base.Optional;",
"import javax.annotation.Nullable;",
"import com.google.common.base.Function;",
"public class TestPositive2 {",
" void foo() {",
" Optional<Object> a = Optional.absent();",
" // BUG: Diagnostic contains: Optional a can be empty",
" a.get().toString();",
" }",
" public void lambdaConsumer(Function a){",
" return;",
" }",
" void bar() {",
" Optional<Object> b = Optional.absent();",
" // BUG: Diagnostic contains: Optional b can be empty",
" lambdaConsumer(v -> b.get().toString());",
" }",
"}")
.doTest();
}

@Test
public void OptionalEmptinessUncheckedTest() {
compilationHelper
Expand Down