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.auto.common.AnnotationMirrors.getAnnotatedAnnotations; 20 import static com.google.auto.common.MoreElements.getAnnotationMirror; 21 import static com.google.auto.common.MoreElements.isAnnotationPresent; 22 import static com.google.common.collect.Iterables.getOnlyElement; 23 import static dagger.android.processor.AndroidMapKeys.injectedTypeFromMapKey; 24 25 import com.google.auto.common.BasicAnnotationProcessor.ProcessingStep; 26 import com.google.auto.common.MoreElements; 27 import com.google.common.collect.ImmutableSet; 28 import com.google.common.collect.SetMultimap; 29 import dagger.Binds; 30 import dagger.MapKey; 31 import dagger.android.AndroidInjectionKey; 32 import dagger.android.AndroidInjector; 33 import dagger.multibindings.ClassKey; 34 import java.lang.annotation.Annotation; 35 import java.util.Set; 36 import javax.annotation.processing.Messager; 37 import javax.inject.Qualifier; 38 import javax.inject.Scope; 39 import javax.lang.model.element.AnnotationMirror; 40 import javax.lang.model.element.Element; 41 import javax.lang.model.element.ExecutableElement; 42 import javax.lang.model.element.TypeElement; 43 import javax.lang.model.type.DeclaredType; 44 import javax.lang.model.type.TypeMirror; 45 import javax.lang.model.util.Elements; 46 import javax.lang.model.util.Types; 47 import javax.tools.Diagnostic.Kind; 48 49 /** Validates the correctness of {@link MapKey}s used with {@code dagger.android}. */ 50 final class AndroidMapKeyValidator implements ProcessingStep { 51 private final Elements elements; 52 private final Types types; 53 private final Messager messager; 54 AndroidMapKeyValidator(Elements elements, Types types, Messager messager)55 AndroidMapKeyValidator(Elements elements, Types types, Messager messager) { 56 this.elements = elements; 57 this.types = types; 58 this.messager = messager; 59 } 60 61 @Override annotations()62 public Set<? extends Class<? extends Annotation>> annotations() { 63 return ImmutableSet.<Class<? extends Annotation>>builder() 64 .add(AndroidInjectionKey.class) 65 .add(ClassKey.class) 66 .build(); 67 } 68 69 @Override process( SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation)70 public Set<Element> process( 71 SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation) { 72 ImmutableSet.Builder<Element> deferredElements = ImmutableSet.builder(); 73 elementsByAnnotation 74 .entries() 75 .forEach( 76 entry -> { 77 try { 78 validateMethod(entry.getKey(), MoreElements.asExecutable(entry.getValue())); 79 } catch (TypeNotPresentException e) { 80 deferredElements.add(entry.getValue()); 81 } 82 }); 83 return deferredElements.build(); 84 } 85 validateMethod(Class<? extends Annotation> annotation, ExecutableElement method)86 private void validateMethod(Class<? extends Annotation> annotation, ExecutableElement method) { 87 if (!getAnnotatedAnnotations(method, Qualifier.class).isEmpty()) { 88 return; 89 } 90 91 TypeMirror returnType = method.getReturnType(); 92 if (!types.isAssignable(types.erasure(returnType), factoryElement().asType())) { 93 // if returnType is not related to AndroidInjector.Factory, ignore the method 94 return; 95 } 96 97 if (!getAnnotatedAnnotations(method, Scope.class).isEmpty()) { 98 SuppressWarnings suppressedWarnings = method.getAnnotation(SuppressWarnings.class); 99 if (suppressedWarnings == null 100 || !ImmutableSet.copyOf(suppressedWarnings.value()) 101 .contains("dagger.android.ScopedInjectorFactory")) { 102 AnnotationMirror mapKeyAnnotation = 103 getOnlyElement(getAnnotatedAnnotations(method, MapKey.class)); 104 TypeElement mapKeyValueElement = 105 elements.getTypeElement(injectedTypeFromMapKey(mapKeyAnnotation).get()); 106 messager.printMessage( 107 Kind.ERROR, 108 String.format( 109 "%s bindings should not be scoped. Scoping this method may leak instances of %s.", 110 AndroidInjector.Factory.class.getCanonicalName(), 111 mapKeyValueElement.getQualifiedName()), 112 method); 113 } 114 } 115 116 validateReturnType(method); 117 118 // @Binds methods should only have one parameter, but we can't guarantee the order of Processors 119 // in javac, so do a basic check for valid form 120 if (isAnnotationPresent(method, Binds.class) && method.getParameters().size() == 1) { 121 validateMapKeyMatchesBindsParameter(annotation, method); 122 } 123 } 124 125 /** Report an error if the method's return type is not {@code AndroidInjector.Factory<?>}. */ validateReturnType(ExecutableElement method)126 private void validateReturnType(ExecutableElement method) { 127 TypeMirror returnType = method.getReturnType(); 128 DeclaredType requiredReturnType = injectorFactoryOf(types.getWildcardType(null, null)); 129 130 if (!types.isSameType(returnType, requiredReturnType)) { 131 messager.printMessage( 132 Kind.ERROR, 133 String.format( 134 "%s should bind %s, not %s. See https://dagger.dev/android", 135 method, requiredReturnType, returnType), 136 method); 137 } 138 } 139 140 /** 141 * A valid @Binds method could bind an {@link AndroidInjector.Factory} for one type, while giving 142 * it a map key of a different type. The return type and parameter type would pass typical @Binds 143 * validation, but the map lookup in {@link dagger.android.DispatchingAndroidInjector} would 144 * retrieve the wrong injector factory. 145 * 146 * <pre>{@code 147 * {@literal @Binds} 148 * {@literal @IntoMap} 149 * {@literal @ClassKey(GreenActivity.class)} 150 * abstract AndroidInjector.Factory<?> bindBlueActivity( 151 * BlueActivityComponent.Builder builder); 152 * }</pre> 153 */ validateMapKeyMatchesBindsParameter( Class<? extends Annotation> annotation, ExecutableElement method)154 private void validateMapKeyMatchesBindsParameter( 155 Class<? extends Annotation> annotation, ExecutableElement method) { 156 TypeMirror parameterType = getOnlyElement(method.getParameters()).asType(); 157 AnnotationMirror annotationMirror = getAnnotationMirror(method, annotation).get(); 158 TypeMirror mapKeyType = 159 elements.getTypeElement(injectedTypeFromMapKey(annotationMirror).get()).asType(); 160 if (!types.isAssignable(parameterType, injectorFactoryOf(mapKeyType))) { 161 messager.printMessage( 162 Kind.ERROR, 163 String.format("%s does not implement AndroidInjector<%s>", parameterType, mapKeyType), 164 method, 165 annotationMirror); 166 } 167 } 168 169 /** Returns a {@link DeclaredType} for {@code AndroidInjector.Factory<implementationType>}. */ injectorFactoryOf(TypeMirror implementationType)170 private DeclaredType injectorFactoryOf(TypeMirror implementationType) { 171 return types.getDeclaredType(factoryElement(), implementationType); 172 } 173 factoryElement()174 private TypeElement factoryElement() { 175 return elements.getTypeElement(AndroidInjector.Factory.class.getCanonicalName()); 176 } 177 } 178