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