/*
* Copyright 2013 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.guice;
import static com.google.errorprone.BugPattern.Category.GUICE;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.matchers.InjectMatchers.ASSISTED_ANNOTATION;
import static com.google.errorprone.matchers.InjectMatchers.ASSISTED_INJECT_ANNOTATION;
import static com.google.errorprone.matchers.InjectMatchers.hasInjectAnnotation;
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.methodHasParameters;
import static com.google.errorprone.matchers.Matchers.methodIsConstructor;
import com.google.auto.value.AutoValue;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
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.matchers.ChildMultiMatcher.MatchType;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.matchers.MultiMatcher;
import com.google.errorprone.matchers.MultiMatcher.MultiMatchResult;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Attribute.Compound;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.lang.model.element.TypeElement;
/** @author sgoldfeder@google.com (Steven Goldfeder) */
@BugPattern(
name = "GuiceAssistedParameters",
summary =
"A constructor cannot have two @Assisted parameters of the same type unless they are "
+ "disambiguated with named @Assisted annotations.",
explanation =
"See https://google.github.io/guice/api-docs/latest/javadoc/com/google/inject/assistedinject/FactoryModuleBuilder.html",
category = GUICE,
severity = ERROR
)
public class AssistedParameters extends BugChecker implements MethodTreeMatcher {
private static final Matcher<MethodTree> IS_CONSTRUCTOR_WITH_INJECT_OR_ASSISTED =
allOf(
methodIsConstructor(),
anyOf(hasInjectAnnotation(), hasAnnotation(ASSISTED_INJECT_ANNOTATION)));
private static final MultiMatcher<MethodTree, VariableTree> ASSISTED_PARAMETER_MATCHER =
methodHasParameters(MatchType.AT_LEAST_ONE, Matchers.hasAnnotation(ASSISTED_ANNOTATION));
private static final Function<VariableTree, String> VALUE_FROM_ASSISTED_ANNOTATION =
new Function<VariableTree, String>() {
@Override
public String apply(VariableTree variableTree) {
for (Compound c : ASTHelpers.getSymbol(variableTree).getAnnotationMirrors()) {
if (((TypeElement) c.getAnnotationType().asElement())
.getQualifiedName()
.contentEquals(ASSISTED_ANNOTATION)) {
// Assisted only has 'value', and value can only contain 1 element.
Collection<Attribute> valueEntries = c.getElementValues().values();
if (!valueEntries.isEmpty()) {
return Iterables.getOnlyElement(valueEntries).getValue().toString();
}
}
}
return "";
}
};
@Override
public final Description matchMethod(MethodTree constructor, final VisitorState state) {
if (!IS_CONSTRUCTOR_WITH_INJECT_OR_ASSISTED.matches(constructor, state)) {
return Description.NO_MATCH;
}
// Gather @Assisted parameters, partition by type
MultiMatchResult<VariableTree> assistedParameters =
ASSISTED_PARAMETER_MATCHER.multiMatchResult(constructor, state);
if (!assistedParameters.matches()) {
return Description.NO_MATCH;
}
Multimap<Type, VariableTree> parametersByType =
partitionParametersByType(assistedParameters.matchingNodes(), state);
// If there's more than one parameter with the same type, they could conflict unless their
// @Assisted values are different.
List<ConflictResult> conflicts = new ArrayList<>();
for (Map.Entry<Type, Collection<VariableTree>> typeAndParameters :
parametersByType.asMap().entrySet()) {
Collection<VariableTree> parametersForThisType = typeAndParameters.getValue();
if (parametersForThisType.size() < 2) {
continue;
}
// Gather the @Assisted value from each parameter. If any value is repeated amongst the
// parameters in this type, it's a compile error.
ImmutableListMultimap<String, VariableTree> keyForAssistedVariable =
Multimaps.index(parametersForThisType, VALUE_FROM_ASSISTED_ANNOTATION);
for (Entry<String, List<VariableTree>> assistedValueToParameters :
Multimaps.asMap(keyForAssistedVariable).entrySet()) {
if (assistedValueToParameters.getValue().size() > 1) {
conflicts.add(
ConflictResult.create(
typeAndParameters.getKey(),
assistedValueToParameters.getKey(),
assistedValueToParameters.getValue()));
}
}
}
if (conflicts.isEmpty()) {
return Description.NO_MATCH;
}
return buildDescription(constructor).setMessage(buildErrorMessage(conflicts)).build();
}
private String buildErrorMessage(List<ConflictResult> conflicts) {
StringBuilder sb =
new StringBuilder(
" Assisted parameters of the same type need to have distinct values for the @Assisted"
+ " annotation. There are conflicts between the annotations on this constructor:");
for (ConflictResult conflict : conflicts) {
sb.append("\n").append(conflict.type());
if (!conflict.value().isEmpty()) {
sb.append(", @Assisted(\"").append(conflict.value()).append("\")");
}
sb.append(": ");
List<String> simpleParameterNames =
Lists.transform(
conflict.parameters(),
new Function<VariableTree, String>() {
@Override
public String apply(VariableTree variableTree) {
return variableTree.getName().toString();
}
});
Joiner.on(", ").appendTo(sb, simpleParameterNames);
}
return sb.toString();
}
@AutoValue
abstract static class ConflictResult {
abstract Type type();
abstract String value();
abstract List<VariableTree> parameters();
static ConflictResult create(Type t, String v, List<VariableTree> p) {
return new AutoValue_AssistedParameters_ConflictResult(t, v, p);
}
}
// Since Type doesn't have strong equality semantics, we have to use Types.isSameType to
// determine which parameters are conflicting with each other.
private Multimap<Type, VariableTree> partitionParametersByType(
List<VariableTree> parameters, VisitorState state) {
Types types = state.getTypes();
Multimap<Type, VariableTree> multimap = LinkedListMultimap.create();
variables:
for (VariableTree node : parameters) {
// Normalize Integer => int
Type type = types.unboxedTypeOrType(ASTHelpers.getType(node));
for (Type existingType : multimap.keySet()) {
if (types.isSameType(existingType, type)) {
multimap.put(existingType, node);
continue variables;
}
}
// A new type for the map.
multimap.put(type, node);
}
return multimap;
}
}