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