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