1 /* 2 * Copyright (C) 2020 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.hilt.android.processor.internal.bindvalue; 18 19 import static com.google.common.base.CaseFormat.LOWER_CAMEL; 20 import static com.google.common.base.CaseFormat.UPPER_CAMEL; 21 import static java.util.Comparator.comparing; 22 23 import com.google.common.collect.ImmutableSet; 24 import com.squareup.javapoet.AnnotationSpec; 25 import com.squareup.javapoet.ClassName; 26 import com.squareup.javapoet.CodeBlock; 27 import com.squareup.javapoet.JavaFile; 28 import com.squareup.javapoet.MethodSpec; 29 import com.squareup.javapoet.ParameterSpec; 30 import com.squareup.javapoet.TypeSpec; 31 import dagger.Module; 32 import dagger.Provides; 33 import dagger.hilt.android.processor.internal.bindvalue.BindValueMetadata.BindValueElement; 34 import dagger.hilt.processor.internal.ClassNames; 35 import dagger.hilt.processor.internal.Components; 36 import dagger.hilt.processor.internal.Processors; 37 import dagger.multibindings.ElementsIntoSet; 38 import dagger.multibindings.IntoMap; 39 import dagger.multibindings.IntoSet; 40 import java.io.IOException; 41 import javax.annotation.processing.ProcessingEnvironment; 42 import javax.lang.model.element.Modifier; 43 44 /** 45 * Generates a SINGLETON module for all {@code BindValue} annotated fields in a test class. 46 */ 47 final class BindValueGenerator { 48 private static final String SUFFIX = "_BindValueModule"; 49 50 private final ProcessingEnvironment env; 51 private final BindValueMetadata metadata; 52 private final ClassName testClassName; 53 private final ClassName className; 54 BindValueGenerator(ProcessingEnvironment env, BindValueMetadata metadata)55 BindValueGenerator(ProcessingEnvironment env, BindValueMetadata metadata) { 56 this.env = env; 57 this.metadata = metadata; 58 testClassName = ClassName.get(metadata.testElement()); 59 className = Processors.append(testClassName, SUFFIX); 60 } 61 62 // @Module 63 // @InstallIn(SingletonComponent.class) 64 // public final class FooTest_BindValueModule { 65 // // providesMethods ... 66 // } generate()67 void generate() throws IOException { 68 TypeSpec.Builder builder = 69 TypeSpec.classBuilder(className) 70 .addOriginatingElement(metadata.testElement()) 71 .addAnnotation(Processors.getOriginatingElementAnnotation(metadata.testElement())) 72 .addModifiers(Modifier.PUBLIC, Modifier.FINAL) 73 .addAnnotation(Module.class) 74 .addAnnotation( 75 Components.getInstallInAnnotationSpec( 76 ImmutableSet.of(ClassNames.SINGLETON_COMPONENT))) 77 .addMethod(providesTestMethod()); 78 79 Processors.addGeneratedAnnotation(builder, env, getClass()); 80 81 metadata.bindValueElements().stream() 82 .map(this::providesMethod) 83 .sorted(comparing(MethodSpec::toString)) 84 .forEachOrdered(builder::addMethod); 85 86 JavaFile.builder(className.packageName(), builder.build()) 87 .build() 88 .writeTo(env.getFiler()); 89 } 90 91 // @Provides 92 // static FooTest providesFooTest(@ApplicationContext Context context) { 93 // return (FooTest) 94 // ((TestApplicationComponentManager) 95 // ((TestApplicationComponentManagerHolder) context).componentManager()) 96 // .getTestInstance(); 97 // } providesTestMethod()98 private MethodSpec providesTestMethod() { 99 String methodName = "provides" + testClassName.simpleName(); 100 MethodSpec.Builder builder = 101 MethodSpec.methodBuilder(methodName) 102 .addAnnotation(Provides.class) 103 .addModifiers(Modifier.STATIC) 104 .addParameter( 105 ParameterSpec.builder(ClassNames.CONTEXT, "context") 106 .addAnnotation(ClassNames.APPLICATION_CONTEXT) 107 .build()) 108 .returns(testClassName) 109 .addStatement( 110 "return ($T) (($T) (($T) context).componentManager()).getTestInstance()", 111 testClassName, 112 ClassNames.TEST_APPLICATION_COMPONENT_MANAGER, 113 ClassNames.TEST_APPLICATION_COMPONENT_MANAGER_HOLDER); 114 return builder.build(); 115 } 116 117 // @Provides 118 // @BarQualifier 119 // static Bar providesBarQualifierBar(FooTest test) { 120 // return test.bar; 121 // } providesMethod(BindValueElement bindValue)122 private MethodSpec providesMethod(BindValueElement bindValue) { 123 // We only allow fields in the Test class, which should have unique variable names. 124 String methodName = "provides" 125 + LOWER_CAMEL.to(UPPER_CAMEL, bindValue.variableElement().getSimpleName().toString()); 126 MethodSpec.Builder builder = 127 MethodSpec.methodBuilder(methodName) 128 .addAnnotation(Provides.class) 129 .addModifiers(Modifier.STATIC) 130 .returns(ClassName.get(bindValue.variableElement().asType())); 131 132 if (bindValue.variableElement().getModifiers().contains(Modifier.STATIC)) { 133 builder.addStatement( 134 "return $T.$L", testClassName, bindValue.variableElement().getSimpleName()); 135 } else { 136 builder 137 .addParameter(testClassName, "test") 138 .addStatement( 139 "return $L", 140 bindValue.getterElement().isPresent() 141 ? CodeBlock.of("test.$L()", bindValue.getterElement().get().getSimpleName()) 142 : CodeBlock.of("test.$L", bindValue.variableElement().getSimpleName())); 143 } 144 145 ClassName annotationClassName = bindValue.annotationName(); 146 if (BindValueMetadata.BIND_VALUE_INTO_MAP_ANNOTATIONS.contains(annotationClassName)) { 147 builder.addAnnotation(IntoMap.class); 148 // It is safe to call get() on the Optional<AnnotationMirror> returned by mapKey() 149 // because a @BindValueIntoMap is required to have one and is checked in 150 // BindValueMetadata.BindValueElement.create(). 151 builder.addAnnotation(AnnotationSpec.get(bindValue.mapKey().get())); 152 } else if (BindValueMetadata.BIND_VALUE_INTO_SET_ANNOTATIONS.contains(annotationClassName)) { 153 builder.addAnnotation(IntoSet.class); 154 } else if (BindValueMetadata.BIND_ELEMENTS_INTO_SET_ANNOTATIONS.contains(annotationClassName)) { 155 builder.addAnnotation(ElementsIntoSet.class); 156 } 157 bindValue.qualifier().ifPresent(q -> builder.addAnnotation(AnnotationSpec.get(q))); 158 return builder.build(); 159 } 160 } 161