/* * 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()); } }