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