1 /* 2 * Copyright (C) 2018 The Dagger Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package dagger.android.processor; 18 19 import static com.google.common.collect.Iterables.getOnlyElement; 20 import static dagger.android.processor.AndroidMapKeys.injectedTypeFromMapKey; 21 import static java.util.stream.Collectors.collectingAndThen; 22 import static java.util.stream.Collectors.toList; 23 import static javax.tools.Diagnostic.Kind.ERROR; 24 25 import androidx.room.compiler.processing.XAnnotation; 26 import androidx.room.compiler.processing.XType; 27 import com.google.auto.service.AutoService; 28 import com.google.common.collect.ImmutableListMultimap; 29 import com.google.common.collect.ImmutableSet; 30 import com.google.common.collect.Maps; 31 import com.google.common.collect.Multimaps; 32 import dagger.internal.codegen.xprocessing.DaggerElements; 33 import dagger.internal.codegen.xprocessing.XElements; 34 import dagger.internal.codegen.xprocessing.XTypes; 35 import dagger.spi.model.Binding; 36 import dagger.spi.model.BindingGraph; 37 import dagger.spi.model.BindingGraphPlugin; 38 import dagger.spi.model.BindingKind; 39 import dagger.spi.model.DaggerProcessingEnv; 40 import dagger.spi.model.DiagnosticReporter; 41 import dagger.spi.model.Key; 42 import java.util.Formatter; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Optional; 46 import java.util.stream.Stream; 47 48 /** 49 * Validates that the two maps that {@code DispatchingAndroidInjector} injects have logically 50 * different keys. If a contribution exists for the same {@code FooActivity} with 51 * {@code @ActivityKey(FooActivity.class)} and 52 * {@code @AndroidInjectionKey("com.example.FooActivity")}, report an error. 53 */ 54 @AutoService(BindingGraphPlugin.class) 55 public final class DuplicateAndroidInjectorsChecker implements BindingGraphPlugin { 56 private DaggerProcessingEnv processingEnv; 57 58 @Override init(DaggerProcessingEnv processingEnv, Map<String, String> options)59 public void init(DaggerProcessingEnv processingEnv, Map<String, String> options) { 60 this.processingEnv = processingEnv; 61 } 62 63 @Override visitGraph(BindingGraph graph, DiagnosticReporter diagnosticReporter)64 public void visitGraph(BindingGraph graph, DiagnosticReporter diagnosticReporter) { 65 for (Binding binding : graph.bindings()) { 66 if (isDispatchingAndroidInjector(binding)) { 67 validateMapKeyUniqueness(binding, graph, diagnosticReporter); 68 } 69 } 70 } 71 isDispatchingAndroidInjector(Binding binding)72 private boolean isDispatchingAndroidInjector(Binding binding) { 73 Key key = binding.key(); 74 75 return XTypes.isTypeOf( 76 DaggerElements.toXProcessing(key.type(), processingEnv), 77 TypeNames.DISPATCHING_ANDROID_INJECTOR) 78 && !key.qualifier().isPresent(); 79 } 80 validateMapKeyUniqueness( Binding dispatchingAndroidInjector, BindingGraph graph, DiagnosticReporter diagnosticReporter)81 private void validateMapKeyUniqueness( 82 Binding dispatchingAndroidInjector, 83 BindingGraph graph, 84 DiagnosticReporter diagnosticReporter) { 85 ImmutableSet<Binding> injectorFactories = 86 injectorMapDependencies(dispatchingAndroidInjector, graph) 87 .flatMap(injectorFactoryMap -> graph.requestedBindings(injectorFactoryMap).stream()) 88 .collect(collectingAndThen(toList(), ImmutableSet::copyOf)); 89 90 ImmutableListMultimap.Builder<String, Binding> mapKeyIndex = ImmutableListMultimap.builder(); 91 for (Binding injectorFactory : injectorFactories) { 92 XAnnotation mapKey = mapKey(injectorFactory).get(); 93 Optional<String> injectedType = injectedTypeFromMapKey(mapKey); 94 if (injectedType.isPresent()) { 95 mapKeyIndex.put(injectedType.get(), injectorFactory); 96 } else { 97 diagnosticReporter.reportBinding( 98 ERROR, injectorFactory, "Unrecognized class: %s", mapKey); 99 } 100 } 101 102 Map<String, List<Binding>> duplicates = 103 Maps.filterValues(Multimaps.asMap(mapKeyIndex.build()), bindings -> bindings.size() > 1); 104 if (!duplicates.isEmpty()) { 105 StringBuilder errorMessage = 106 new StringBuilder("Multiple injector factories bound for the same type:\n"); 107 Formatter formatter = new Formatter(errorMessage); 108 duplicates.forEach( 109 (injectedType, duplicateFactories) -> { 110 formatter.format(" %s:\n", injectedType); 111 duplicateFactories.forEach(duplicate -> formatter.format(" %s\n", duplicate)); 112 }); 113 diagnosticReporter.reportBinding(ERROR, dispatchingAndroidInjector, errorMessage.toString()); 114 } 115 } 116 117 /** 118 * Returns a stream of the dependencies of {@code binding} that have a key type of {@code Map<K, 119 * Provider<AndroidInjector.Factory<?>>}. 120 */ injectorMapDependencies(Binding binding, BindingGraph graph)121 private Stream<Binding> injectorMapDependencies(Binding binding, BindingGraph graph) { 122 return graph.requestedBindings(binding).stream() 123 .filter(requestedBinding -> requestedBinding.kind().equals(BindingKind.MULTIBOUND_MAP)) 124 .filter( 125 requestedBinding -> { 126 XType valueType = 127 DaggerElements.toXProcessing(requestedBinding.key().type(), processingEnv) 128 .getTypeArguments() 129 .get(1); 130 if (!XTypes.isTypeOf(valueType, TypeNames.PROVIDER) 131 || !XTypes.isDeclared(valueType)) { 132 return false; 133 } 134 XType providedType = valueType.getTypeArguments().get(0); 135 return XTypes.isTypeOf(providedType, TypeNames.ANDROID_INJECTOR_FACTORY); 136 }); 137 } 138 mapKey(Binding binding)139 private Optional<XAnnotation> mapKey(Binding binding) { 140 return binding 141 .bindingElement() 142 .map( 143 bindingElement -> 144 XElements.getAnnotatedAnnotations( 145 DaggerElements.toXProcessing(bindingElement, processingEnv), TypeNames.MAP_KEY)) 146 .flatMap( 147 annotations -> 148 annotations.isEmpty() 149 ? Optional.empty() 150 : Optional.of(getOnlyElement(annotations))); 151 } 152 153 @Override pluginName()154 public String pluginName() { 155 return "Dagger/Android/DuplicateAndroidInjectors"; 156 } 157 } 158