Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow contributing bindings with generic supertypes #726

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ClassReferenceWithGenericParameters> {
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<out OutputT : Any> : ParentInterface<OutputT>
// 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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TypeName>
)
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ public sealed class TypeReference {

fun KtUserType.isTypeParameter(): Boolean {
return parents.filterIsInstance<KtClassOrObject>().first().typeParameters.any {
val typeParameter = it.text.split(":").first().trim()
val typeParameter = it.text.split(":").first().removePrefix("out").removePrefix("in").trim()
typeParameter == text
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -330,7 +332,7 @@ private fun List<ContributedBinding>.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 = "]"
Expand All @@ -344,16 +346,26 @@ private fun List<ContributedBinding>.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")
Expand All @@ -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(
Expand All @@ -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
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnnotationSpec>,
val qualifiers: List<AnnotationSpec>,
val boundType: ClassReference,
val boundType: ClassReferenceWithGenericParameters,
val priority: Priority,
val qualifiersKeyLazy: Lazy<String>
)
Expand Down Expand Up @@ -48,82 +47,44 @@ 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. " +
"If there are multiple or none, then the bound type must be explicitly defined in " +
"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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading