/*
* 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.common.base.Preconditions.checkState;
import static com.google.errorprone.BugPattern.Category.DAGGER;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.bugpatterns.inject.dagger.DaggerAnnotations.ELEMENTS_INTO_SET_CLASS_NAME;
import static com.google.errorprone.bugpatterns.inject.dagger.DaggerAnnotations.INTO_MAP_CLASS_NAME;
import static com.google.errorprone.bugpatterns.inject.dagger.DaggerAnnotations.INTO_SET_CLASS_NAME;
import static com.google.errorprone.bugpatterns.inject.dagger.DaggerAnnotations.PRODUCES_CLASS_NAME;
import static com.google.errorprone.bugpatterns.inject.dagger.DaggerAnnotations.PROVIDES_CLASS_NAME;
import static com.google.errorprone.bugpatterns.inject.dagger.DaggerAnnotations.isBindingMethod;
import static com.google.errorprone.bugpatterns.inject.dagger.Util.IS_DAGGER_2_MODULE;
import static com.google.errorprone.bugpatterns.inject.dagger.Util.makeConcreteClassAbstract;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.sun.source.tree.Tree.Kind.ASSIGNMENT;
import static com.sun.source.tree.Tree.Kind.RETURN;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Flags.Flag;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCAssign;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCModifiers;
import com.sun.tools.javac.util.Name;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.Modifier;
@BugPattern(
name = "UseBinds",
summary = "@Binds is a more efficient and declarative mechanism for delegating a binding.",
explanation =
"A @Provides or @Produces method that returns its single parameter has long been Dagger's "
+ "only mechanism for delegating a binding. Since the delegation is implemented via a "
+ "user-defined method there is a disproportionate amount of overhead for such a "
+ "conceptually simple operation. @Binds was introduced to provide a declarative way of "
+ "delegating from one binding to another in a way that allows for minimal overhead in "
+ "the implementation. @Binds should always be preferred over @Provides or @Produces for "
+ "delegation.",
category = DAGGER,
severity = SUGGESTION
)
public class UseBinds extends BugChecker implements MethodTreeMatcher {
private static final Matcher<MethodTree> SIMPLE_METHOD =
new Matcher<MethodTree>() {
@Override
public boolean matches(MethodTree t, VisitorState state) {
List<? extends VariableTree> parameters = t.getParameters();
if (parameters.size() != 1) {
return false;
}
final VariableTree onlyParameter = Iterables.getOnlyElement(parameters);
BlockTree body = t.getBody();
if (body == null) {
return false;
}
List<? extends StatementTree> statements = body.getStatements();
if (statements.size() != 1) {
return false;
}
StatementTree onlyStatement = Iterables.getOnlyElement(statements);
if (!onlyStatement.getKind().equals(RETURN)) {
return false;
}
Symbol returnedSymbol = getSymbol(((ReturnTree) onlyStatement).getExpression());
if (returnedSymbol == null) {
return false;
}
return getSymbol(onlyParameter).equals(returnedSymbol);
}
};
private static final Matcher<MethodTree> CAN_BE_A_BINDS_METHOD =
allOf(isBindingMethod(), SIMPLE_METHOD);
@Override
public Description matchMethod(MethodTree method, VisitorState state) {
if (!CAN_BE_A_BINDS_METHOD.matches(method, state)) {
return NO_MATCH;
}
JCClassDecl enclosingClass = ASTHelpers.findEnclosingNode(state.getPath(), JCClassDecl.class);
// Dagger 1 modules don't support @Binds.
if (!IS_DAGGER_2_MODULE.matches(enclosingClass, state)) {
return NO_MATCH;
}
if (enclosingClass.getExtendsClause() != null) {
return fixByDelegating();
}
for (Tree member : enclosingClass.getMembers()) {
if (member.getKind().equals(Tree.Kind.METHOD) && !getSymbol(member).isConstructor()) {
MethodTree siblingMethod = (MethodTree) member;
Set<Modifier> siblingFlags = siblingMethod.getModifiers().getFlags();
if (!(siblingFlags.contains(Modifier.STATIC) || siblingFlags.contains(Modifier.ABSTRACT))
&& !CAN_BE_A_BINDS_METHOD.matches(siblingMethod, state)) {
return fixByDelegating();
}
}
}
return fixByModifyingMethod(state, enclosingClass, method);
}
private Description fixByModifyingMethod(
VisitorState state, JCClassDecl enclosingClass, MethodTree method) {
return describeMatch(
method,
SuggestedFix.builder()
.addImport("dagger.Binds")
.merge(convertMethodToBinds(method, state))
.merge(makeConcreteClassAbstract(enclosingClass, state))
.build());
}
private SuggestedFix.Builder convertMethodToBinds(MethodTree method, VisitorState state) {
SuggestedFix.Builder fix = SuggestedFix.builder();
JCModifiers modifiers = ((JCMethodDecl) method).getModifiers();
ImmutableList.Builder<String> modifierStringsBuilder =
new ImmutableList.Builder<String>().add("@Binds");
for (JCAnnotation annotation : modifiers.annotations) {
Name annotationQualifiedName = getSymbol(annotation).getQualifiedName();
if (annotationQualifiedName.contentEquals(PROVIDES_CLASS_NAME)
|| annotationQualifiedName.contentEquals(PRODUCES_CLASS_NAME)) {
List<JCExpression> arguments = annotation.getArguments();
if (!arguments.isEmpty()) {
JCExpression argument = Iterables.getOnlyElement(arguments);
checkState(argument.getKind().equals(ASSIGNMENT));
JCAssign assignment = (JCAssign) argument;
checkState(getSymbol(assignment.getVariable()).getSimpleName().contentEquals("type"));
String typeName = getSymbol(assignment.getExpression()).getSimpleName().toString();
switch (typeName) {
case "SET":
modifierStringsBuilder.add("@IntoSet");
fix.addImport(INTO_SET_CLASS_NAME);
break;
case "SET_VALUES":
modifierStringsBuilder.add("@ElementsIntoSet");
fix.addImport(ELEMENTS_INTO_SET_CLASS_NAME);
break;
case "MAP":
modifierStringsBuilder.add("@IntoMap");
fix.addImport(INTO_MAP_CLASS_NAME);
break;
default:
throw new AssertionError("Unknown type name: " + typeName);
}
}
} else {
modifierStringsBuilder.add(state.getSourceForNode(annotation));
}
}
EnumSet<Flag> methodFlags = Flags.asFlagSet(modifiers.flags);
methodFlags.remove(Flags.Flag.STATIC);
methodFlags.remove(Flags.Flag.FINAL);
methodFlags.add(Flags.Flag.ABSTRACT);
for (Flag flag : methodFlags) {
modifierStringsBuilder.add(flag.toString());
}
fix.replace(modifiers, Joiner.on(' ').join(modifierStringsBuilder.build()));
fix.replace(method.getBody(), ";");
return fix;
}
private Description fixByDelegating() {
// TODO(gak): add a suggested fix by which we make a nested abstract module that we can include
return NO_MATCH;
}
}