diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReference.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReference.kt index c60df97e6..62e2d331f 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReference.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReference.kt @@ -10,8 +10,10 @@ import com.squareup.anvil.compiler.internal.reference.Visibility.PRIVATE import com.squareup.anvil.compiler.internal.reference.Visibility.PROTECTED import com.squareup.anvil.compiler.internal.reference.Visibility.PUBLIC import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind.DECLARATION import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassKind @@ -406,6 +408,56 @@ public fun ClassReference.allSuperTypeClassReferences( .distinct() } +/** + * This will return all super types as [ClassReferenceWithGenericParameters], whether they're parsed as [KtClassOrObject] + * or [ClassDescriptor]. This will include generated code, assuming it has already been generated. + * The returned sequence will be distinct by FqName, and Psi types are preferred over Descriptors. + * + * The first elements in the returned sequence represent the direct superclass to the receiver. The + * last elements represent the types which are furthest up-stream. + * + * @param includeSelf If true, the receiver class is the first element of the sequence + */ +@ExperimentalAnvilApi +public fun ClassReference.allSuperTypeClassReferencesWithGenericParameters( + includeSelf: Boolean = false +): Sequence { + return generateSequence(listOf( + ClassReferenceWithGenericParameters(this, emptyList()) + )) { superTypes -> + superTypes + .flatMap { classRefWithGenericParames -> + val classReference = classRefWithGenericParames.classReference + classReference.directSuperTypeReferences().mapNotNull { typeRef -> + typeRef.asClassReferenceOrNull() + ?.let { clasRef -> + val rawTypeArguments = (typeRef.asTypeName() as? ParameterizedTypeName)?.typeArguments ?: emptyList() + + // Type arguments will only contain direct references to their parents. For example + // interface MiddleInterface : ParentInterface + // Type argument of the ParentInterface will be OutputT, instead of actual provided value by the + // parent class + // This attempts to resolve the actual type provided from parent + val parentResolvedTypeArguments = rawTypeArguments.map {rawArgument -> + if (rawArgument is TypeVariableName) { + val parentIndex = classReference.typeParameters.indexOfFirst { it.name == rawArgument.name } + classRefWithGenericParames.typeArguments.elementAt(parentIndex) + } else { + rawArgument + } + } + + ClassReferenceWithGenericParameters(clasRef, parentResolvedTypeArguments) + } + } + } + .takeIf { it.isNotEmpty() } + } + .drop(if (includeSelf) 0 else 1) + .flatten() + .distinct() +} + @ExperimentalAnvilApi @Suppress("FunctionName") public fun AnvilCompilationExceptionClassReference( diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReferenceWithGenericParameters.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReferenceWithGenericParameters.kt new file mode 100644 index 000000000..4d4d11024 --- /dev/null +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/ClassReferenceWithGenericParameters.kt @@ -0,0 +1,8 @@ +package com.squareup.anvil.compiler.internal.reference + +import com.squareup.kotlinpoet.TypeName + +public data class ClassReferenceWithGenericParameters( + public val classReference: ClassReference, + public val typeArguments: List +) diff --git a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TypeReference.kt b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TypeReference.kt index de759bb12..06068965e 100644 --- a/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TypeReference.kt +++ b/compiler-utils/src/main/java/com/squareup/anvil/compiler/internal/reference/TypeReference.kt @@ -215,7 +215,7 @@ public sealed class TypeReference { fun KtUserType.isTypeParameter(): Boolean { return parents.filterIsInstance().first().typeParameters.any { - val typeParameter = it.text.split(":").first().trim() + val typeParameter = it.text.split(":").first().removePrefix("out").removePrefix("in").trim() typeParameter == text } } diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt index 1a639b0f5..92b12986d 100644 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/BindingModuleGenerator.kt @@ -28,6 +28,8 @@ import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.STAR import com.squareup.kotlinpoet.TypeSpec import dagger.Binds import dagger.Module @@ -330,7 +332,7 @@ private fun List.findHighestPriorityBinding(): ContributedBi if (bindings.size > 1) { throw AnvilCompilationException( "There are multiple contributed bindings with the same bound type. The bound type is " + - "${bindings[0].boundType.fqName}. The contributed binding classes are: " + + "${bindings[0].boundType.classReference.fqName}. The contributed binding classes are: " + bindings.joinToString( prefix = "[", postfix = "]" @@ -344,16 +346,26 @@ private fun List.findHighestPriorityBinding(): ContributedBi private fun ContributedBinding.toGeneratedMethod( isMultibinding: Boolean ): GeneratedMethod { - val isMapMultibinding = mapKeys.isNotEmpty() val methodNameSuffix = buildString { - append(boundType.shortName.capitalize()) + append(boundType.classReference.shortName.capitalize()) if (isMultibinding) { append("Multi") } } + val boundTypeWithTypeParameters = boundType.classReference.asClassName() + .let { + if (boundType.classReference.typeParameters.isEmpty()) { + it + } else if (isMultibinding) { + it.parameterizedBy(boundType.classReference.typeParameters.map { STAR }) + } else { + it.parameterizedBy(boundType.typeArguments) + } + } + return if (contributedClass.isObject()) { ProviderMethod( spec = FunSpec.builder(name = "provide$methodNameSuffix") @@ -366,11 +378,11 @@ private fun ContributedBinding.toGeneratedMethod( } .addAnnotations(qualifiers) .addAnnotations(mapKeys) - .returns(boundType.asClassName()) + .returns(boundTypeWithTypeParameters) .addStatement("return %T", contributedClass.asClassName()) .build(), contributedClass = contributedClass, - boundType = boundType + boundType = boundType.classReference ) } else { BindingMethod( @@ -389,10 +401,10 @@ private fun ContributedBinding.toGeneratedMethod( name = contributedClass.shortName.decapitalize(), type = contributedClass.asClassName() ) - .returns(boundType.asClassName()) + .returns(boundTypeWithTypeParameters) .build(), contributedClass = contributedClass, - boundType = boundType + boundType = boundType.classReference ) } } diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributedBinding.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributedBinding.kt index 5fb53d8c3..c61add61e 100644 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributedBinding.kt +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/ContributedBinding.kt @@ -5,20 +5,19 @@ import com.squareup.anvil.compiler.anyFqName import com.squareup.anvil.compiler.api.AnvilCompilationException import com.squareup.anvil.compiler.internal.reference.AnnotationReference import com.squareup.anvil.compiler.internal.reference.ClassReference -import com.squareup.anvil.compiler.internal.reference.ClassReference.Descriptor -import com.squareup.anvil.compiler.internal.reference.ClassReference.Psi -import com.squareup.anvil.compiler.internal.reference.allSuperTypeClassReferences +import com.squareup.anvil.compiler.internal.reference.ClassReferenceWithGenericParameters +import com.squareup.anvil.compiler.internal.reference.allSuperTypeClassReferencesWithGenericParameters import com.squareup.anvil.compiler.internal.reference.toClassReference import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ParameterizedTypeName import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.types.KotlinType import kotlin.LazyThreadSafetyMode.NONE internal data class ContributedBinding( val contributedClass: ClassReference, val mapKeys: List, val qualifiers: List, - val boundType: ClassReference, + val boundType: ClassReferenceWithGenericParameters, val priority: Priority, val qualifiersKeyLazy: Lazy ) @@ -48,33 +47,35 @@ internal fun AnnotationReference.toContributedBinding( qualifiers = qualifiers, boundType = boundType, priority = priority(), - qualifiersKeyLazy = declaringClass().qualifiersKeyLazy(boundType, ignoreQualifier) + qualifiersKeyLazy = declaringClass().qualifiersKeyLazy(boundType.classReference, ignoreQualifier) ) } -private fun AnnotationReference.requireBoundType(module: ModuleDescriptor): ClassReference { +private fun AnnotationReference.requireBoundType(module: ModuleDescriptor): ClassReferenceWithGenericParameters { val boundFromAnnotation = boundTypeOrNull() if (boundFromAnnotation != null) { // Since all classes extend Any, we can stop here. - if (boundFromAnnotation.fqName == anyFqName) return anyFqName.toClassReference(module) + if (boundFromAnnotation.fqName == anyFqName) return ClassReferenceWithGenericParameters( + anyFqName.toClassReference(module), + emptyList() + ) // ensure that the bound type is actually a supertype of the contributing class - val boundType = declaringClass().allSuperTypeClassReferences() - .firstOrNull { it.fqName == boundFromAnnotation.fqName } + val boundType = declaringClass().allSuperTypeClassReferencesWithGenericParameters() + .firstOrNull { + it.classReference.fqName == boundFromAnnotation.fqName + } ?: throw AnvilCompilationException( "$fqName contributes a binding for ${boundFromAnnotation.fqName}, " + "but doesn't extend this type." ) - - boundType.checkNotGeneric(contributedClass = declaringClass()) return boundType } // If there's no bound type in the annotation, // it must be the only supertype of the contributing class val boundType = declaringClass().directSuperTypeReferences().singleOrNull() - ?.asClassReference() ?: throw AnvilCompilationException( message = "$fqName contributes a binding, but does not " + "specify the bound type. This is only allowed with exactly one direct super type. " + @@ -82,48 +83,8 @@ private fun AnnotationReference.requireBoundType(module: ModuleDescriptor): Clas "the @$shortName annotation." ) - boundType.checkNotGeneric(contributedClass = declaringClass()) - return boundType -} - -private fun ClassReference.checkNotGeneric( - contributedClass: ClassReference -) { - fun exceptionText(typeString: String): String { - return "Class ${contributedClass.fqName} binds $fqName," + - " but the bound type contains type parameter(s) $typeString." + - " Type parameters in bindings are not supported. This binding needs" + - " to be contributed in a Dagger module manually." - } - - fun KotlinType.describeTypeParameters(): String = arguments - .ifEmpty { return "" } - .joinToString(prefix = "<", postfix = ">") { typeArgument -> - typeArgument.type.toString() + typeArgument.type.describeTypeParameters() - } - - when (this) { - is Descriptor -> { - if (clazz.declaredTypeParameters.isNotEmpty()) { - - throw AnvilCompilationException( - classDescriptor = clazz, - message = exceptionText(clazz.defaultType.describeTypeParameters()) - ) - } - } - is Psi -> { - if (clazz.typeParameters.isNotEmpty()) { - val typeString = clazz.typeParameters - .joinToString(prefix = "<", postfix = ">") { it.name!! } - - throw AnvilCompilationException( - message = exceptionText(typeString), - element = clazz.nameIdentifier - ) - } - } - } + val typeArguments = (boundType.asTypeNameOrNull() as? ParameterizedTypeName)?.typeArguments ?: emptyList() + return ClassReferenceWithGenericParameters(boundType.asClassReference(), typeArguments.map { it }) } private fun ClassReference.qualifiersKeyLazy( diff --git a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/ProvidesMethodFactoryGenerator.kt b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/ProvidesMethodFactoryGenerator.kt index e3bbd2a1a..d96a3792d 100644 --- a/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/ProvidesMethodFactoryGenerator.kt +++ b/compiler/src/main/java/com/squareup/anvil/compiler/codegen/dagger/ProvidesMethodFactoryGenerator.kt @@ -79,7 +79,7 @@ internal class ProvidesMethodFactoryGenerator : PrivateCodeGenerator() { .asSequence() .flatMap { it.properties } .filter { property -> - // Must be '@get:Provides'. + // Must be '@get:"Provides"'. property.annotations.singleOrNull { it.fqName == daggerProvidesFqName }?.annotation?.useSiteTarget?.text == "get" diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt index da48ead6a..abb5513de 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleGeneratorTest.kt @@ -14,13 +14,13 @@ import com.squareup.anvil.compiler.internal.testing.AnyDaggerComponent import com.squareup.anvil.compiler.internal.testing.anyDaggerComponent import com.squareup.anvil.compiler.internal.testing.daggerModule import com.squareup.anvil.compiler.internal.testing.isAbstract -import com.squareup.anvil.compiler.isError import com.squareup.anvil.compiler.isFullTestRun import com.squareup.anvil.compiler.parentInterface import com.squareup.anvil.compiler.parentInterface1 import com.squareup.anvil.compiler.parentInterface2 import com.squareup.anvil.compiler.secondContributingInterface import com.squareup.anvil.compiler.subcomponentInterface +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK import dagger.Binds import dagger.Provides @@ -28,6 +28,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters +import java.io.File import kotlin.reflect.KClass @RunWith(Parameterized::class) @@ -373,7 +374,7 @@ class BindingModuleGeneratorTest( } } - @Test fun `the contributed binding class must not have a generic type parameter`() { + @Test fun `the Dagger binding method with parent type is generated for generic type parameters`() { compile( """ package com.squareup.test @@ -393,19 +394,25 @@ class BindingModuleGeneratorTest( interface ComponentInterface """ ) { - assertThat(exitCode).isError() + assertThat(exitCode).isEqualTo(OK) - assertThat(messages).contains("Source0.kt:6:11") - assertThat(messages).contains( - "Class com.squareup.test.ContributingInterface binds com.squareup.test.ParentInterface, " + - "but the bound type contains type parameter(s) . Type parameters in bindings " + - "are not supported. This binding needs to be contributed in a Dagger module manually." - ) + // Because of type erasure, we cannot check through the type system that generated type + // is the right one. Instead, we can check generated string. + + val generatedModuleFile = File(outputDirectory.parent, "build/anvil") + .walk() + .single { it.isFile && it.name == "ComponentInterface.kt" } + + assertThat(generatedModuleFile.readText().replace(Regex("\\s"), "")) + .contains( + "bindParentInterface(contributingInterface:ContributingInterface):" + + "ParentInterface>>,SomeOtherType>" + ) } } @Test - fun `the contributed binding class must not have a generic type parameter with super type chain`() { + fun `the Dagger binding method with parent type is generated for a super type chain`() { compile( """ package com.squareup.test @@ -425,15 +432,20 @@ class BindingModuleGeneratorTest( interface ComponentInterface """ ) { - assertThat(exitCode).isError() - - assertThat(messages).contains("Source0.kt:6:11") - assertThat(messages).contains( - "Class com.squareup.test.ContributingInterface binds com.squareup.test.ParentInterface, " + - "but the bound type contains type parameter(s) . Type parameters in " + - "bindings are not supported. This binding needs to be contributed in a Dagger module " + - "manually." - ) + assertThat(exitCode).isEqualTo(OK) + + // Because of type erasure, we cannot check through the type system that generated type + // is the right one. Instead, we can check generated string. + + val generatedModuleFile = File(outputDirectory.parent, "build/anvil") + .walk() + .single { it.isFile && it.name == "ComponentInterface.kt" } + + assertThat(generatedModuleFile.readText().replace(Regex("\\s"), "")) + .contains( + "bindParentInterface(contributingInterface:ContributingInterface):" + + "ParentInterface" + ) } } @@ -783,6 +795,43 @@ class BindingModuleGeneratorTest( } } + @Test fun `fill in parent type when boundType parameter is used for generic type parameters`() { + compile( + """ + package com.squareup.test + + import com.squareup.anvil.annotations.ContributesBinding + $import + + interface ParentInterface1 + interface ParentInterface2 + + class SomeOtherType + + @ContributesBinding(Any::class, boundType = ParentInterface1::class) + interface ContributingInterface : ParentInterface1, ParentInterface2 + + $annotation(Any::class) + interface ComponentInterface + """ + ) { + assertThat(exitCode).isEqualTo(OK) + + // Because of type erasure, we cannot check through the type system that generated type + // is the right one. Instead, we can check generated string. + + val generatedModuleFile = File(outputDirectory.parent, "build/anvil") + .walk() + .single { it.isFile && it.name == "ComponentInterface.kt" } + + assertThat(generatedModuleFile.readText().replace(Regex("\\s"), "")) + .contains( + "bindParentInterface1(contributingInterface:ContributingInterface):" + + "ParentInterface1" + ) + } + } + @Test fun `bindings contributed to multiple scopes can be replaced by other contributed bindings`() { compile( diff --git a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleMultibindingSetTest.kt b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleMultibindingSetTest.kt index 3ea88ed23..b6ffb4534 100644 --- a/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleMultibindingSetTest.kt +++ b/compiler/src/test/java/com/squareup/anvil/compiler/codegen/BindingModuleMultibindingSetTest.kt @@ -12,13 +12,13 @@ import com.squareup.anvil.compiler.internal.testing.AnyDaggerComponent import com.squareup.anvil.compiler.internal.testing.anyDaggerComponent import com.squareup.anvil.compiler.internal.testing.daggerModule import com.squareup.anvil.compiler.internal.testing.isAbstract -import com.squareup.anvil.compiler.isError import com.squareup.anvil.compiler.isFullTestRun import com.squareup.anvil.compiler.parentInterface import com.squareup.anvil.compiler.parentInterface1 import com.squareup.anvil.compiler.parentInterface2 import com.squareup.anvil.compiler.secondContributingInterface import com.squareup.anvil.compiler.subcomponentInterface +import com.tschuchort.compiletesting.KotlinCompilation.ExitCode import dagger.Binds import dagger.Provides import dagger.multibindings.IntoSet @@ -26,6 +26,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters +import java.io.File import kotlin.reflect.KClass @RunWith(Parameterized::class) @@ -255,7 +256,7 @@ class BindingModuleMultibindingSetTest( } } - @Test fun `the contributed multibinding class must not have a generic type parameter`() { + @Test fun `the Dagger multibinding method with star type is generated for generic type parameters`() { compile( """ package com.squareup.test @@ -275,14 +276,18 @@ class BindingModuleMultibindingSetTest( interface ComponentInterface """ ) { - assertThat(exitCode).isError() + assertThat(exitCode).isEqualTo(ExitCode.OK) - assertThat(messages).contains("Source0.kt:6:11") - assertThat(messages).contains( - "Class com.squareup.test.ContributingInterface binds com.squareup.test.ParentInterface, " + - "but the bound type contains type parameter(s) . Type parameters in bindings " + - "are not supported. This binding needs to be contributed in a Dagger module manually." - ) + // Because of type erasure, we cannot check through the type system that generated type + // is the right one. Instead, we can check generated string. + + val generatedModuleFile = File(outputDirectory.parent, "build/anvil") + .walk() + .single { it.isFile && it.name == "ComponentInterface.kt" } + + assertThat(generatedModuleFile.readText().replace(Regex("\\s"), "")) + .contains("bindParentInterfaceMulti(contributingInterface:ContributingInterface):" + + "ParentInterface<*,*>") } }