diff --git a/java/dagger/internal/codegen/DelegateComponentProcessor.java b/java/dagger/internal/codegen/DelegateComponentProcessor.java index b3e2c2fd794..8f8d5040b7c 100644 --- a/java/dagger/internal/codegen/DelegateComponentProcessor.java +++ b/java/dagger/internal/codegen/DelegateComponentProcessor.java @@ -32,6 +32,7 @@ import dagger.internal.codegen.base.ClearableCache; import dagger.internal.codegen.base.SourceFileGenerationException; import dagger.internal.codegen.base.SourceFileGenerator; +import dagger.internal.codegen.base.SourceFileHjarGenerator; import dagger.internal.codegen.binding.BindingGraphFactory; import dagger.internal.codegen.binding.InjectBindingRegistry; import dagger.internal.codegen.binding.MembersInjectionBinding; @@ -53,7 +54,6 @@ import dagger.internal.codegen.validation.InjectValidator; import dagger.internal.codegen.validation.ValidationBindingGraphPlugins; import dagger.internal.codegen.writing.FactoryGenerator; -import dagger.internal.codegen.writing.HjarSourceFileGenerator; import dagger.internal.codegen.writing.MembersInjectorGenerator; import dagger.internal.codegen.writing.ModuleGenerator; import dagger.internal.codegen.writing.ModuleProxies.ModuleConstructorProxyGenerator; @@ -191,34 +191,44 @@ interface ProcessingRoundCacheModule { interface SourceFileGeneratorsModule { @Provides static SourceFileGenerator factoryGenerator( - FactoryGenerator generator, CompilerOptions compilerOptions) { - return hjarWrapper(generator, compilerOptions); + FactoryGenerator generator, + CompilerOptions compilerOptions, + XProcessingEnv processingEnv) { + return hjarWrapper(generator, compilerOptions, processingEnv); } @Provides static SourceFileGenerator producerFactoryGenerator( - ProducerFactoryGenerator generator, CompilerOptions compilerOptions) { - return hjarWrapper(generator, compilerOptions); + ProducerFactoryGenerator generator, + CompilerOptions compilerOptions, + XProcessingEnv processingEnv) { + return hjarWrapper(generator, compilerOptions, processingEnv); } @Provides static SourceFileGenerator membersInjectorGenerator( - MembersInjectorGenerator generator, CompilerOptions compilerOptions) { - return hjarWrapper(generator, compilerOptions); + MembersInjectorGenerator generator, + CompilerOptions compilerOptions, + XProcessingEnv processingEnv) { + return hjarWrapper(generator, compilerOptions, processingEnv); } @Provides @ModuleGenerator static SourceFileGenerator moduleConstructorProxyGenerator( - ModuleConstructorProxyGenerator generator, CompilerOptions compilerOptions) { - return hjarWrapper(generator, compilerOptions); + ModuleConstructorProxyGenerator generator, + CompilerOptions compilerOptions, + XProcessingEnv processingEnv) { + return hjarWrapper(generator, compilerOptions, processingEnv); } } private static SourceFileGenerator hjarWrapper( - SourceFileGenerator generator, CompilerOptions compilerOptions) { + SourceFileGenerator generator, + CompilerOptions compilerOptions, + XProcessingEnv processingEnv) { return compilerOptions.headerCompilation() - ? HjarSourceFileGenerator.wrap(generator) + ? SourceFileHjarGenerator.wrap(generator, processingEnv) : generator; } } diff --git a/java/dagger/internal/codegen/base/SourceFileHjarGenerator.java b/java/dagger/internal/codegen/base/SourceFileHjarGenerator.java new file mode 100644 index 00000000000..6857c366fa5 --- /dev/null +++ b/java/dagger/internal/codegen/base/SourceFileHjarGenerator.java @@ -0,0 +1,211 @@ +/* + * 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.base; + +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.extension.DaggerStreams.toImmutableList; +import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; +import static dagger.internal.codegen.langmodel.Accessibility.isElementAccessibleFrom; +import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement; +import static javax.lang.model.element.Modifier.PRIVATE; + +import androidx.room.compiler.processing.XConstructorElement; +import androidx.room.compiler.processing.XElement; +import androidx.room.compiler.processing.XExecutableParameterElement; +import androidx.room.compiler.processing.XProcessingEnv; +import androidx.room.compiler.processing.XType; +import androidx.room.compiler.processing.XTypeElement; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +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.codegen.javapoet.CodeBlocks; +import dagger.internal.codegen.javapoet.TypeNames; +import java.util.Optional; +import javax.lang.model.element.Modifier; + +/** + * A source file generator that only writes the relevant code necessary for Bazel to create a + * correct header (ABI) jar. + */ +public final class SourceFileHjarGenerator extends SourceFileGenerator { + public static SourceFileGenerator wrap( + SourceFileGenerator delegate, XProcessingEnv processingEnv) { + return new SourceFileHjarGenerator<>(delegate, processingEnv); + } + + private final SourceFileGenerator delegate; + private final XProcessingEnv processingEnv; + + private SourceFileHjarGenerator(SourceFileGenerator delegate, XProcessingEnv processingEnv) { + super(delegate); + this.delegate = delegate; + this.processingEnv = processingEnv; + } + + @Override + public XElement originatingElement(T input) { + return delegate.originatingElement(input); + } + + @Override + public ImmutableList topLevelTypes(T input) { + String packageName = closestEnclosingTypeElement(originatingElement(input)).getPackageName(); + return delegate.topLevelTypes(input).stream() + .map(completeType -> skeletonType(packageName, completeType.build())) + .collect(toImmutableList()); + } + + private TypeSpec.Builder skeletonType(String packageName, TypeSpec completeType) { + TypeSpec.Builder skeleton = + classBuilder(completeType.name) + .addSuperinterfaces(completeType.superinterfaces) + .addTypeVariables(completeType.typeVariables) + .addModifiers(completeType.modifiers.toArray(new Modifier[0])) + .addAnnotations(completeType.annotations); + + if (!completeType.superclass.equals(ClassName.OBJECT)) { + skeleton.superclass(completeType.superclass); + } + + completeType.methodSpecs.stream() + .filter(method -> !method.modifiers.contains(PRIVATE) || method.isConstructor()) + .map(completeMethod -> skeletonMethod(packageName, completeType, completeMethod)) + .forEach(skeleton::addMethod); + + completeType.fieldSpecs.stream() + .filter(field -> !field.modifiers.contains(PRIVATE)) + .map(this::skeletonField) + .forEach(skeleton::addField); + + completeType.typeSpecs.stream() + .map(type -> skeletonType(packageName, type).build()) + .forEach(skeleton::addType); + + completeType.alwaysQualifiedNames + .forEach(skeleton::alwaysQualify); + + return skeleton; + } + + private MethodSpec skeletonMethod( + String packageName, TypeSpec completeType, MethodSpec completeMethod) { + MethodSpec.Builder skeleton = + completeMethod.isConstructor() + ? constructorBuilder() + : methodBuilder(completeMethod.name).returns(completeMethod.returnType); + + if (completeMethod.isConstructor()) { + getRequiredSuperCall(packageName, completeType) + .ifPresent(superCall -> skeleton.addStatement("$L", superCall)); + } else if (!completeMethod.returnType.equals(TypeName.VOID)) { + skeleton.addStatement("return $L", getDefaultValueCodeBlock(completeMethod.returnType)); + } + + return skeleton + .addModifiers(completeMethod.modifiers) + .addTypeVariables(completeMethod.typeVariables) + .addParameters(completeMethod.parameters) + .addExceptions(completeMethod.exceptions) + .varargs(completeMethod.varargs) + .addAnnotations(completeMethod.annotations) + .build(); + } + + private Optional getRequiredSuperCall(String packageName, TypeSpec completeType) { + if (completeType.superclass.equals(TypeName.OBJECT)) { + return Optional.empty(); + } + + ClassName rawSuperClass = (ClassName) TypeNames.rawTypeName(completeType.superclass); + XTypeElement superTypeElement = + processingEnv.requireTypeElement(rawSuperClass.canonicalName()); + + ImmutableSet accessibleConstructors = + superTypeElement.getConstructors().stream() + .filter( + constructor -> + // isElementAccessibleFrom doesn't take protected into account so check manually + constructor.isProtected() + || isElementAccessibleFrom(constructor, packageName)) + .collect(toImmutableSet()); + + // If there's an accessible default constructor we don't need to call super() manually. + if (accessibleConstructors.isEmpty() + || accessibleConstructors.stream() + .anyMatch(constructor -> constructor.getParameters().isEmpty())) { + return Optional.empty(); + } + + return Optional.of( + CodeBlock.of( + "super($L)", + CodeBlocks.makeParametersCodeBlock( + // We just choose the first constructor (it doesn't really matter since we're just + // trying to ensure the constructor body compiles). + accessibleConstructors.stream().findFirst().get().getParameters().stream() + .map(XExecutableParameterElement::getType) + .map(XType::getTypeName) + .map(SourceFileHjarGenerator::getDefaultValueCodeBlock) + .collect(toImmutableList())))); + } + + /** + * Returns a {@link CodeBlock} containing the default value for the given {@code typeName}. + * + *

See https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html. + */ + private static CodeBlock getDefaultValueCodeBlock(TypeName typeName) { + if (typeName.isPrimitive()) { + if (typeName.equals(TypeName.BOOLEAN)) { + return CodeBlock.of("false"); + } else if (typeName.equals(TypeName.CHAR)) { + return CodeBlock.of("'\u0000'"); + } else if (typeName.equals(TypeName.BYTE)) { + return CodeBlock.of("0"); + } else if (typeName.equals(TypeName.SHORT)) { + return CodeBlock.of("0"); + } else if (typeName.equals(TypeName.INT)) { + return CodeBlock.of("0"); + } else if (typeName.equals(TypeName.LONG)) { + return CodeBlock.of("0L"); + } else if (typeName.equals(TypeName.FLOAT)) { + return CodeBlock.of("0.0f"); + } else if (typeName.equals(TypeName.DOUBLE)) { + return CodeBlock.of("0.0d"); + } else { + throw new AssertionError("Unexpected type: " + typeName); + } + } + return CodeBlock.of("null"); + } + + private FieldSpec skeletonField(FieldSpec completeField) { + return FieldSpec.builder( + completeField.type, + completeField.name, + completeField.modifiers.toArray(new Modifier[0])) + .addAnnotations(completeField.annotations) + .build(); + } +} diff --git a/java/dagger/internal/codegen/componentgenerator/ComponentGeneratorModule.java b/java/dagger/internal/codegen/componentgenerator/ComponentGeneratorModule.java index 179c411e401..a071eaeb25a 100644 --- a/java/dagger/internal/codegen/componentgenerator/ComponentGeneratorModule.java +++ b/java/dagger/internal/codegen/componentgenerator/ComponentGeneratorModule.java @@ -16,9 +16,12 @@ package dagger.internal.codegen.componentgenerator; +import androidx.room.compiler.processing.XProcessingEnv; import dagger.Binds; import dagger.Module; +import dagger.Provides; import dagger.internal.codegen.base.SourceFileGenerator; +import dagger.internal.codegen.base.SourceFileHjarGenerator; import dagger.internal.codegen.binding.BindingGraph; import dagger.internal.codegen.binding.ComponentDescriptor; @@ -29,14 +32,23 @@ public interface ComponentGeneratorModule { @Binds abstract SourceFileGenerator componentGenerator(ComponentGenerator generator); - // The HjarSourceFileGenerator wrapper first generates the entire TypeSpec before stripping out + // The SourceFileHjarGenerator wrapper first generates the entire TypeSpec before stripping out // things that aren't needed for the hjar. However, this can be really expensive for the component // because it is usually the most expensive file to generate, and most of its content is not - // needed in the hjar. Thus, instead of wrapping the ComponentGenerator in HjarSourceFileGenerator - // we provide a completely separate processing step, ComponentHjarProcessingStep, and generator, - // ComponentHjarGenerator, for when generating hjars for components, which can avoid generating - // the parts of the component that would have been stripped out by the HjarSourceFileGenerator. - @Binds - abstract SourceFileGenerator componentHjarGenerator( - ComponentHjarGenerator hjarGenerator); + // needed in the hjar. Thus, we provide a completely separate processing step, + // ComponentHjarProcessingStep and ComponentHjarGenerator, for when generating hjars for + // components, which can avoid generating the parts of the component that would have been stripped + // out by the HjarSourceFileGenerator anyway. Note that we still wrap ComponentHjarGenerator in + // SourceFileHjarGenerator because it adds in constructor and method bodies that are needed for + // Javac to compile correctly, e.g. super(...) calls in the constructor and return statements in + // methods. + @Provides + static SourceFileGenerator componentHjarGenerator( + XProcessingEnv processingEnv, + ComponentHjarGenerator hjarGenerator) { + // Note: technically the ComponentHjarGenerator is already in hjar form, but the + // SourceFileHjarGenerator wrapper adds in proper method bodies, e.g. constructors that require + // super() calls or methods that require return statements. + return SourceFileHjarGenerator.wrap(hjarGenerator, processingEnv); + } } diff --git a/java/dagger/internal/codegen/writing/HjarSourceFileGenerator.java b/java/dagger/internal/codegen/writing/HjarSourceFileGenerator.java deleted file mode 100644 index 4bafb5aff8d..00000000000 --- a/java/dagger/internal/codegen/writing/HjarSourceFileGenerator.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.writing; - -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.extension.DaggerStreams.toImmutableList; -import static javax.lang.model.element.Modifier.PRIVATE; - -import androidx.room.compiler.processing.XElement; -import com.google.common.collect.ImmutableList; -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.FieldSpec; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.TypeSpec; -import dagger.internal.codegen.base.SourceFileGenerator; -import javax.lang.model.element.Modifier; - -/** - * A source file generator that only writes the relevant code necessary for Bazel to create a - * correct header (ABI) jar. - */ -public final class HjarSourceFileGenerator extends SourceFileGenerator { - private final SourceFileGenerator delegate; - - private HjarSourceFileGenerator(SourceFileGenerator delegate) { - super(delegate); - this.delegate = delegate; - } - - public static SourceFileGenerator wrap(SourceFileGenerator delegate) { - return new HjarSourceFileGenerator<>(delegate); - } - - @Override - public XElement originatingElement(T input) { - return delegate.originatingElement(input); - } - - @Override - public ImmutableList topLevelTypes(T input) { - return delegate.topLevelTypes(input).stream() - .map(completeType -> skeletonType(completeType.build())) - .collect(toImmutableList()); - } - - private TypeSpec.Builder skeletonType(TypeSpec completeType) { - TypeSpec.Builder skeleton = - classBuilder(completeType.name) - .addSuperinterfaces(completeType.superinterfaces) - .addTypeVariables(completeType.typeVariables) - .addModifiers(completeType.modifiers.toArray(new Modifier[0])) - .addAnnotations(completeType.annotations); - - if (!completeType.superclass.equals(ClassName.OBJECT)) { - skeleton.superclass(completeType.superclass); - } - - completeType.methodSpecs.stream() - .filter(method -> !method.modifiers.contains(PRIVATE) || method.isConstructor()) - .map(this::skeletonMethod) - .forEach(skeleton::addMethod); - - completeType.fieldSpecs.stream() - .filter(field -> !field.modifiers.contains(PRIVATE)) - .map(this::skeletonField) - .forEach(skeleton::addField); - - completeType.typeSpecs.stream() - .map(type -> skeletonType(type).build()) - .forEach(skeleton::addType); - - return skeleton; - } - - private MethodSpec skeletonMethod(MethodSpec completeMethod) { - MethodSpec.Builder skeleton = - completeMethod.isConstructor() - ? constructorBuilder() - : methodBuilder(completeMethod.name).returns(completeMethod.returnType); - - if (completeMethod.isConstructor()) { - // Code in Turbine must (for technical reasons in javac) have a valid super() call for - // constructors, otherwise javac will bark, and Turbine has no way to avoid this. So we retain - // constructor method bodies if they do exist - skeleton.addCode(completeMethod.code); - } - - return skeleton - .addModifiers(completeMethod.modifiers) - .addTypeVariables(completeMethod.typeVariables) - .addParameters(completeMethod.parameters) - .addExceptions(completeMethod.exceptions) - .varargs(completeMethod.varargs) - .addAnnotations(completeMethod.annotations) - .build(); - } - - private FieldSpec skeletonField(FieldSpec completeField) { - return FieldSpec.builder( - completeField.type, - completeField.name, - completeField.modifiers.toArray(new Modifier[0])) - .addAnnotations(completeField.annotations) - .build(); - } -} diff --git a/javatests/dagger/internal/codegen/HjarTest.java b/javatests/dagger/internal/codegen/HjarTest.java new file mode 100644 index 00000000000..e8f67b7250e --- /dev/null +++ b/javatests/dagger/internal/codegen/HjarTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2014 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 androidx.room.compiler.processing.util.Source; +import com.google.common.collect.ImmutableMap; +import dagger.testing.compile.CompilerTests; +import dagger.testing.compile.CompilerTests.DaggerCompiler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests compilation with the {@code experimental_turbine_hjar} flag enabled. */ +@RunWith(JUnit4.class) +public class HjarTest { + /** Returns a {@link DaggerCompiler} with hjar generation enabled. */ + private static DaggerCompiler daggerCompiler(Source... sources) { + return CompilerTests.daggerCompiler(sources) + .withProcessingOptions(ImmutableMap.of("experimental_turbine_hjar", "")); + } + + @Test + public void componentTest() { + Source component = + CompilerTests.javaSource( + "test.MyComponent", + "package test;", + "", + "import dagger.Component;", + "", + "@Component", + "interface MyComponent {", + " String getString();", + " int getInt();", + " void inject(String str);", + "}"); + daggerCompiler(component).compile(subject -> subject.hasErrorCount(0)); + } + + @Test + public void moduleTest() { + Source module = + CompilerTests.javaSource( + "test.MyModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.Provides;", + "", + "@Module", + "interface MyModule {", + " @Provides static int provideInt() { return 0; }", + " @Provides static String provideString() { return null; }", + " @Provides static String[] provideStringArray() { return null; }", + " @Provides static int[] provideIntArray() { return null; }", + " @Provides static boolean provideBoolean() { return false; }", + "}"); + daggerCompiler(module).compile(subject -> subject.hasErrorCount(0)); + } + + @Test + public void producerModuleTest() { + Source module = + CompilerTests.javaSource( + "test.MyModule", + "package test;", + "", + "import com.google.common.util.concurrent.ListenableFuture;", + "import dagger.producers.ProducerModule;", + "import dagger.producers.Produces;", + "", + "@ProducerModule", + "interface MyModule {", + " @Produces static ListenableFuture produceString() { return null; }", + "}"); + daggerCompiler(module).compile(subject -> subject.hasErrorCount(0)); + } +}