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

Added Optional emptiness handler #278

Merged
merged 6 commits into from Mar 6, 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: 6 additions & 1 deletion nullaway/src/main/java/com/uber/nullaway/Config.java
Expand Up @@ -107,7 +107,12 @@ public interface Config {
*/
boolean acknowledgeRestrictiveAnnotations();

/** @return true if Optional Emptiness Handler is to be used. */
/**
* @return true if Optional Emptiness Handler is to be used. When Optional.get() method is called
* on an empty optional, program will crash with an exception. This handler warns on possible
* cases where Optional.get() call is made on an empty optional. Nullaway determines if an
* optional is non-empty based on Optional.isPresent() call.
*/
boolean checkOptionalEmptiness();

/**
Expand Down
42 changes: 27 additions & 15 deletions nullaway/src/main/java/com/uber/nullaway/NullAway.java
Expand Up @@ -365,7 +365,8 @@ private void updateEnvironmentMapping(Tree tree, VisitorState state) {
// from the nested scope, so the program point doesn't matter
// 2. we keep info on all locals rather than just effectively final ones for simplicity
EnclosingEnvironmentNullness.instance(state.context)
.addEnvironmentMapping(tree, analysis.getLocalVarInfoBefore(state.getPath(), state));
.addEnvironmentMapping(
tree, analysis.getLocalVarInfoBefore(state.getPath(), state, handler));
}

private Symbol.MethodSymbol getSymbolOfSuperConstructor(
Expand Down Expand Up @@ -1932,21 +1933,16 @@ private Description matchDereference(
return Description.NO_MATCH;
}
if (mayBeNullExpr(state, baseExpression)) {
String message;
if (handler.checkIfOptionalGetCall(baseExpression, state)) {
final int exprStringSize = baseExpression.toString().length();
// Name of the optional is extracted from the expression
message =
"Optional "
+ baseExpression.toString().substring(0, exprStringSize - 6)
+ " can be empty, dereferenced get() call on it";
} else {
message = "dereferenced expression " + baseExpression.toString() + " is @Nullable";
}
final String message =
"dereferenced expression " + baseExpression.toString() + " is @Nullable";
ErrorMessage errorMessage = new ErrorMessage(MessageTypes.DEREFERENCE_NULLABLE, message);

handler.getErrorMessage(baseExpression, state, errorMessage);
Copy link
Collaborator

Choose a reason for hiding this comment

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

If this is changing the error message text, rather than retrieving a new copy, it shouldn't be called get[...]. How about Handler.onPrepareErrorMessage?

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 👍


return createErrorDescriptionForNullAssignment(
MessageTypes.DEREFERENCE_NULLABLE,
errorMessage.messageType,
derefExpression,
message,
errorMessage.message,
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should probably change all the createErrorDescription[...] methods to just take ErrorMessage rather than the separate components. But let's leave that for a follow up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, definitely.

baseExpression,
state.getPath());
}
Expand Down Expand Up @@ -2346,7 +2342,23 @@ public enum MessageTypes {
UNBOX_NULLABLE,
NONNULL_FIELD_READ_BEFORE_INIT,
ANNOTATION_VALUE_INVALID,
CAST_TO_NONNULL_ARG_NONNULL;
CAST_TO_NONNULL_ARG_NONNULL,
GET_ON_EMPTY_OPTIONAL;
}

public static class ErrorMessage {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Like this. But, can we extract this and MessageTypes to another class/file if we are using them from the handlers. NullAway.java is just massive. Maybe this should be a top level ErrorMessage class containing the ErrorMessage.MessageTypes enum?

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

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

MessageTypes messageType;
String message;

ErrorMessage(MessageTypes messageType, String message) {
this.messageType = messageType;
this.message = message;
}

public void updateErrorMessage(MessageTypes messageType, String message) {
this.messageType = messageType;
this.message = message;
}
}

@AutoValue
Expand Down
Expand Up @@ -311,7 +311,7 @@ public static boolean isMapPut(Symbol.MethodSymbol symbol, Types types) {
* root of an access path; either a variable {@link javax.lang.model.element.Element} or <code>
* this</code> (enclosing method receiver)
*/
static final class Root {
public static final class Root {

/** does this represent the receiver? */
private final boolean isMethodReceiver;
Expand Down
Expand Up @@ -21,15 +21,12 @@
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
import com.sun.source.util.TreePath;
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.Nullness;
import com.uber.nullaway.handlers.Handler;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -160,7 +157,7 @@ public Set<Element> getNonnullStaticFieldsBefore(TreePath path, Context context)
* @param state visitor state
* @return nullness info for local variables just before the node
*/
public NullnessStore getLocalVarInfoBefore(TreePath path, VisitorState state) {
public NullnessStore getLocalVarInfoBefore(TreePath path, VisitorState state, Handler handler) {
NullnessStore store = dataFlow.resultBefore(path, state.context, nullnessPropagation);
if (store == null) {
return NullnessStore.empty();
Expand All @@ -176,30 +173,14 @@ public NullnessStore getLocalVarInfoBefore(TreePath path, VisitorState state) {
}
}

// a filter for Optional get() call
if (ap.getElements().size() == 1) {
AccessPath.Root root = ap.getRoot();
if (!root.isReceiver() && (ap.getElements().get(0) instanceof Symbol.MethodSymbol)) {
final Element e = root.getVarElement();
final Symbol.MethodSymbol g = (Symbol.MethodSymbol) ap.getElements().get(0);
final Optional<Type> tbaseType =
Optional.ofNullable(state.getTypeFromString(OPTIONAL_PATH))
.map(state.getTypes()::erasure);
return e.getKind().equals(ElementKind.LOCAL_VARIABLE)
&& g.getSimpleName().toString().equals("get")
&& g.getParameters().length() == 0
&& tbaseType.isPresent()
&& state.getTypes().isSubtype(g.owner.type, tbaseType.get());
}
}
return false;
return handler.filterApForLocalVarInfoBefore(ap, state);
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure the name is super clear. But can't also think of a better one. @msridhar ? (This definitely is a better design, though :) )

p.s. See #278 (comment) for further discussion.

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

});
}

/**
* @param path tree path of static method, or initializer block
* @param context Javac context
* @return fields guaranteed to be nonnull at exit of static method (or initializer block)
* @return fields guaranteed to be nonnull at exit of static method (or initialize r block)
Copy link
Collaborator

Choose a reason for hiding this comment

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

typo?

*/
public Set<Element> getNonnullStaticFieldsAtExit(TreePath path, Context context) {
NullnessStore nullnessResult = dataFlow.finalResult(path, context, nullnessPropagation);
Expand Down
Expand Up @@ -35,6 +35,7 @@
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import com.uber.nullaway.dataflow.NullnessStore;
import java.util.List;
Expand Down Expand Up @@ -168,7 +169,13 @@ public void onDataflowVisitLambdaResultExpression(
}

@Override
public boolean checkIfOptionalGetCall(ExpressionTree expr, VisitorState state) {
public void getErrorMessage(
ExpressionTree expr, VisitorState state, NullAway.ErrorMessage errorMessage) {
// NoOp
}

@Override
public boolean filterApForLocalVarInfoBefore(AccessPath accessPath, VisitorState state) {
return false;
}
}
Expand Up @@ -36,6 +36,7 @@
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import com.uber.nullaway.dataflow.NullnessStore;
import java.util.List;
Expand Down Expand Up @@ -209,11 +210,19 @@ public void onDataflowVisitLambdaResultExpression(
}

@Override
public boolean checkIfOptionalGetCall(ExpressionTree expr, VisitorState state) {
boolean ifOptionalGetCall = false;
public void getErrorMessage(
ExpressionTree expr, VisitorState state, NullAway.ErrorMessage errorMessage) {
for (Handler h : handlers) {
ifOptionalGetCall |= h.checkIfOptionalGetCall(expr, state);
h.getErrorMessage(expr, state, errorMessage);
}
return ifOptionalGetCall;
}

@Override
public boolean filterApForLocalVarInfoBefore(AccessPath accessPath, VisitorState state) {
boolean shouldFilter = false;
for (Handler h : handlers) {
shouldFilter |= h.filterApForLocalVarInfoBefore(accessPath, state);
}
return shouldFilter;
}
}
17 changes: 15 additions & 2 deletions nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java
Expand Up @@ -36,6 +36,7 @@
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import com.uber.nullaway.dataflow.NullnessStore;
import java.util.List;
Expand Down Expand Up @@ -275,9 +276,21 @@ void onDataflowVisitLambdaResultExpression(
*
* @param expr The AST node for the expression being matched.
* @param state The current visitor state.
* @return If the method call in the expression is to Optional.get().
* @param errorMessage error message string and type of the error wrapped in {@link
* com.uber.nullaway.NullAway.ErrorMessage}.
*/
boolean checkIfOptionalGetCall(ExpressionTree expr, VisitorState state);
void getErrorMessage(ExpressionTree expr, VisitorState state, NullAway.ErrorMessage errorMessage);

/**
* Called when the store access paths are filtered for local variable information before an
* expression.
*
* @param accessPath The access path that needs to be checked if filtered.
* @param state The current visitor state.
* @return true if the accesspath should be filtered to be included in the local variable
Copy link
Collaborator

Choose a reason for hiding this comment

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

"should be filtered to be included" doesn't easily parse in my mind. Are we excluding or including? (e.g. is this a whitelist or a blacklist?).

Copy link
Collaborator

Choose a reason for hiding this comment

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

After reading the rest of the code more carefully, I'd go with "@return true if the nullability information for this accesspath should be treated as part of the surrounding context when processing a lambda expression or anonymous class declaration"

I'd also rename getLocalVarInfoBefore to e.g. getNullnessInfoBeforeNewContext, to more accurately reflect the fact that it preserves info for APs other than locals. But let's see if that makes sense to @msridhar as well before going ahead with that renaming for a core API method :)

If we do rename that, we can have this method be Handler.includeApInfoInSavedContext, or something like that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with @lazaroclapp's comments and suggested names 😄

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

* information.
*/
boolean filterApForLocalVarInfoBefore(AccessPath accessPath, VisitorState state);

/**
* A three value enum for handlers implementing onDataflowVisitMethodInvocation to communicate
Expand Down
Expand Up @@ -100,10 +100,31 @@ public NullnessHint onDataflowVisitMethodInvocation(
}

@Override
public boolean checkIfOptionalGetCall(ExpressionTree expr, VisitorState state) {
public void getErrorMessage(
ExpressionTree expr, VisitorState state, NullAway.ErrorMessage errorMessage) {
if (expr.getKind() == Tree.Kind.METHOD_INVOCATION
&& optionalIsGetCall((Symbol.MethodSymbol) ASTHelpers.getSymbol(expr), state.getTypes())) {
return true;
final int exprStringSize = expr.toString().length();
// Name of the optional is extracted from the expression
final String message =
"Optional "
+ expr.toString().substring(0, exprStringSize - 6)
+ " can be empty, dereferenced get() call on it";
errorMessage.updateErrorMessage(NullAway.MessageTypes.GET_ON_EMPTY_OPTIONAL, message);
}
}

@Override
public boolean filterApForLocalVarInfoBefore(AccessPath accessPath, VisitorState state) {

if (accessPath.getElements().size() == 1) {
AccessPath.Root root = accessPath.getRoot();
if (!root.isReceiver() && (accessPath.getElements().get(0) instanceof Symbol.MethodSymbol)) {
final Element e = root.getVarElement();
final Symbol.MethodSymbol g = (Symbol.MethodSymbol) accessPath.getElements().get(0);
return e.getKind().equals(ElementKind.LOCAL_VARIABLE)
&& optionalIsGetCall(g, state.getTypes());
}
}
return false;
}
Expand Down