• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.internal.codegen.binding;
18 
19 import static androidx.room.compiler.codegen.XTypeNameKt.toJavaPoet;
20 import static androidx.room.compiler.processing.XTypeKt.isArray;
21 import static com.google.common.base.Preconditions.checkArgument;
22 import static com.google.common.collect.Iterables.getOnlyElement;
23 import static com.squareup.javapoet.MethodSpec.methodBuilder;
24 import static dagger.internal.codegen.base.MapKeyAccessibility.isMapKeyPubliclyAccessible;
25 import static dagger.internal.codegen.binding.SourceFiles.elementBasedClassName;
26 import static dagger.internal.codegen.extension.DaggerCollectors.toOptional;
27 import static dagger.internal.codegen.xprocessing.XElements.asExecutable;
28 import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
29 import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
30 import static dagger.internal.codegen.xprocessing.XTypes.isTypeOf;
31 import static dagger.internal.codegen.xprocessing.XTypes.rewrapType;
32 import static javax.lang.model.element.Modifier.PUBLIC;
33 import static javax.lang.model.element.Modifier.STATIC;
34 
35 import androidx.room.compiler.processing.XAnnotation;
36 import androidx.room.compiler.processing.XAnnotationValue;
37 import androidx.room.compiler.processing.XElement;
38 import androidx.room.compiler.processing.XMethodElement;
39 import androidx.room.compiler.processing.XProcessingEnv;
40 import androidx.room.compiler.processing.XType;
41 import androidx.room.compiler.processing.XTypeElement;
42 import com.google.common.collect.ImmutableSet;
43 import com.squareup.javapoet.ClassName;
44 import com.squareup.javapoet.CodeBlock;
45 import com.squareup.javapoet.MethodSpec;
46 import dagger.MapKey;
47 import dagger.internal.codegen.base.DaggerSuperficialValidation;
48 import dagger.internal.codegen.base.MapKeyAccessibility;
49 import dagger.internal.codegen.javapoet.TypeNames;
50 import dagger.internal.codegen.xprocessing.XElements;
51 import java.util.NoSuchElementException;
52 import java.util.Optional;
53 
54 /** Methods for extracting {@link MapKey} annotations and key code blocks from binding elements. */
55 public final class MapKeys {
56   public static final String LAZY_CLASS_KEY_NAME_FIELD = "lazyClassKeyName";
57   public static final String KEEP_FIELD_TYPE_FIELD = "keepFieldType";
58 
59   /**
60    * If {@code bindingElement} is annotated with a {@link MapKey} annotation, returns it.
61    *
62    * @throws IllegalArgumentException if the element is annotated with more than one {@code MapKey}
63    *     annotation
64    */
getMapKey(XElement bindingElement)65   static Optional<XAnnotation> getMapKey(XElement bindingElement) {
66     return getMapKeys(bindingElement).stream().collect(toOptional());
67   }
68 
69   /** Returns all of the {@link MapKey} annotations that annotate {@code bindingElement}. */
getMapKeys(XElement bindingElement)70   public static ImmutableSet<XAnnotation> getMapKeys(XElement bindingElement) {
71     return XElements.getAnnotatedAnnotations(bindingElement, TypeNames.MAP_KEY);
72   }
73 
74   /**
75    * Returns the annotation value if {@code mapKey}'s type is annotated with {@link
76    * MapKey @MapKey(unwrapValue = true)}.
77    *
78    * @throws IllegalArgumentException if {@code mapKey}'s type is not annotated with {@link
79    *     MapKey @MapKey} at all.
80    */
unwrapValue(XAnnotation mapKey)81   private static Optional<XAnnotationValue> unwrapValue(XAnnotation mapKey) {
82     XTypeElement mapKeyType = mapKey.getType().getTypeElement();
83     XAnnotation mapKeyAnnotation = mapKeyType.getAnnotation(TypeNames.MAP_KEY);
84     checkArgument(mapKeyAnnotation != null, "%s is not annotated with @MapKey", mapKeyType);
85     return mapKeyAnnotation.getAsBoolean("unwrapValue")
86         ? Optional.of(getOnlyElement(mapKey.getAnnotationValues()))
87         : Optional.empty();
88   }
89 
mapKeyType(XAnnotation mapKey)90   static XType mapKeyType(XAnnotation mapKey) {
91     return unwrapValue(mapKey).isPresent()
92         ? getUnwrappedMapKeyType(mapKey.getType())
93         : mapKey.getType();
94   }
95 
96   /**
97    * Returns the map key type for an unwrapped {@link MapKey} annotation type. If the single member
98    * type is primitive, returns the boxed type.
99    *
100    * @throws IllegalArgumentException if {@code mapKeyAnnotationType} is not an annotation type or
101    *     has more than one member, or if its single member is an array
102    * @throws NoSuchElementException if the annotation has no members
103    */
getUnwrappedMapKeyType(XType mapKeyAnnotationType)104   public static XType getUnwrappedMapKeyType(XType mapKeyAnnotationType) {
105     checkArgument(
106         isDeclared(mapKeyAnnotationType)
107             && mapKeyAnnotationType.getTypeElement().isAnnotationClass(),
108         "%s is not an annotation type",
109         mapKeyAnnotationType);
110 
111     XMethodElement annotationValueMethod =
112         getOnlyElement(mapKeyAnnotationType.getTypeElement().getDeclaredMethods());
113     XType annotationValueType = annotationValueMethod.getReturnType();
114     if (isArray(annotationValueType)) {
115       throw new IllegalArgumentException(
116           mapKeyAnnotationType
117               + "."
118               + getSimpleName(annotationValueMethod)
119               + " cannot be an array");
120     }
121     // If the source kind is Kotlin, the annotation value type is seen as KClass rather than Class,
122     // but either way we want the multibinding key to be Class so we rewrap it here.
123     return isTypeOf(annotationValueType, TypeNames.KCLASS)
124         ? rewrapType(annotationValueType, TypeNames.CLASS)
125         : annotationValueType.boxed();
126   }
127 
128   /**
129    * Returns a code block for {@code binding}'s {@link ContributionBinding#mapKeyAnnotation() map
130    * key}. If for whatever reason the map key is not accessible from within {@code requestingClass}
131    * (i.e. it has a package-private {@code enum} from a different package), this will return an
132    * invocation of a proxy-method giving it access.
133    *
134    * @throws IllegalStateException if {@code binding} is not a {@link dagger.multibindings.IntoMap
135    *     map} contribution.
136    */
getMapKeyExpression( ContributionBinding binding, ClassName requestingClass, XProcessingEnv processingEnv)137   public static CodeBlock getMapKeyExpression(
138       ContributionBinding binding, ClassName requestingClass, XProcessingEnv processingEnv) {
139     XAnnotation mapKeyAnnotation = binding.mapKey().get();
140     return MapKeyAccessibility.isMapKeyAccessibleFrom(
141             mapKeyAnnotation, requestingClass.packageName())
142         ? directMapKeyExpression(mapKeyAnnotation, processingEnv)
143         : CodeBlock.of("$T.create()", mapKeyProxyClassName(binding));
144   }
145 
146   /**
147    * Returns a code block for the map key annotation {@code mapKey}.
148    *
149    * <p>This method assumes the map key will be accessible in the context that the returned {@link
150    * CodeBlock} is used. Use {@link #getMapKeyExpression(ContributionBinding, ClassName,
151    * XProcessingEnv)} when that assumption is not guaranteed.
152    *
153    * @throws IllegalArgumentException if the element is annotated with more than one {@code MapKey}
154    *     annotation
155    * @throws IllegalStateException if {@code bindingElement} is not annotated with a {@code MapKey}
156    *     annotation
157    */
directMapKeyExpression( XAnnotation mapKey, XProcessingEnv processingEnv)158   private static CodeBlock directMapKeyExpression(
159       XAnnotation mapKey, XProcessingEnv processingEnv) {
160     Optional<XAnnotationValue> unwrappedValue = unwrapValue(mapKey);
161     if (mapKey.getQualifiedName().contentEquals("dagger.android.AndroidInjectionKey")) {
162       XTypeElement unwrappedType =
163           DaggerSuperficialValidation.requireTypeElement(
164               processingEnv, unwrappedValue.get().asString());
165       return CodeBlock.of(
166           "$T.of($S)",
167           ClassName.get("dagger.android.internal", "AndroidInjectionKeys"),
168           unwrappedType.getClassName().reflectionName());
169     }
170     AnnotationExpression annotationExpression = new AnnotationExpression(mapKey);
171     return unwrappedValue.isPresent()
172         ? annotationExpression.getValueExpression(unwrappedValue.get())
173         : annotationExpression.getAnnotationInstanceExpression();
174   }
175 
176   /**
177    * Returns the {@link ClassName} in which {@link #mapKeyFactoryMethod(ContributionBinding,
178    * XProcessingEnv)} is generated.
179    */
mapKeyProxyClassName(ContributionBinding binding)180   public static ClassName mapKeyProxyClassName(ContributionBinding binding) {
181     return toJavaPoet(
182         elementBasedClassName(asExecutable(binding.bindingElement().get()), "MapKey"));
183   }
184 
185   /**
186    * A {@code static create()} method to be added to {@link
187    * #mapKeyProxyClassName(ContributionBinding)} when the {@code @MapKey} annotation is not publicly
188    * accessible.
189    */
mapKeyFactoryMethod( ContributionBinding binding, XProcessingEnv processingEnv)190   public static Optional<MethodSpec> mapKeyFactoryMethod(
191       ContributionBinding binding, XProcessingEnv processingEnv) {
192     return binding
193         .mapKey()
194         .filter(mapKey -> !isMapKeyPubliclyAccessible(mapKey))
195         .map(
196             mapKey ->
197                 methodBuilder("create")
198                     .addModifiers(PUBLIC, STATIC)
199                     .returns(mapKeyType(mapKey).getTypeName())
200                     .addStatement("return $L", directMapKeyExpression(mapKey, processingEnv))
201                     .build());
202   }
203 
204   /**
205    * Returns if this binding is a map binding and uses @LazyClassKey for contributing class keys.
206    *
207    * <p>@LazyClassKey won't co-exist with @ClassKey in the graph, since the same binding type cannot
208    * use more than one @MapKey annotation type and Dagger validation will fail.
209    */
useLazyClassKey(Binding binding, BindingGraph graph)210   public static boolean useLazyClassKey(Binding binding, BindingGraph graph) {
211     if (!binding.dependencies().isEmpty()) {
212       ContributionBinding contributionBinding =
213           graph.contributionBinding(binding.dependencies().iterator().next().key());
214       return contributionBinding.mapKey().isPresent()
215           && contributionBinding
216               .mapKey()
217               .get()
218               .getClassName()
219               .equals(TypeNames.LAZY_CLASS_KEY);
220     }
221     return false;
222   }
223 
getLazyClassMapKeyExpression(ContributionBinding contributionBinding)224   public static CodeBlock getLazyClassMapKeyExpression(ContributionBinding contributionBinding) {
225     ClassName proxyClassName =
226         lazyClassKeyProxyClassName(XElements.asMethod(contributionBinding.bindingElement().get()));
227     return CodeBlock.of("$T.$N", proxyClassName, LAZY_CLASS_KEY_NAME_FIELD);
228   }
229 
lazyClassKeyProxyClassName(XMethodElement methodElement)230   public static ClassName lazyClassKeyProxyClassName(XMethodElement methodElement) {
231     return toJavaPoet(elementBasedClassName(methodElement, "_LazyMapKey"));
232   }
233 
MapKeys()234   private MapKeys() {}
235 }
236