blob: 8d92c6e1bd8003852a7f0ffa71d6108364cbe9d9 [file] [log] [blame]
/*
* Copyright (C) 2018 The Dagger Authors.
*
* 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 dagger.internal.codegen.bindinggraphvalidation;
import static com.google.common.base.Preconditions.checkArgument;
import static dagger.internal.codegen.base.Formatter.INDENT;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static dagger.internal.codegen.model.BindingKind.MULTIBOUND_MAP;
import static dagger.internal.codegen.xprocessing.XAnnotations.getClassName;
import static javax.tools.Diagnostic.Kind.ERROR;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimaps;
import com.squareup.javapoet.ClassName;
import dagger.internal.codegen.base.MapType;
import dagger.internal.codegen.binding.BindingNode;
import dagger.internal.codegen.binding.ContributionBinding;
import dagger.internal.codegen.binding.Declaration;
import dagger.internal.codegen.binding.DeclarationFormatter;
import dagger.internal.codegen.binding.KeyFactory;
import dagger.internal.codegen.model.Binding;
import dagger.internal.codegen.model.BindingGraph;
import dagger.internal.codegen.model.DiagnosticReporter;
import dagger.internal.codegen.model.Key;
import dagger.internal.codegen.validation.ValidationBindingGraphPlugin;
import dagger.internal.codegen.xprocessing.XAnnotations;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
/**
* Reports an error for any map binding with either more than one contribution with the same map key
* or contributions with inconsistent map key annotation types.
*/
final class MapMultibindingValidator extends ValidationBindingGraphPlugin {
private final DeclarationFormatter declarationFormatter;
private final KeyFactory keyFactory;
@Inject
MapMultibindingValidator(
DeclarationFormatter declarationFormatter, KeyFactory keyFactory) {
this.declarationFormatter = declarationFormatter;
this.keyFactory = keyFactory;
}
@Override
public String pluginName() {
return "Dagger/MapKeys";
}
@Override
public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) {
mapMultibindings(bindingGraph)
.forEach(
binding -> {
ImmutableSet<ContributionBinding> contributions =
mapBindingContributions(binding, bindingGraph);
checkForDuplicateMapKeys(binding, contributions, diagnosticReporter);
checkForInconsistentMapKeyAnnotationTypes(binding, contributions, diagnosticReporter);
});
}
/**
* Returns the map multibindings in the binding graph. If a graph contains bindings for more than
* one of the following for the same {@code K} and {@code V}, then only the first one found will
* be returned so we don't report the same map contribution problem more than once.
*
* <ol>
* <li>{@code Map<K, V>}
* <li>{@code Map<K, Provider<V>>}
* <li>{@code Map<K, Producer<V>>}
* </ol>
*/
private ImmutableSet<Binding> mapMultibindings(BindingGraph bindingGraph) {
Set<Key> visitedKeys = new HashSet<>();
return bindingGraph.bindings().stream()
.filter(binding -> binding.kind().equals(MULTIBOUND_MAP))
// Sort by the order of the value in the RequestKind:
// (Map<K, V>, then Map<K, Provider<V>>, then Map<K, Producer<V>>).
.sorted(Comparator.comparing(binding -> MapType.from(binding.key()).valueRequestKind()))
// Only take the first binding (post sorting) per unwrapped key.
.filter(binding -> visitedKeys.add(unwrappedKey(binding)))
.collect(toImmutableSet());
}
private Key unwrappedKey(Binding binding) {
return keyFactory.unwrapMapValueType(binding.key());
}
private ImmutableSet<ContributionBinding> mapBindingContributions(
Binding binding, BindingGraph bindingGraph) {
checkArgument(binding.kind().equals(MULTIBOUND_MAP));
return bindingGraph.requestedBindings(binding).stream()
.map(b -> (BindingNode) b)
.map(b -> (ContributionBinding) b.delegate())
.collect(toImmutableSet());
}
private void checkForDuplicateMapKeys(
Binding multiboundMapBinding,
ImmutableSet<ContributionBinding> contributions,
DiagnosticReporter diagnosticReporter) {
ImmutableSetMultimap<?, ContributionBinding> contributionsByMapKey =
ImmutableSetMultimap.copyOf(
Multimaps.index(
contributions,
// Note: We're wrapping in XAnnotations.equivalence() to get proper equals/hashcode.
binding -> binding.mapKey().map(XAnnotations.equivalence()::wrap)));
for (Set<ContributionBinding> contributionsForOneMapKey :
Multimaps.asMap(contributionsByMapKey).values()) {
if (contributionsForOneMapKey.size() > 1) {
diagnosticReporter.reportBinding(
ERROR,
multiboundMapBinding,
duplicateMapKeyErrorMessage(contributionsForOneMapKey, multiboundMapBinding.key()));
}
}
}
private void checkForInconsistentMapKeyAnnotationTypes(
Binding multiboundMapBinding,
ImmutableSet<ContributionBinding> contributions,
DiagnosticReporter diagnosticReporter) {
ImmutableSetMultimap<ClassName, ContributionBinding> contributionsByMapKeyAnnotationType =
ImmutableSetMultimap.copyOf(
Multimaps.index(contributions, mapBinding -> getClassName(mapBinding.mapKey().get())));
if (contributionsByMapKeyAnnotationType.keySet().size() > 1) {
diagnosticReporter.reportBinding(
ERROR,
multiboundMapBinding,
inconsistentMapKeyAnnotationTypesErrorMessage(
contributionsByMapKeyAnnotationType, multiboundMapBinding.key()));
}
}
private String inconsistentMapKeyAnnotationTypesErrorMessage(
ImmutableSetMultimap<ClassName, ContributionBinding> contributionsByMapKeyAnnotationType,
Key mapBindingKey) {
StringBuilder message =
new StringBuilder(mapBindingKey.toString())
.append(" uses more than one @MapKey annotation type");
Multimaps.asMap(contributionsByMapKeyAnnotationType)
.forEach(
(annotationType, contributions) -> {
message.append('\n').append(INDENT).append(annotationType).append(':');
declarationFormatter.formatIndentedList(message, contributions, 2);
});
return message.toString();
}
private String duplicateMapKeyErrorMessage(
Set<ContributionBinding> contributionsForOneMapKey, Key mapBindingKey) {
StringBuilder message =
new StringBuilder("The same map key is bound more than once for ").append(mapBindingKey);
declarationFormatter.formatIndentedList(
message,
ImmutableList.sortedCopyOf(Declaration.COMPARATOR, contributionsForOneMapKey),
1);
return message.toString();
}
}