• 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.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