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