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.auto.common.AnnotationMirrors.getAnnotatedAnnotations;
20 import static com.google.common.collect.Iterables.getOnlyElement;
21 import static dagger.android.processor.AndroidMapKeys.injectedTypeFromMapKey;
22 import static java.util.stream.Collectors.collectingAndThen;
23 import static java.util.stream.Collectors.toList;
24 import static javax.tools.Diagnostic.Kind.ERROR;
25 
26 import com.google.auto.common.MoreTypes;
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.MapKey;
33 import dagger.android.AndroidInjector;
34 import dagger.android.DispatchingAndroidInjector;
35 import dagger.model.Binding;
36 import dagger.model.BindingGraph;
37 import dagger.model.BindingKind;
38 import dagger.model.Key;
39 import dagger.spi.BindingGraphPlugin;
40 import dagger.spi.DiagnosticReporter;
41 import java.util.Formatter;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Optional;
45 import java.util.stream.Stream;
46 import javax.inject.Provider;
47 import javax.lang.model.element.AnnotationMirror;
48 import javax.lang.model.type.TypeKind;
49 import javax.lang.model.type.TypeMirror;
50 
51 /**
52  * Validates that the two maps that {@link DispatchingAndroidInjector} injects have logically
53  * different keys. If a contribution exists for the same {@code FooActivity} with
54  * {@code @ActivityKey(FooActivity.class)} and
55  * {@code @AndroidInjectionKey("com.example.FooActivity")}, report an error.
56  */
57 @AutoService(BindingGraphPlugin.class)
58 public final class DuplicateAndroidInjectorsChecker implements BindingGraphPlugin {
59   @Override
visitGraph(BindingGraph graph, DiagnosticReporter diagnosticReporter)60   public void visitGraph(BindingGraph graph, DiagnosticReporter diagnosticReporter) {
61     for (Binding binding : graph.bindings()) {
62       if (isDispatchingAndroidInjector(binding)) {
63         validateMapKeyUniqueness(binding, graph, diagnosticReporter);
64       }
65     }
66   }
67 
isDispatchingAndroidInjector(Binding binding)68   private boolean isDispatchingAndroidInjector(Binding binding) {
69     Key key = binding.key();
70     return MoreTypes.isTypeOf(DispatchingAndroidInjector.class, key.type())
71         && !key.qualifier().isPresent();
72   }
73 
validateMapKeyUniqueness( Binding dispatchingAndroidInjector, BindingGraph graph, DiagnosticReporter diagnosticReporter)74   private void validateMapKeyUniqueness(
75       Binding dispatchingAndroidInjector,
76       BindingGraph graph,
77       DiagnosticReporter diagnosticReporter) {
78     ImmutableSet<Binding> injectorFactories =
79         injectorMapDependencies(dispatchingAndroidInjector, graph)
80             .flatMap(injectorFactoryMap -> graph.requestedBindings(injectorFactoryMap).stream())
81             .collect(collectingAndThen(toList(), ImmutableSet::copyOf));
82 
83     ImmutableListMultimap.Builder<String, Binding> mapKeyIndex = ImmutableListMultimap.builder();
84     for (Binding injectorFactory : injectorFactories) {
85       AnnotationMirror mapKey = mapKey(injectorFactory).get();
86       Optional<String> injectedType = injectedTypeFromMapKey(mapKey);
87       if (injectedType.isPresent()) {
88         mapKeyIndex.put(injectedType.get(), injectorFactory);
89       } else {
90         diagnosticReporter.reportBinding(
91             ERROR, injectorFactory, "Unrecognized class: %s", mapKey);
92       }
93     }
94 
95     Map<String, List<Binding>> duplicates =
96         Maps.filterValues(Multimaps.asMap(mapKeyIndex.build()), bindings -> bindings.size() > 1);
97     if (!duplicates.isEmpty()) {
98       StringBuilder errorMessage =
99           new StringBuilder("Multiple injector factories bound for the same type:\n");
100       Formatter formatter = new Formatter(errorMessage);
101       duplicates.forEach(
102           (injectedType, duplicateFactories) -> {
103             formatter.format("  %s:\n", injectedType);
104             duplicateFactories.forEach(duplicate -> formatter.format("    %s\n", duplicate));
105           });
106       diagnosticReporter.reportBinding(ERROR, dispatchingAndroidInjector, errorMessage.toString());
107     }
108   }
109 
110   /**
111    * Returns a stream of the dependencies of {@code binding} that have a key type of {@code Map<K,
112    * Provider<AndroidInjector.Factory<?>>}.
113    */
injectorMapDependencies(Binding binding, BindingGraph graph)114   private Stream<Binding> injectorMapDependencies(Binding binding, BindingGraph graph) {
115     return graph.requestedBindings(binding).stream()
116         .filter(requestedBinding -> requestedBinding.kind().equals(BindingKind.MULTIBOUND_MAP))
117         .filter(
118             requestedBinding -> {
119               TypeMirror valueType =
120                   MoreTypes.asDeclared(requestedBinding.key().type()).getTypeArguments().get(1);
121               if (!MoreTypes.isTypeOf(Provider.class, valueType)
122                   || !valueType.getKind().equals(TypeKind.DECLARED)) {
123                 return false;
124               }
125               TypeMirror providedType = MoreTypes.asDeclared(valueType).getTypeArguments().get(0);
126               return MoreTypes.isTypeOf(AndroidInjector.Factory.class, providedType);
127             });
128   }
129 
mapKey(Binding binding)130   private Optional<AnnotationMirror> mapKey(Binding binding) {
131     return binding
132         .bindingElement()
133         .map(bindingElement -> getAnnotatedAnnotations(bindingElement, MapKey.class))
134         .flatMap(
135             annotations ->
136                 annotations.isEmpty()
137                     ? Optional.empty()
138                     : Optional.of(getOnlyElement(annotations)));
139   }
140 
141   @Override
pluginName()142   public String pluginName() {
143     return "Dagger/Android/DuplicateAndroidInjectors";
144   }
145 }
146