/*
* 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.SUGGESTION;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.allOf;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.hasAnnotation;
import static com.google.errorprone.matchers.Matchers.hasArgumentWithValue;
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.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
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.matchers.Matchers;
import com.google.errorprone.matchers.method.MethodMatchers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ExpressionTree;
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.tree.JCTree.JCAnnotation;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
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.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/** @author gak@google.com (Gregory Kick) */
@BugPattern(
name = "EmptySetMultibindingContributions",
summary =
"@Multibinds is a more efficient and declarative mechanism for ensuring that a set"
+ " multibinding is present in the graph.",
explanation = "",
category = DAGGER,
severity = SUGGESTION
)
public final class EmptySetMultibindingContributions extends BugChecker
implements MethodTreeMatcher {
private static final Matcher<AnnotationTree> HAS_DAGGER_ONE_MODULE_ARGUMENT =
anyOf(
hasArgumentWithValue("injects", Matchers.<ExpressionTree>anything()),
hasArgumentWithValue("staticInjections", Matchers.<ExpressionTree>anything()),
hasArgumentWithValue("overrides", Matchers.<ExpressionTree>anything()),
hasArgumentWithValue("addsTo", Matchers.<ExpressionTree>anything()),
hasArgumentWithValue("complete", Matchers.<ExpressionTree>anything()),
hasArgumentWithValue("library", Matchers.<ExpressionTree>anything()));
/** We're just not going to worry about Collections.EMPTY_SET. */
private static final Matcher<ExpressionTree> COLLECTIONS_EMPTY_SET =
MethodMatchers.staticMethod().onClass(Collections.class.getCanonicalName()).named("emptySet");
private static final Matcher<ExpressionTree> IMMUTABLE_SETS_OF =
MethodMatchers.staticMethod()
.onClassAny(
ImmutableSet.class.getCanonicalName(), ImmutableSortedSet.class.getCanonicalName())
.named("of")
.withParameters();
private static final Matcher<ExpressionTree> SET_CONSTRUCTORS =
anyOf(
noArgSetConstructor(HashSet.class),
noArgSetConstructor(LinkedHashSet.class),
noArgSetConstructor(TreeSet.class));
@SuppressWarnings("rawtypes")
private static Matcher<ExpressionTree> noArgSetConstructor(Class<? extends Set> setClass) {
return MethodMatchers.constructor().forClass(setClass.getCanonicalName()).withParameters();
}
private static final Matcher<ExpressionTree> SET_FACTORY_METHODS =
anyOf(
setFactory("newHashSet"),
setFactory("newLinkedHashSet"),
setFactory("newConcurrentHashSet"));
private static Matcher<ExpressionTree> setFactory(String factoryName) {
return MethodMatchers.staticMethod()
.onClass(Sets.class.getCanonicalName())
.named(factoryName)
.withParameters();
}
private static final Matcher<ExpressionTree> ENUM_SET_NONE_OF =
MethodMatchers.staticMethod().onClass(EnumSet.class.getCanonicalName()).named("noneOf");
private static final Matcher<ExpressionTree> EMPTY_SET =
anyOf(
COLLECTIONS_EMPTY_SET,
IMMUTABLE_SETS_OF,
SET_CONSTRUCTORS,
SET_FACTORY_METHODS,
ENUM_SET_NONE_OF);
private static final Matcher<MethodTree> RETURNS_EMPTY_SET =
new Matcher<MethodTree>() {
@Override
public boolean matches(MethodTree method, VisitorState state) {
List<? extends VariableTree> parameters = method.getParameters();
if (!parameters.isEmpty()) {
return false;
}
BlockTree body = method.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;
}
return EMPTY_SET.matches(((ReturnTree) onlyStatement).getExpression(), state);
}
};
private static final Matcher<Tree> ANNOTATED_WITH_PRODUCES_OR_PROVIDES =
anyOf(hasAnnotation("dagger.Provides"), hasAnnotation("dagger.producers.Produces"));
private static final Matcher<MethodTree> CAN_BE_A_MULTIBINDS_METHOD =
allOf(
ANNOTATED_WITH_PRODUCES_OR_PROVIDES,
hasAnnotation("dagger.multibindings.ElementsIntoSet"),
RETURNS_EMPTY_SET);
@Override
public Description matchMethod(MethodTree method, VisitorState state) {
if (!CAN_BE_A_MULTIBINDS_METHOD.matches(method, state)) {
return NO_MATCH;
}
JCClassDecl enclosingClass = ASTHelpers.findEnclosingNode(state.getPath(), JCClassDecl.class);
// Check to see if this is in a Dagger 1 module b/c it doesn't support @Multibinds
for (JCAnnotation annotation : enclosingClass.getModifiers().getAnnotations()) {
if (ASTHelpers.getSymbol(annotation.getAnnotationType())
.getQualifiedName()
.contentEquals("dagger.Module")
&& HAS_DAGGER_ONE_MODULE_ARGUMENT.matches(annotation, state)) {
return NO_MATCH;
}
}
return fixByModifyingMethod(state, enclosingClass, method);
}
private Description fixByModifyingMethod(
VisitorState state, JCClassDecl enclosingClass, MethodTree method) {
JCModifiers methodModifiers = ((JCMethodDecl) method).getModifiers();
String replacementModifiersString = createReplacementMethodModifiers(state, methodModifiers);
JCModifiers enclosingClassModifiers = enclosingClass.getModifiers();
String enclosingClassReplacementModifiersString =
createReplacementClassModifiers(state, enclosingClassModifiers);
SuggestedFix.Builder fixBuilder =
SuggestedFix.builder()
.addImport("dagger.multibindings.Multibinds")
.replace(methodModifiers, replacementModifiersString)
.replace(method.getBody(), ";");
fixBuilder =
(enclosingClassModifiers.pos == -1)
? fixBuilder.prefixWith(enclosingClass, enclosingClassReplacementModifiersString)
: fixBuilder.replace(enclosingClassModifiers, enclosingClassReplacementModifiersString);
return describeMatch(method, fixBuilder.build());
}
private Description fixByDelegating() {
// TODO(gak): add a suggested fix by which we make a nested abstract module that we can include
return NO_MATCH;
}
private String createReplacementMethodModifiers(VisitorState state, JCModifiers modifiers) {
ImmutableList.Builder<String> modifierStringsBuilder =
new ImmutableList.Builder<String>().add("@Multibinds");
for (JCAnnotation annotation : modifiers.annotations) {
Name annotationQualifiedName = ASTHelpers.getSymbol(annotation).getQualifiedName();
if (!(annotationQualifiedName.contentEquals("dagger.Provides")
|| annotationQualifiedName.contentEquals("dagger.producers.Produces")
|| annotationQualifiedName.contentEquals("dagger.multibindings.ElementsIntoSet"))) {
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());
}
return Joiner.on(' ').join(modifierStringsBuilder.build());
}
private String createReplacementClassModifiers(
VisitorState state, JCModifiers enclosingClassModifiers) {
ImmutableList.Builder<String> classModifierStringsBuilder = new ImmutableList.Builder<String>();
for (JCAnnotation annotation : enclosingClassModifiers.annotations) {
classModifierStringsBuilder.add(state.getSourceForNode(annotation));
}
EnumSet<Flag> classFlags = Flags.asFlagSet(enclosingClassModifiers.flags);
classFlags.remove(Flags.Flag.FINAL);
classFlags.add(Flags.Flag.ABSTRACT);
for (Flag flag : classFlags) {
classModifierStringsBuilder.add(flag.toString());
}
return Joiner.on(' ').join(classModifierStringsBuilder.build());
}
}