/* * 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 com.google.common.collect.Multimaps.filterKeys; import static dagger.internal.codegen.base.Formatter.INDENT; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSetMultimap; import static dagger.model.BindingKind.MULTIBOUND_MAP; import static javax.tools.Diagnostic.Kind.ERROR; import com.google.auto.common.MoreTypes; import com.google.common.base.Equivalence; 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.google.common.collect.SetMultimap; import dagger.internal.codegen.base.MapType; import dagger.internal.codegen.binding.BindingDeclaration; import dagger.internal.codegen.binding.BindingDeclarationFormatter; import dagger.internal.codegen.binding.BindingNode; import dagger.internal.codegen.binding.ContributionBinding; import dagger.internal.codegen.binding.KeyFactory; import dagger.model.BindingGraph; import dagger.model.Key; import dagger.producers.Producer; import dagger.spi.BindingGraphPlugin; import dagger.spi.DiagnosticReporter; import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; import javax.lang.model.type.DeclaredType; /** * 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 implements BindingGraphPlugin { private final BindingDeclarationFormatter bindingDeclarationFormatter; private final KeyFactory keyFactory; @Inject MapMultibindingValidator( BindingDeclarationFormatter bindingDeclarationFormatter, KeyFactory keyFactory) { this.bindingDeclarationFormatter = bindingDeclarationFormatter; this.keyFactory = keyFactory; } @Override public String pluginName() { return "Dagger/MapKeys"; } @Override public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { mapMultibindings(bindingGraph) .forEach( binding -> { ImmutableSet 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. * *
    *
  1. {@code Map} *
  2. {@code Map>} *
  3. {@code Map>} *
*/ private ImmutableSet mapMultibindings(BindingGraph bindingGraph) { ImmutableSetMultimap mapMultibindings = bindingGraph.bindings().stream() .filter(node -> node.kind().equals(MULTIBOUND_MAP)) .collect(toImmutableSetMultimap(dagger.model.Binding::key, node -> node)); // Mutlbindings for Map SetMultimap plainValueMapMultibindings = filterKeys(mapMultibindings, key -> !MapType.from(key).valuesAreFrameworkType()); // Multibindings for Map> where Map isn't in plainValueMapMultibindings SetMultimap providerValueMapMultibindings = filterKeys( mapMultibindings, key -> MapType.from(key).valuesAreTypeOf(Provider.class) && !plainValueMapMultibindings.containsKey(keyFactory.unwrapMapValueType(key))); // Multibindings for Map> where Map isn't in plainValueMapMultibindings and // Map> isn't in providerValueMapMultibindings SetMultimap producerValueMapMultibindings = filterKeys( mapMultibindings, key -> MapType.from(key).valuesAreTypeOf(Producer.class) && !plainValueMapMultibindings.containsKey(keyFactory.unwrapMapValueType(key)) && !providerValueMapMultibindings.containsKey( keyFactory.rewrapMapKey(key, Producer.class, Provider.class).get())); return new ImmutableSet.Builder() .addAll(plainValueMapMultibindings.values()) .addAll(providerValueMapMultibindings.values()) .addAll(producerValueMapMultibindings.values()) .build(); } private ImmutableSet mapBindingContributions( dagger.model.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( dagger.model.Binding multiboundMapBinding, ImmutableSet contributions, DiagnosticReporter diagnosticReporter) { ImmutableSetMultimap contributionsByMapKey = ImmutableSetMultimap.copyOf( Multimaps.index(contributions, ContributionBinding::wrappedMapKeyAnnotation)); for (Set contributionsForOneMapKey : Multimaps.asMap(contributionsByMapKey).values()) { if (contributionsForOneMapKey.size() > 1) { diagnosticReporter.reportBinding( ERROR, multiboundMapBinding, duplicateMapKeyErrorMessage(contributionsForOneMapKey, multiboundMapBinding.key())); } } } private void checkForInconsistentMapKeyAnnotationTypes( dagger.model.Binding multiboundMapBinding, ImmutableSet contributions, DiagnosticReporter diagnosticReporter) { ImmutableSetMultimap, ContributionBinding> contributionsByMapKeyAnnotationType = indexByMapKeyAnnotationType(contributions); if (contributionsByMapKeyAnnotationType.keySet().size() > 1) { diagnosticReporter.reportBinding( ERROR, multiboundMapBinding, inconsistentMapKeyAnnotationTypesErrorMessage( contributionsByMapKeyAnnotationType, multiboundMapBinding.key())); } } private static ImmutableSetMultimap, ContributionBinding> indexByMapKeyAnnotationType(ImmutableSet contributions) { return ImmutableSetMultimap.copyOf( Multimaps.index( contributions, mapBinding -> MoreTypes.equivalence() .wrap(mapBinding.mapKeyAnnotation().get().getAnnotationType()))); } private String inconsistentMapKeyAnnotationTypesErrorMessage( ImmutableSetMultimap, 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.get()).append(':'); bindingDeclarationFormatter.formatIndentedList(message, contributions, 2); }); return message.toString(); } private String duplicateMapKeyErrorMessage( Set contributionsForOneMapKey, Key mapBindingKey) { StringBuilder message = new StringBuilder("The same map key is bound more than once for ").append(mapBindingKey); bindingDeclarationFormatter.formatIndentedList( message, ImmutableList.sortedCopyOf(BindingDeclaration.COMPARATOR, contributionsForOneMapKey), 1); return message.toString(); } }