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