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