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