blob: c1fdc435814fc54044657055ab1cfe93659019c5 [file] [log] [blame]
/*
* Copyright (C) 2017 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.internal.codegen;
import static com.squareup.javapoet.MethodSpec.constructorBuilder;
import static com.squareup.javapoet.MethodSpec.methodBuilder;
import static com.squareup.javapoet.TypeSpec.classBuilder;
import static dagger.internal.codegen.SourceFiles.simpleVariableName;
import static dagger.internal.codegen.TypeSpecs.addSupertype;
import static javax.lang.model.element.Modifier.ABSTRACT;
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.type.TypeKind.VOID;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import dagger.internal.Preconditions;
import dagger.internal.codegen.ComponentDescriptor.BuilderRequirementMethod;
import dagger.internal.codegen.ComponentDescriptor.BuilderSpec;
import java.util.Optional;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
/** Models the generated code for a component builder. */
final class GeneratedComponentBuilderModel {
private final TypeSpec typeSpec;
private final ClassName name;
private final ImmutableMap<ComponentRequirement, FieldSpec> builderFields;
private GeneratedComponentBuilderModel(
TypeSpec typeSpec,
ClassName name,
ImmutableMap<ComponentRequirement, FieldSpec> builderFields) {
this.typeSpec = typeSpec;
this.name = name;
this.builderFields = builderFields;
}
TypeSpec typeSpec() {
return typeSpec;
}
ClassName name() {
return name;
}
ImmutableMap<ComponentRequirement, FieldSpec> builderFields() {
return builderFields;
}
static Optional<GeneratedComponentBuilderModel> create(
GeneratedComponentModel generatedComponentModel,
BindingGraph graph,
Elements elements,
Types types) {
return hasBuilder(graph.componentDescriptor())
? Optional.of(new Creator(generatedComponentModel, graph, elements, types).create())
: Optional.empty();
}
private static boolean hasBuilder(ComponentDescriptor component) {
return component.kind().isTopLevel() || component.builderSpec().isPresent();
}
private static final class Creator {
private static final String NOOP_BUILDER_METHOD_JAVADOC =
"This module is declared, but an instance is not used in the component. This method is a "
+ "no-op. For more, see https://google.github.io/dagger/unused-modules.\n";
private final BindingGraph graph;
private final TypeSpec.Builder builder;
private final GeneratedComponentModel generatedComponentModel;
private final Elements elements;
private final Types types;
Creator(
GeneratedComponentModel generatedComponentModel,
BindingGraph graph,
Elements elements,
Types types) {
this.generatedComponentModel = generatedComponentModel;
this.builder = classBuilder(generatedComponentModel.getBuilderName());
this.graph = graph;
this.elements = elements;
this.types = types;
}
GeneratedComponentBuilderModel create() {
if (!generatedComponentModel.isNested()) {
builder.addModifiers(STATIC);
}
if (builderSpec().isPresent()) {
if (generatedComponentModel.isAbstract()) {
builder.addModifiers(PUBLIC);
} else {
builder.addModifiers(PRIVATE);
}
setSupertype();
} else {
builder.addModifiers(PUBLIC).addMethod(constructorBuilder().addModifiers(PRIVATE).build());
}
ImmutableMap<ComponentRequirement, FieldSpec> builderFields = builderFields(graph);
if (generatedComponentModel.isAbstract()) {
builder.addModifiers(ABSTRACT);
} else {
builder.addModifiers(FINAL);
builder.addMethod(buildMethod(builderFields)); // Can only instantiate concrete classes.
}
builder
.addFields(builderFields.values())
// TODO(ronshapiro): this should be switched with buildMethod(), but that currently breaks
// compile-testing tests that rely on the order of the methods
.addMethods(builderMethods(builderFields));
return new GeneratedComponentBuilderModel(
builder.build(), generatedComponentModel.getBuilderName(), builderFields);
}
/** Set the superclass being extended or interface being implemented for this builder. */
private void setSupertype() {
if (generatedComponentModel.supermodel().isPresent()) {
// If there's a superclass, extend the Builder defined there.
builder.superclass(generatedComponentModel.supermodel().get().getBuilderName());
} else {
addSupertype(builder, builderSpec().get().builderDefinitionType());
}
}
/**
* Computes fields for each of the {@linkplain BindingGraph#componentRequirements component
* requirements}. Regardless of builder spec, there is always one field per requirement.
*/
private static ImmutableMap<ComponentRequirement, FieldSpec> builderFields(BindingGraph graph) {
UniqueNameSet fieldNames = new UniqueNameSet();
ImmutableMap.Builder<ComponentRequirement, FieldSpec> builderFields = ImmutableMap.builder();
for (ComponentRequirement componentRequirement : graph.componentRequirements()) {
String name = fieldNames.getUniqueName(componentRequirement.variableName());
builderFields.put(
componentRequirement,
FieldSpec.builder(TypeName.get(componentRequirement.type()), name, PRIVATE).build());
}
return builderFields.build();
}
private MethodSpec buildMethod(ImmutableMap<ComponentRequirement, FieldSpec> builderFields) {
MethodSpec.Builder buildMethod;
if (builderSpec().isPresent()) {
ExecutableElement specBuildMethod = builderSpec().get().buildMethod();
// Note: we don't use the specBuildMethod.getReturnType() as the return type
// because it might be a type variable. We make use of covariant returns to allow
// us to return the component type, which will always be valid.
buildMethod =
methodBuilder(specBuildMethod.getSimpleName().toString()).addAnnotation(Override.class);
} else {
buildMethod = methodBuilder("build");
}
buildMethod.returns(ClassName.get(graph.componentType())).addModifiers(PUBLIC);
builderFields.forEach(
(requirement, field) -> {
switch (requirement.nullPolicy(elements, types)) {
case NEW:
buildMethod
.beginControlFlow("if ($N == null)", field)
.addStatement("this.$N = new $T()", field, field.type)
.endControlFlow();
break;
case THROW:
buildMethod
.beginControlFlow("if ($N == null)", field)
.addStatement(
"throw new $T($T.class.getCanonicalName() + $S)",
IllegalStateException.class,
TypeNames.rawTypeName(field.type),
" must be set")
.endControlFlow();
break;
case ALLOW:
break;
default:
throw new AssertionError(requirement);
}
});
buildMethod.addStatement("return new $T(this)", generatedComponentModel.name());
return buildMethod.build();
}
/**
* Computes the methods that set each of parameters on the builder. If the {@link BuilderSpec}
* is present, it will tailor the methods to match the spec.
*/
private ImmutableSet<MethodSpec> builderMethods(
ImmutableMap<ComponentRequirement, FieldSpec> builderFields) {
ImmutableSet<ComponentRequirement> componentRequirements = graph.componentRequirements();
ImmutableSet.Builder<MethodSpec> methods = ImmutableSet.builder();
if (builderSpec().isPresent()) {
UniqueNameSet parameterNames = new UniqueNameSet();
for (BuilderRequirementMethod requirementMethod :
builderSpec().get().requirementMethods()) {
ComponentRequirement builderRequirement = requirementMethod.requirement();
ExecutableElement specMethod = requirementMethod.method();
MethodSpec.Builder builderMethod = addBuilderMethodFromSpec(specMethod);
VariableElement parameterElement = Iterables.getOnlyElement(specMethod.getParameters());
String parameterName = parameterNames.getUniqueName(parameterElement.getSimpleName());
TypeName argType =
parameterElement.asType().getKind().isPrimitive()
// Primitives need to use the original (unresolved) type to avoid boxing.
? TypeName.get(parameterElement.asType())
// Otherwise we use the full resolved type.
: TypeName.get(builderRequirement.type());
builderMethod.addParameter(argType, parameterName);
if (componentRequirements.contains(builderRequirement)) {
// required type
builderMethod.addStatement(
"this.$N = $L",
builderFields.get(builderRequirement),
builderRequirement
.nullPolicy(elements, types)
.equals(ComponentRequirement.NullPolicy.ALLOW)
? parameterName
: CodeBlock.of("$T.checkNotNull($L)", Preconditions.class, parameterName));
addBuilderMethodReturnStatementForSpec(specMethod, builderMethod);
} else if (graph.ownedModuleTypes().contains(builderRequirement.typeElement())) {
// owned, but not required
builderMethod.addJavadoc(NOOP_BUILDER_METHOD_JAVADOC);
addBuilderMethodReturnStatementForSpec(specMethod, builderMethod);
} else {
// neither owned nor required, so it must be an inherited module
builderMethod.addStatement(
"throw new $T($T.format($S, $T.class.getCanonicalName()))",
UnsupportedOperationException.class,
String.class,
"%s cannot be set because it is inherited from the enclosing component",
TypeNames.rawTypeName(TypeName.get(builderRequirement.type())));
}
methods.add(builderMethod.build());
}
} else {
for (ComponentRequirement componentRequirement :
graph.componentDescriptor().availableDependencies()) {
String componentRequirementName = simpleVariableName(componentRequirement.typeElement());
MethodSpec.Builder builderMethod =
methodBuilder(componentRequirementName)
.returns(generatedComponentModel.getBuilderName())
.addModifiers(PUBLIC)
.addParameter(
TypeName.get(componentRequirement.type()), componentRequirementName);
if (componentRequirements.contains(componentRequirement)) {
builderMethod.addStatement(
"this.$N = $T.checkNotNull($L)",
builderFields.get(componentRequirement),
Preconditions.class,
componentRequirementName);
} else {
builderMethod.addStatement(
"$T.checkNotNull($L)", Preconditions.class, componentRequirementName);
builderMethod.addJavadoc("@deprecated " + NOOP_BUILDER_METHOD_JAVADOC);
builderMethod.addAnnotation(Deprecated.class);
}
builderMethod.addStatement("return this");
methods.add(builderMethod.build());
}
}
return methods.build();
}
private MethodSpec.Builder addBuilderMethodFromSpec(ExecutableElement method) {
TypeMirror returnType = method.getReturnType();
MethodSpec.Builder builderMethod =
methodBuilder(method.getSimpleName().toString())
.addAnnotation(Override.class)
.addModifiers(Sets.difference(method.getModifiers(), ImmutableSet.of(ABSTRACT)));
// If the return type is void, we add a method with the void return type.
// Otherwise we use the generated builder name and take advantage of covariant returns
// (so that we don't have to worry about setter methods that return type variables).
if (!returnType.getKind().equals(VOID)) {
builderMethod.returns(generatedComponentModel.getBuilderName());
}
return builderMethod;
}
private static void addBuilderMethodReturnStatementForSpec(
ExecutableElement specMethod, MethodSpec.Builder builderMethod) {
if (!specMethod.getReturnType().getKind().equals(VOID)) {
builderMethod.addStatement("return this");
}
}
private Optional<BuilderSpec> builderSpec() {
return graph.componentDescriptor().builderSpec();
}
}
}