/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.errorprone.bugpatterns.inject.dagger;
import static com.google.errorprone.BugPattern.Category.DAGGER;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.ReturnTreeMatcher;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
/**
* Bug checker for null-returning methods annotated with {@code @Provides} but not
* {@code @Nullable}.
*/
@BugPattern(
name = "DaggerProvidesNull",
summary = "Dagger @Provides methods may not return null unless annotated with @Nullable",
explanation =
"Dagger `@Provides` methods may not return null unless annotated with `@Nullable`. "
+ "Such a method will cause a `NullPointerException` at runtime if the `return null` "
+ "path is ever taken.\n\n"
+ "If you believe the `return null` path can never be taken, please throw a "
+ "`RuntimeException` instead. Otherwise, please annotate the method with `@Nullable`.",
category = DAGGER,
severity = ERROR
)
public class ProvidesNull extends BugChecker implements ReturnTreeMatcher {
/**
* Matches explicit "return null" statements in methods annotated with {@code @Provides} but not
* {@code @Nullable}. Suggests either annotating the method with {@code @Nullable} or throwing a
* {@link RuntimeException} instead.
*/
// TODO(eaftan): Use nullness dataflow analysis when it's ready
@Override
public Description matchReturn(ReturnTree returnTree, VisitorState state) {
ExpressionTree returnExpression = returnTree.getExpression();
if (returnExpression == null || returnExpression.getKind() != Kind.NULL_LITERAL) {
return Description.NO_MATCH;
}
TreePath path = state.getPath();
MethodTree enclosingMethod = null;
while (true) {
if (path == null || path.getLeaf() instanceof LambdaExpressionTree) {
return Description.NO_MATCH;
} else if (path.getLeaf() instanceof MethodTree) {
enclosingMethod = (MethodTree) path.getLeaf();
break;
} else {
path = path.getParentPath();
}
}
MethodSymbol enclosingMethodSym = ASTHelpers.getSymbol(enclosingMethod);
if (enclosingMethodSym == null) {
return Description.NO_MATCH;
}
if (!ASTHelpers.hasAnnotation(enclosingMethodSym, "dagger.Provides", state)
|| ASTHelpers.hasDirectAnnotationWithSimpleName(enclosingMethodSym, "Nullable")) {
return Description.NO_MATCH;
}
Fix addNullableFix =
SuggestedFix.builder()
.prefixWith(enclosingMethod, "@Nullable\n")
.addImport("javax.annotation.Nullable")
.build();
CatchTree enclosingCatch = ASTHelpers.findEnclosingNode(state.getPath(), CatchTree.class);
if (enclosingCatch == null) {
// If not in a catch block, suggest adding @Nullable first, then throwing an exception.
Fix throwRuntimeExceptionFix =
SuggestedFix.replace(returnTree, "throw new RuntimeException();");
return buildDescription(returnTree)
.addFix(addNullableFix)
.addFix(throwRuntimeExceptionFix)
.build();
} else {
// If in a catch block, suggest throwing an exception first, then adding @Nullable.
String replacement =
String.format("throw new RuntimeException(%s);", enclosingCatch.getParameter().getName());
Fix throwRuntimeExceptionFix = SuggestedFix.replace(returnTree, replacement);
return buildDescription(returnTree)
.addFix(throwRuntimeExceptionFix)
.addFix(addNullableFix)
.build();
}
}
}