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.processor.internal.root; 18 19 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; 20 import static java.util.stream.Collectors.joining; 21 import static javax.lang.model.element.Modifier.FINAL; 22 import static javax.lang.model.element.Modifier.PRIVATE; 23 import static javax.lang.model.element.Modifier.PROTECTED; 24 import static javax.lang.model.element.Modifier.PUBLIC; 25 import static javax.lang.model.element.Modifier.STATIC; 26 import static javax.lang.model.util.ElementFilter.constructorsIn; 27 28 import com.google.common.collect.ImmutableSet; 29 import com.squareup.javapoet.AnnotationSpec; 30 import com.squareup.javapoet.ClassName; 31 import com.squareup.javapoet.CodeBlock; 32 import com.squareup.javapoet.JavaFile; 33 import com.squareup.javapoet.MethodSpec; 34 import com.squareup.javapoet.TypeSpec; 35 import dagger.hilt.processor.internal.ClassNames; 36 import dagger.hilt.processor.internal.ComponentNames; 37 import dagger.hilt.processor.internal.Processors; 38 import java.io.IOException; 39 import java.util.List; 40 import javax.annotation.processing.ProcessingEnvironment; 41 import javax.lang.model.element.ExecutableElement; 42 import javax.lang.model.element.TypeElement; 43 44 /** Generates an implementation of {@link dagger.hilt.android.internal.TestComponentData}. */ 45 public final class TestComponentDataGenerator { 46 private final ProcessingEnvironment processingEnv; 47 private final TypeElement originatingElement; 48 private final RootMetadata rootMetadata; 49 private final ClassName name; 50 private final ComponentNames componentNames; 51 TestComponentDataGenerator( ProcessingEnvironment processingEnv, TypeElement originatingElement, RootMetadata rootMetadata, ComponentNames componentNames)52 public TestComponentDataGenerator( 53 ProcessingEnvironment processingEnv, 54 TypeElement originatingElement, 55 RootMetadata rootMetadata, 56 ComponentNames componentNames) { 57 this.processingEnv = processingEnv; 58 this.originatingElement = originatingElement; 59 this.rootMetadata = rootMetadata; 60 this.componentNames = componentNames; 61 this.name = 62 Processors.append( 63 Processors.getEnclosedClassName(rootMetadata.testRootMetadata().testName()), 64 "_TestComponentDataSupplier"); 65 } 66 67 /** 68 * 69 * 70 * <pre><code>{@code 71 * public final class FooTest_TestComponentDataSupplier extends TestComponentDataSupplier { 72 * @Override 73 * protected TestComponentData get() { 74 * return new TestComponentData( 75 * false, // waitForBindValue 76 * testInstance -> injectInternal(($1T) testInstance), 77 * Arrays.asList(FooTest.TestModule.class, ...), 78 * modules -> 79 * DaggerFooTest_ApplicationComponent.builder() 80 * .applicationContextModule( 81 * new ApplicationContextModule( 82 * Contexts.getApplication(ApplicationProvider.getApplicationContext()))) 83 * .testModule((FooTest.TestModule) modules.get(FooTest.TestModule.class)) 84 * .testModule(modules.containsKey(FooTest.TestModule.class) 85 * ? (FooTest.TestModule) modules.get(FooTest.TestModule.class) 86 * : ((TestInstace) testInstance).new TestModule()) 87 * .build()); 88 * } 89 * } 90 * }</code></pre> 91 */ generate()92 public void generate() throws IOException { 93 TypeSpec.Builder generator = 94 TypeSpec.classBuilder(name) 95 .addOriginatingElement(originatingElement) 96 .superclass(ClassNames.TEST_COMPONENT_DATA_SUPPLIER) 97 .addModifiers(PUBLIC, FINAL) 98 .addMethod(getMethod()) 99 .addMethod(getTestInjectInternalMethod()); 100 101 Processors.addGeneratedAnnotation( 102 generator, processingEnv, ClassNames.ROOT_PROCESSOR.toString()); 103 104 JavaFile.builder(name.packageName(), generator.build()) 105 .build() 106 .writeTo(processingEnv.getFiler()); 107 } 108 getMethod()109 private MethodSpec getMethod() { 110 TypeElement testElement = rootMetadata.testRootMetadata().testElement(); 111 ClassName component = 112 componentNames.generatedComponent( 113 ClassName.get(testElement), ClassNames.SINGLETON_COMPONENT); 114 ImmutableSet<TypeElement> daggerRequiredModules = 115 rootMetadata.modulesThatDaggerCannotConstruct(ClassNames.SINGLETON_COMPONENT); 116 ImmutableSet<TypeElement> hiltRequiredModules = 117 daggerRequiredModules.stream() 118 .filter(module -> !canBeConstructedByHilt(module, testElement)) 119 .collect(toImmutableSet()); 120 121 return MethodSpec.methodBuilder("get") 122 .addModifiers(PROTECTED) 123 .returns(ClassNames.TEST_COMPONENT_DATA) 124 .addStatement( 125 "return new $T($L, $L, $L, $L, $L)", 126 ClassNames.TEST_COMPONENT_DATA, 127 rootMetadata.waitForBindValue(), 128 CodeBlock.of("testInstance -> injectInternal(($1T) testInstance)", testElement), 129 getElementsListed(daggerRequiredModules), 130 getElementsListed(hiltRequiredModules), 131 CodeBlock.of( 132 "(modules, testInstance, autoAddModuleEnabled) -> $T.builder()\n" 133 + ".applicationContextModule(\n" 134 + " new $T($T.getApplication($T.getApplicationContext())))\n" 135 + "$L" 136 + ".build()", 137 Processors.prepend(Processors.getEnclosedClassName(component), "Dagger"), 138 ClassNames.APPLICATION_CONTEXT_MODULE, 139 ClassNames.CONTEXTS, 140 ClassNames.APPLICATION_PROVIDER, 141 daggerRequiredModules.stream() 142 .map(module -> getAddModuleStatement(module, testElement)) 143 .collect(joining("\n")))) 144 .build(); 145 } 146 147 /** 148 * 149 * 150 * <pre><code> 151 * .testModule(modules.get(FooTest.TestModule.class)) 152 * </code></pre> 153 * 154 * <pre><code> 155 * .testModule(autoAddModuleEnabled 156 * ? ((FooTest) testInstance).new TestModule() 157 * : (FooTest.TestModule) modules.get(FooTest.TestModule.class)) 158 * </code></pre> 159 */ getAddModuleStatement(TypeElement module, TypeElement testElement)160 private static String getAddModuleStatement(TypeElement module, TypeElement testElement) { 161 ClassName className = ClassName.get(module); 162 return canBeConstructedByHilt(module, testElement) 163 ? CodeBlock.of( 164 ".$1L(autoAddModuleEnabled\n" 165 // testInstance can never be null if we reach here, because this flag can be 166 // turned on only when testInstance is not null 167 + " ? (($3T) testInstance).new $4L()\n" 168 + " : ($2T) modules.get($2T.class))", 169 Processors.upperToLowerCamel(className.simpleName()), 170 className, 171 className.enclosingClassName(), 172 className.simpleName()) 173 .toString() 174 : CodeBlock.of( 175 ".$1L(($2T) modules.get($2T.class))", 176 Processors.upperToLowerCamel(className.simpleName()), 177 className) 178 .toString(); 179 } 180 canBeConstructedByHilt(TypeElement module, TypeElement testElement)181 private static boolean canBeConstructedByHilt(TypeElement module, TypeElement testElement) { 182 return hasOnlyAccessibleNoArgConstructor(module) 183 && module.getEnclosingElement().equals(testElement); 184 } 185 hasOnlyAccessibleNoArgConstructor(TypeElement module)186 private static boolean hasOnlyAccessibleNoArgConstructor(TypeElement module) { 187 List<ExecutableElement> declaredConstructors = constructorsIn(module.getEnclosedElements()); 188 return declaredConstructors.isEmpty() 189 || (declaredConstructors.size() == 1 190 && !declaredConstructors.get(0).getModifiers().contains(PRIVATE) 191 && declaredConstructors.get(0).getParameters().isEmpty()); 192 } 193 194 /* Arrays.asList(FooTest.TestModule.class, ...) */ getElementsListed(ImmutableSet<TypeElement> modules)195 private static CodeBlock getElementsListed(ImmutableSet<TypeElement> modules) { 196 return modules.isEmpty() 197 ? CodeBlock.of("$T.emptySet()", ClassNames.COLLECTIONS) 198 : CodeBlock.of( 199 "new $T<>($T.asList($L))", 200 ClassNames.HASH_SET, 201 ClassNames.ARRAYS, 202 modules.stream() 203 .map(module -> CodeBlock.of("$T.class", module).toString()) 204 .collect(joining(","))); 205 } 206 getTestInjectInternalMethod()207 private MethodSpec getTestInjectInternalMethod() { 208 TypeElement testElement = rootMetadata.testRootMetadata().testElement(); 209 ClassName testName = ClassName.get(testElement); 210 return MethodSpec.methodBuilder("injectInternal") 211 .addModifiers(PRIVATE, STATIC) 212 .addParameter(testName, "testInstance") 213 .addAnnotation( 214 AnnotationSpec.builder(SuppressWarnings.class) 215 .addMember("value", "$S", "unchecked") 216 .build()) 217 .addStatement(callInjectTest(testElement)) 218 .build(); 219 } 220 callInjectTest(TypeElement testElement)221 private CodeBlock callInjectTest(TypeElement testElement) { 222 return CodeBlock.of( 223 "(($T) (($T) $T.getApplication($T.getApplicationContext())).generatedComponent()).injectTest(testInstance)", 224 rootMetadata.testRootMetadata().testInjectorName(), 225 ClassNames.GENERATED_COMPONENT_MANAGER, 226 ClassNames.CONTEXTS, 227 ClassNames.APPLICATION_PROVIDER); 228 } 229 } 230