• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.auto.common.MoreElements.getAnnotationMirror;
21 import static com.google.auto.common.MoreElements.isAnnotationPresent;
22 import static com.google.common.collect.Iterables.getOnlyElement;
23 import static dagger.android.processor.AndroidMapKeys.injectedTypeFromMapKey;
24 
25 import com.google.auto.common.BasicAnnotationProcessor.ProcessingStep;
26 import com.google.auto.common.MoreElements;
27 import com.google.common.collect.ImmutableSet;
28 import com.google.common.collect.SetMultimap;
29 import dagger.Binds;
30 import dagger.MapKey;
31 import dagger.android.AndroidInjectionKey;
32 import dagger.android.AndroidInjector;
33 import dagger.multibindings.ClassKey;
34 import java.lang.annotation.Annotation;
35 import java.util.Set;
36 import javax.annotation.processing.Messager;
37 import javax.inject.Qualifier;
38 import javax.inject.Scope;
39 import javax.lang.model.element.AnnotationMirror;
40 import javax.lang.model.element.Element;
41 import javax.lang.model.element.ExecutableElement;
42 import javax.lang.model.element.TypeElement;
43 import javax.lang.model.type.DeclaredType;
44 import javax.lang.model.type.TypeMirror;
45 import javax.lang.model.util.Elements;
46 import javax.lang.model.util.Types;
47 import javax.tools.Diagnostic.Kind;
48 
49 /** Validates the correctness of {@link MapKey}s used with {@code dagger.android}. */
50 final class AndroidMapKeyValidator implements ProcessingStep {
51   private final Elements elements;
52   private final Types types;
53   private final Messager messager;
54 
AndroidMapKeyValidator(Elements elements, Types types, Messager messager)55   AndroidMapKeyValidator(Elements elements, Types types, Messager messager) {
56     this.elements = elements;
57     this.types = types;
58     this.messager = messager;
59   }
60 
61   @Override
annotations()62   public Set<? extends Class<? extends Annotation>> annotations() {
63     return ImmutableSet.<Class<? extends Annotation>>builder()
64         .add(AndroidInjectionKey.class)
65         .add(ClassKey.class)
66         .build();
67   }
68 
69   @Override
process( SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation)70   public Set<Element> process(
71       SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation) {
72     ImmutableSet.Builder<Element> deferredElements = ImmutableSet.builder();
73     elementsByAnnotation
74         .entries()
75         .forEach(
76             entry -> {
77               try {
78                 validateMethod(entry.getKey(), MoreElements.asExecutable(entry.getValue()));
79               } catch (TypeNotPresentException e) {
80                 deferredElements.add(entry.getValue());
81               }
82             });
83     return deferredElements.build();
84   }
85 
validateMethod(Class<? extends Annotation> annotation, ExecutableElement method)86   private void validateMethod(Class<? extends Annotation> annotation, ExecutableElement method) {
87     if (!getAnnotatedAnnotations(method, Qualifier.class).isEmpty()) {
88       return;
89     }
90 
91     TypeMirror returnType = method.getReturnType();
92     if (!types.isAssignable(types.erasure(returnType), factoryElement().asType())) {
93       // if returnType is not related to AndroidInjector.Factory, ignore the method
94       return;
95     }
96 
97     if (!getAnnotatedAnnotations(method, Scope.class).isEmpty()) {
98       SuppressWarnings suppressedWarnings = method.getAnnotation(SuppressWarnings.class);
99       if (suppressedWarnings == null
100           || !ImmutableSet.copyOf(suppressedWarnings.value())
101               .contains("dagger.android.ScopedInjectorFactory")) {
102         AnnotationMirror mapKeyAnnotation =
103             getOnlyElement(getAnnotatedAnnotations(method, MapKey.class));
104         TypeElement mapKeyValueElement =
105             elements.getTypeElement(injectedTypeFromMapKey(mapKeyAnnotation).get());
106         messager.printMessage(
107             Kind.ERROR,
108             String.format(
109                 "%s bindings should not be scoped. Scoping this method may leak instances of %s.",
110                 AndroidInjector.Factory.class.getCanonicalName(),
111                 mapKeyValueElement.getQualifiedName()),
112             method);
113       }
114     }
115 
116     validateReturnType(method);
117 
118     // @Binds methods should only have one parameter, but we can't guarantee the order of Processors
119     // in javac, so do a basic check for valid form
120     if (isAnnotationPresent(method, Binds.class) && method.getParameters().size() == 1) {
121       validateMapKeyMatchesBindsParameter(annotation, method);
122     }
123   }
124 
125   /** Report an error if the method's return type is not {@code AndroidInjector.Factory<?>}. */
validateReturnType(ExecutableElement method)126   private void validateReturnType(ExecutableElement method) {
127     TypeMirror returnType = method.getReturnType();
128     DeclaredType requiredReturnType = injectorFactoryOf(types.getWildcardType(null, null));
129 
130     if (!types.isSameType(returnType, requiredReturnType)) {
131       messager.printMessage(
132           Kind.ERROR,
133           String.format(
134               "%s should bind %s, not %s. See https://dagger.dev/android",
135               method, requiredReturnType, returnType),
136           method);
137     }
138   }
139 
140   /**
141    * A valid @Binds method could bind an {@link AndroidInjector.Factory} for one type, while giving
142    * it a map key of a different type. The return type and parameter type would pass typical @Binds
143    * validation, but the map lookup in {@link dagger.android.DispatchingAndroidInjector} would
144    * retrieve the wrong injector factory.
145    *
146    * <pre>{@code
147    * {@literal @Binds}
148    * {@literal @IntoMap}
149    * {@literal @ClassKey(GreenActivity.class)}
150    * abstract AndroidInjector.Factory<?> bindBlueActivity(
151    *     BlueActivityComponent.Builder builder);
152    * }</pre>
153    */
validateMapKeyMatchesBindsParameter( Class<? extends Annotation> annotation, ExecutableElement method)154   private void validateMapKeyMatchesBindsParameter(
155       Class<? extends Annotation> annotation, ExecutableElement method) {
156     TypeMirror parameterType = getOnlyElement(method.getParameters()).asType();
157     AnnotationMirror annotationMirror = getAnnotationMirror(method, annotation).get();
158     TypeMirror mapKeyType =
159         elements.getTypeElement(injectedTypeFromMapKey(annotationMirror).get()).asType();
160     if (!types.isAssignable(parameterType, injectorFactoryOf(mapKeyType))) {
161       messager.printMessage(
162           Kind.ERROR,
163           String.format("%s does not implement AndroidInjector<%s>", parameterType, mapKeyType),
164           method,
165           annotationMirror);
166     }
167   }
168 
169   /** Returns a {@link DeclaredType} for {@code AndroidInjector.Factory<implementationType>}. */
injectorFactoryOf(TypeMirror implementationType)170   private DeclaredType injectorFactoryOf(TypeMirror implementationType) {
171     return types.getDeclaredType(factoryElement(), implementationType);
172   }
173 
factoryElement()174   private TypeElement factoryElement() {
175     return elements.getTypeElement(AndroidInjector.Factory.class.getCanonicalName());
176   }
177 }
178