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 1 commit
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 String optionalClassPath;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should probably be an ImmutableSet<String>, like with unannotatedClasses. It should also be @Nullable (for the case where checkOptionalEmptiness is false). We want to support the case of: a) No Optional classes passed, b) One Optional class passed (or taken from the default), c) N Optional classes passed. This field would only support (b)

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 👍


protected boolean assertsEnabled;

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

@Override
public String getOptionalClassPath() {
return optionalClassPath;
}

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

/**
* @return the path for Optional class. In default case the path of {@link java.util.Optional} is
* used.
*/
String getOptionalClassPath();

/**
* @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 @@ -117,6 +117,11 @@ public boolean checkOptionalEmptiness() {
throw new IllegalStateException(error_msg);
}

@Override
public String getOptionalClassPath() {
throw new IllegalStateException(error_msg);
}

@Override
@Nullable
public String getCastToNonNullMethod() {
Expand Down
Expand Up @@ -56,6 +56,7 @@ 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_PACKAGE = EP_FL_NAMESPACE + ":OptionalClassPath";
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 +142,7 @@ 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("");
optionalClassPath = flags.get(FL_OPTIONAL_CLASS_PACKAGE).orElse("java.util.Optional");
if (autofixSuppressionComment.contains("\n")) {
throw new IllegalStateException(
"Invalid -XepOpt" + FL_SUPPRESS_COMMENT + " value. Comment must be single line.");
Expand Down
2 changes: 1 addition & 1 deletion nullaway/src/main/java/com/uber/nullaway/NullAway.java
Expand Up @@ -1109,7 +1109,7 @@ public Description matchClass(ClassTree tree, VisitorState state) {
matchWithinClass = !isExcludedClass(classSymbol, state);
// since we are processing a new top-level class, invalidate any cached
// results for previous classes
handler.onMatchTopLevelClass(this, tree, state, classSymbol);
handler.onMatchTopLevelClass(this, tree, state, classSymbol, config);
getNullnessAnalysis(state).invalidateCaches();
initTree2PrevFieldInit.clear();
class2Entities.clear();
Expand Down
Expand Up @@ -29,6 +29,7 @@
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.NullAway;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
Expand All @@ -54,7 +55,11 @@ public class ApacheThriftIsSetHandler extends BaseNoOpHandler {

@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
NullAway analysis,
ClassTree tree,
VisitorState state,
Symbol.ClassSymbol classSymbol,
Config config) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't pass this here, pass it to the handler's constructor in Handlers.buildDefault(...), that's the convention used by existing handlers.

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 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

Before I go ahead and do a full pass, it looks to me like this remains to be addressed :)

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 now 😅

if (tbaseType == null) {
tbaseType =
Optional.ofNullable(state.getTypeFromString(TBASE_NAME)).map(state.getTypes()::erasure);
Expand Down
Expand Up @@ -34,6 +34,7 @@
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.ErrorMessage;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.dataflow.AccessPath;
Expand All @@ -60,7 +61,11 @@ protected BaseNoOpHandler() {

@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
NullAway analysis,
ClassTree tree,
VisitorState state,
Symbol.ClassSymbol classSymbol,
Config config) {
// NoOp
}

Expand Down
Expand Up @@ -35,6 +35,7 @@
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.ErrorMessage;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.dataflow.AccessPath;
Expand Down Expand Up @@ -62,9 +63,13 @@ class CompositeHandler implements Handler {

@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
NullAway analysis,
ClassTree tree,
VisitorState state,
Symbol.ClassSymbol classSymbol,
Config config) {
for (Handler h : handlers) {
h.onMatchTopLevelClass(analysis, tree, state, classSymbol);
h.onMatchTopLevelClass(analysis, tree, state, classSymbol, config);
}
}

Expand Down
Expand Up @@ -30,6 +30,7 @@
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.ErrorMessage;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
Expand Down Expand Up @@ -83,7 +84,11 @@ public class ContractHandler extends BaseNoOpHandler {

@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
NullAway analysis,
ClassTree tree,
VisitorState state,
Symbol.ClassSymbol classSymbol,
Config config) {
this.analysis = analysis;
this.state = state;
}
Expand Down
Expand Up @@ -34,6 +34,7 @@
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.ErrorMessage;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
Expand Down Expand Up @@ -63,9 +64,14 @@ public interface Handler {
* @param tree The AST node for the class being matched.
* @param state The current visitor state.
* @param classSymbol The class symbol for the class being matched.
* @param config The nullaway config.
*/
void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol);
NullAway analysis,
ClassTree tree,
VisitorState state,
Symbol.ClassSymbol classSymbol,
Config config);

/**
* Called when NullAway first matches a particular method node.
Expand Down
Expand Up @@ -31,6 +31,7 @@
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;
Expand All @@ -49,8 +50,6 @@
*/
public class OptionalEmptinessHandler extends BaseNoOpHandler {

private static String OPTIONAL_PATH = "java.util.Optional";

@Nullable private Optional<Type> optionalType;

@Override
Expand All @@ -65,10 +64,14 @@ && optionalIsGetCall((Symbol.MethodSymbol) ASTHelpers.getSymbol(expr), state.get

@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
NullAway analysis,
ClassTree tree,
VisitorState state,
Symbol.ClassSymbol classSymbol,
Config config) {
if (optionalType == null) {
optionalType =
Optional.ofNullable(state.getTypeFromString(OPTIONAL_PATH))
Optional.ofNullable(state.getTypeFromString(config.getOptionalClassPath()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Must be generalized to deal with multiple Optional classes.

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 👍

.map(state.getTypes()::erasure);
}
}
Expand Down
Expand Up @@ -45,6 +45,7 @@
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import com.uber.nullaway.Config;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.NullabilityUtil;
import com.uber.nullaway.Nullness;
Expand Down Expand Up @@ -203,7 +204,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 All @@ -213,7 +214,11 @@ class RxNullabilityPropagator extends BaseNoOpHandler {

@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
NullAway analysis,
ClassTree tree,
VisitorState state,
Symbol.ClassSymbol classSymbol,
Config config) {
// Clear compilation unit specific state
this.filterMethodOrLambdaSet.clear();
this.observableOuterCallInChain.clear();
Expand Down
59 changes: 59 additions & 0 deletions nullaway/src/test/java/com/uber/nullaway/NullAwayTest.java
Expand Up @@ -1233,6 +1233,65 @@ public void OptionalEmptinessHandlerTest() {
.doTest();
}

@Test
public void OptionalEmptinessHandlerWithCustomPathTest() {
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:OptionalClassPath=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.

👍 Would suggest also:

  1. Testing for "-XepOpt:NullAway:OptionalClassPath=\"\"" (I'd say this should just raise a config error if XepOpt:NullAway:CheckOptionalEmptiness is set)
  2. Testing for "-XepOpt:NullAway:OptionalClassPath=java.utils.Optional,com.google.common.base.Optional"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we not have java.utils.Optional by default in the set?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The way I would do it, if possible:

  1. No option at all passed in the command line => set is (java.utils.Optional)
  2. Explicit option passed, empty or unparsable => error
  3. Explicit option passed, parsable non-empty set => the classes in that set, and nothing else

This allows people using this handler for their own custom Optional without enabling it for java.utils.Optional.

Then again, not sure if it's more consistent with how we do other stuff to just have java.utils.Optional always on (when CheckOptionalEmptiness is set), then rename this option to something like CustomOptionalClasses. Maybe on second thought, I am liking this later way of doing it better.

@msridhar , thoughts ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should always check java.util.Optional and this option should add additional aliases for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay then. So first two cases as Lazaro described and and in case 3. Explicit option passed, parsable non-empty set => the classes in that set + java.utils.Optional.
That works @lazaroclapp?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure if we really need the unparsable check. Since we do not have it for other configs. Also I'm not sure how to define it exactly. So let me know if you think it should be necessary.
@msridhar @lazaroclapp

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think given what Manu is saying, then there is just two cases:

  1. No option at all passed in the command line => set is (java.utils.Optional)
  2. CustomOptionalClasses=a.b.c.A1;a.b.c.A2 => set is (java.utils.Optional,a.b.c.A1,a.b.c.A2)

Still worth to check that it works with zero (no CustomOptionalClasses option), one and two classes being passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added test for zero, one and two custom paths case.

.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 com.google.common.base.Optional;",
"import javax.annotation.Nullable;",
"import com.google.common.base.Function;",
"public class TestPositive {",
" 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