• 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.common.collect.Iterables.getOnlyElement;
20 import static dagger.android.processor.AndroidMapKeys.injectedTypeFromMapKey;
21 import static dagger.internal.codegen.xprocessing.XTypes.toStableString;
22 
23 import androidx.room.compiler.processing.XAnnotation;
24 import androidx.room.compiler.processing.XElement;
25 import androidx.room.compiler.processing.XMethodElement;
26 import androidx.room.compiler.processing.XProcessingEnv;
27 import androidx.room.compiler.processing.XType;
28 import androidx.room.compiler.processing.XTypeElement;
29 import com.google.common.collect.ImmutableSet;
30 import com.google.common.collect.Sets;
31 import com.squareup.javapoet.ClassName;
32 import dagger.internal.codegen.xprocessing.XElements;
33 import dagger.internal.codegen.xprocessing.XTypes;
34 import javax.tools.Diagnostic.Kind;
35 
36 /** Validates the correctness of {@link dagger.MapKey}s used with {@code dagger.android}. */
37 final class AndroidMapKeyProcessingStep extends BaseProcessingStep {
38   private final XProcessingEnv processingEnv;
39 
AndroidMapKeyProcessingStep(XProcessingEnv processingEnv)40   AndroidMapKeyProcessingStep(XProcessingEnv processingEnv) {
41     this.processingEnv = processingEnv;
42   }
43 
44   @Override
annotationClassNames()45   public ImmutableSet<ClassName> annotationClassNames() {
46     return ImmutableSet.of(TypeNames.ANDROID_INJECTION_KEY, TypeNames.CLASS_KEY);
47   }
48 
49   @Override
process(XElement element, ImmutableSet<ClassName> annotationNames)50   public void process(XElement element, ImmutableSet<ClassName> annotationNames) {
51     for (ClassName annotationName : annotationNames) {
52       validateMethod(annotationName, XElements.asMethod(element));
53     }
54   }
55 
validateMethod(ClassName annotation, XMethodElement method)56   private void validateMethod(ClassName annotation, XMethodElement method) {
57     if (!Sets.union(
58             method.getAnnotationsAnnotatedWith(TypeNames.QUALIFIER),
59             method.getAnnotationsAnnotatedWith(TypeNames.QUALIFIER_JAVAX))
60         .isEmpty()) {
61       return;
62     }
63 
64     XType returnType = method.getReturnType();
65     if (!factoryElement().getType().getRawType().isAssignableFrom(returnType.getRawType())) {
66       // if returnType is not related to AndroidInjector.Factory, ignore the method
67       return;
68     }
69 
70     if (!Sets.union(
71             method.getAnnotationsAnnotatedWith(TypeNames.SCOPE),
72             method.getAnnotationsAnnotatedWith(TypeNames.SCOPE_JAVAX))
73         .isEmpty()) {
74       XAnnotation suppressedWarnings = method.getAnnotation(ClassName.get(SuppressWarnings.class));
75       if (suppressedWarnings == null
76           || !ImmutableSet.copyOf(suppressedWarnings.getAsStringList("value"))
77               .contains("dagger.android.ScopedInjectorFactory")) {
78         XAnnotation mapKeyAnnotation =
79             getOnlyElement(method.getAnnotationsAnnotatedWith(TypeNames.MAP_KEY));
80         XTypeElement mapKeyValueElement =
81             processingEnv.requireTypeElement(injectedTypeFromMapKey(mapKeyAnnotation).get());
82         processingEnv
83             .getMessager()
84             .printMessage(
85                 Kind.ERROR,
86                 String.format(
87                     "%s bindings should not be scoped. Scoping this method may leak instances of"
88                         + " %s.",
89                     TypeNames.ANDROID_INJECTOR_FACTORY.canonicalName(),
90                     mapKeyValueElement.getQualifiedName()),
91                 method);
92       }
93     }
94 
95     validateReturnType(method);
96 
97     // @Binds methods should only have one parameter, but we can't guarantee the order of Processors
98     // in javac, so do a basic check for valid form
99     if (method.hasAnnotation(TypeNames.BINDS) && method.getParameters().size() == 1) {
100       validateMapKeyMatchesBindsParameter(annotation, method);
101     }
102   }
103 
104   /** Report an error if the method's return type is not {@code AndroidInjector.Factory<?>}. */
validateReturnType(XMethodElement method)105   private void validateReturnType(XMethodElement method) {
106     XType returnType = method.getReturnType();
107     XType requiredReturnType = injectorFactoryOf(processingEnv.getWildcardType(null, null));
108 
109     // TODO(b/311460276) use XType.isSameType when the bug is fixed.
110     if (!returnType.getTypeName().equals(requiredReturnType.getTypeName())) {
111       processingEnv
112           .getMessager()
113           .printMessage(
114               Kind.ERROR,
115               String.format(
116                   "%s should bind %s, not %s. See https://dagger.dev/android",
117                   method, toStableString(requiredReturnType), toStableString(returnType)),
118               method);
119     }
120   }
121 
122   /**
123    * A valid @Binds method could bind an {@code AndroidInjector.Factory} for one type, while giving
124    * it a map key of a different type. The return type and parameter type would pass typical @Binds
125    * validation, but the map lookup in {@code DispatchingAndroidInjector} would retrieve the wrong
126    * injector factory.
127    *
128    * <pre>{@code
129    * {@literal @Binds}
130    * {@literal @IntoMap}
131    * {@literal @ClassKey(GreenActivity.class)}
132    * abstract AndroidInjector.Factory<?> bindBlueActivity(
133    *     BlueActivityComponent.Builder builder);
134    * }</pre>
135    */
validateMapKeyMatchesBindsParameter( ClassName annotationName, XMethodElement method)136   private void validateMapKeyMatchesBindsParameter(
137       ClassName annotationName, XMethodElement method) {
138     XType parameterType = getOnlyElement(method.getParameters()).getType();
139     XAnnotation annotation = method.getAnnotation(annotationName);
140     XType mapKeyType =
141         processingEnv.requireTypeElement(injectedTypeFromMapKey(annotation).get()).getType();
142     if (!XTypes.isAssignableTo(parameterType, injectorFactoryOf(mapKeyType))) {
143       processingEnv
144           .getMessager()
145           .printMessage(
146               Kind.ERROR,
147               String.format(
148                   "%s does not implement AndroidInjector<%s>",
149                   toStableString(parameterType), toStableString(mapKeyType)),
150               method,
151               annotation);
152     }
153   }
154 
155   /** Returns a {@link XType} for {@code AndroidInjector.Factory<implementationType>}. */
injectorFactoryOf(XType implementationType)156   private XType injectorFactoryOf(XType implementationType) {
157     return processingEnv.getDeclaredType(factoryElement(), implementationType);
158   }
159 
factoryElement()160   private XTypeElement factoryElement() {
161     return processingEnv.requireTypeElement(TypeNames.ANDROID_INJECTOR_FACTORY.canonicalName());
162   }
163 }
164