• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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