blob: 284f8cdeee74fde67b1b92de44fa1a67aed7f017 [file] [log] [blame]
/*
* Copyright (C) 2020 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dagger.hilt.processor.internal.root;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import static java.util.stream.Collectors.joining;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.lang.model.util.ElementFilter.constructorsIn;
import com.google.common.collect.ImmutableSet;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import dagger.hilt.processor.internal.ClassNames;
import dagger.hilt.processor.internal.ComponentNames;
import dagger.hilt.processor.internal.Processors;
import java.io.IOException;
import java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
/** Generates an implementation of {@link dagger.hilt.android.internal.TestComponentData}. */
public final class TestComponentDataGenerator {
private final ProcessingEnvironment processingEnv;
private final RootMetadata rootMetadata;
private final ClassName name;
public TestComponentDataGenerator(
ProcessingEnvironment processingEnv,
RootMetadata rootMetadata) {
this.processingEnv = processingEnv;
this.rootMetadata = rootMetadata;
this.name =
Processors.append(
Processors.getEnclosedClassName(rootMetadata.testRootMetadata().testName()),
"_ComponentDataHolder");
}
/**
*
*
* <pre><code>{@code
* public final class FooTest_ComponentDataHolder {
* public static TestComponentData get() {
* return new TestComponentData(
* false, // waitForBindValue
* testInstance -> injectInternal(($1T) testInstance),
* Arrays.asList(FooTest.TestModule.class, ...),
* modules ->
* DaggerFooTest_ApplicationComponent.builder()
* .applicationContextModule(
* new ApplicationContextModule(ApplicationProvider.getApplicationContext()))
* .testModule((FooTest.TestModule) modules.get(FooTest.TestModule.class))
* .testModule(modules.containsKey(FooTest.TestModule.class)
* ? (FooTest.TestModule) modules.get(FooTest.TestModule.class)
* : ((TestInstace) testInstance).new TestModule())
* .build());
* }
* }
* }</code></pre>
*/
public void generate() throws IOException {
TypeSpec.Builder generator =
TypeSpec.classBuilder(name)
.addModifiers(PUBLIC, FINAL)
.addMethod(MethodSpec.constructorBuilder().addModifiers(PRIVATE).build())
.addMethod(getMethod())
.addMethod(getTestInjectInternalMethod());
Processors.addGeneratedAnnotation(
generator, processingEnv, ClassNames.ROOT_PROCESSOR.toString());
JavaFile.builder(rootMetadata.testRootMetadata().testName().packageName(), generator.build())
.build()
.writeTo(processingEnv.getFiler());
}
private MethodSpec getMethod() {
TypeElement testElement = rootMetadata.testRootMetadata().testElement();
ClassName component =
ComponentNames.generatedComponent(
ClassName.get(testElement), ClassNames.SINGLETON_COMPONENT);
ImmutableSet<TypeElement> daggerRequiredModules =
rootMetadata.modulesThatDaggerCannotConstruct(ClassNames.SINGLETON_COMPONENT);
ImmutableSet<TypeElement> hiltRequiredModules =
daggerRequiredModules.stream()
.filter(module -> !canBeConstructedByHilt(module, testElement))
.collect(toImmutableSet());
return MethodSpec.methodBuilder("get")
.addModifiers(PUBLIC, STATIC)
.returns(ClassNames.TEST_COMPONENT_DATA)
.addStatement(
"return new $T($L, $L, $L, $L, $L)",
ClassNames.TEST_COMPONENT_DATA,
rootMetadata.waitForBindValue(),
CodeBlock.of("testInstance -> injectInternal(($1T) testInstance)", testElement),
getElementsListed(daggerRequiredModules),
getElementsListed(hiltRequiredModules),
CodeBlock.of(
"(modules, testInstance, autoAddModuleEnabled) -> $T.builder()\n"
+ ".applicationContextModule(new $T($T.getApplicationContext()))\n"
+ "$L"
+ ".build()",
Processors.prepend(Processors.getEnclosedClassName(component), "Dagger"),
ClassNames.APPLICATION_CONTEXT_MODULE,
ClassNames.APPLICATION_PROVIDER,
daggerRequiredModules.stream()
.map(module -> getAddModuleStatement(module, testElement))
.collect(joining("\n"))))
.build();
}
/**
*
*
* <pre><code>
* .testModule(modules.get(FooTest.TestModule.class))
* </code></pre>
*
* <pre><code>
* .testModule(autoAddModuleEnabled
* ? ((FooTest) testInstance).new TestModule()
* : (FooTest.TestModule) modules.get(FooTest.TestModule.class))
* </code></pre>
*/
private static String getAddModuleStatement(TypeElement module, TypeElement testElement) {
ClassName className = ClassName.get(module);
return canBeConstructedByHilt(module, testElement)
? CodeBlock.of(
".$1L(autoAddModuleEnabled\n"
// testInstance can never be null if we reach here, because this flag can be
// turned on only when testInstance is not null
+ " ? (($3T) testInstance).new $4L()\n"
+ " : ($2T) modules.get($2T.class))",
Processors.upperToLowerCamel(className.simpleName()),
className,
className.enclosingClassName(),
className.simpleName())
.toString()
: CodeBlock.of(
".$1L(($2T) modules.get($2T.class))",
Processors.upperToLowerCamel(className.simpleName()),
className)
.toString();
}
private static boolean canBeConstructedByHilt(TypeElement module, TypeElement testElement) {
return hasOnlyAccessibleNoArgConstructor(module)
&& module.getEnclosingElement().equals(testElement);
}
private static boolean hasOnlyAccessibleNoArgConstructor(TypeElement module) {
List<ExecutableElement> declaredConstructors = constructorsIn(module.getEnclosedElements());
return declaredConstructors.isEmpty()
|| (declaredConstructors.size() == 1
&& !declaredConstructors.get(0).getModifiers().contains(PRIVATE)
&& declaredConstructors.get(0).getParameters().isEmpty());
}
/* Arrays.asList(FooTest.TestModule.class, ...) */
private static CodeBlock getElementsListed(ImmutableSet<TypeElement> modules) {
return modules.isEmpty()
? CodeBlock.of("$T.emptySet()", ClassNames.COLLECTIONS)
: CodeBlock.of(
"new $T<>($T.asList($L))",
ClassNames.HASH_SET,
ClassNames.ARRAYS,
modules.stream()
.map(module -> CodeBlock.of("$T.class", module).toString())
.collect(joining(",")));
}
private MethodSpec getTestInjectInternalMethod() {
TypeElement testElement = rootMetadata.testRootMetadata().testElement();
ClassName testName = ClassName.get(testElement);
return MethodSpec.methodBuilder("injectInternal")
.addModifiers(PRIVATE, STATIC)
.addParameter(testName, "testInstance")
.addAnnotation(
AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "unchecked")
.build())
.addStatement("$L.injectTest(testInstance)", getInjector(testElement))
.build();
}
private static CodeBlock getInjector(TypeElement testElement) {
return CodeBlock.of(
"(($T) (($T) $T.getApplicationContext()).generatedComponent())",
ClassNames.TEST_INJECTOR,
ClassNames.GENERATED_COMPONENT_MANAGER,
ClassNames.APPLICATION_PROVIDER);
}
}