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

Support generating Factory for generic classes #27

Closed
wants to merge 5 commits into from
Closed
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
@@ -0,0 +1,42 @@
/*
* Copyright 2022 Baptiste Candellier
* Copyright 2019 Stephane Nicolas
* Copyright 2019 Daniel Molinero Reguera
*
* 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 toothpick.compiler.common

import com.google.devtools.ksp.symbol.KSTypeParameter
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.ksp.TypeParameterResolver
import toothpick.compiler.common.generators.toBoundTypeVariableName

/**
* Recursively resolves typeParameters to it`s first bound type.
*
* The returned [TypeVariableName.name] uses [ClassName.simpleName] and may produce an uncompilable code for cases, when any of parameter`s types is not imported.
*/
class BoundTypeParameterResolver(typeParameters: List<KSTypeParameter>) : TypeParameterResolver {
private val mapByName = typeParameters.associateBy { it.name.getShortName() }
override val parametersMap: MutableMap<String, TypeVariableName> = LinkedHashMap()
override operator fun get(index: String): TypeVariableName = parametersMap.getOrPut(index) {
buildVariableTypeName(index)
}

private fun buildVariableTypeName(index: String): TypeVariableName {
val parameter = mapByName[index] ?: throw NoSuchElementException("No TypeParameter found for index $index")
return parameter.toBoundTypeVariableName(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.google.devtools.ksp.isPrivate
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
Expand All @@ -34,6 +35,7 @@ import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.Variance
import com.squareup.kotlinpoet.ksp.TypeParameterResolver
import com.squareup.kotlinpoet.ksp.writeTo
import toothpick.compiler.common.generators.TPCodeGenerator
import toothpick.compiler.common.generators.error
Expand All @@ -45,13 +47,11 @@ import java.io.IOException
import javax.inject.Inject

@OptIn(KspExperimental::class)
abstract class ToothpickProcessor(
processorOptions: Map<String, String>,
private val codeGenerator: CodeGenerator,
protected val logger: KSPLogger,
) : SymbolProcessor {
abstract class ToothpickProcessor(env: SymbolProcessorEnvironment) : SymbolProcessor {
private val codeGenerator: CodeGenerator = env.codeGenerator
protected val logger: KSPLogger = env.logger

protected val options = processorOptions.readOptions()
protected val options = env.options.readOptions()

protected fun writeToFile(tpCodeGenerator: TPCodeGenerator, fileDescription: String): Boolean {
return try {
Expand Down Expand Up @@ -118,8 +118,10 @@ abstract class ToothpickProcessor(
return true
}

protected fun KSFunctionDeclaration.getParamInjectionTargetList(): List<VariableInjectionTarget> =
parameters.map { param -> VariableInjectionTarget.create(param, logger) }
protected fun KSFunctionDeclaration.getParamInjectionTargetList(
typeParameterResolver: TypeParameterResolver
): List<VariableInjectionTarget> =
parameters.map { param -> VariableInjectionTarget.create(param, typeParameterResolver, logger) }

protected fun KSDeclaration.isExcludedByFilters(): Boolean {
val qualifiedName = qualifiedName?.asString() ?: return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
package toothpick.compiler.common.generators

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeName

/**
* The name of the generated factory class for a given class.
Expand All @@ -36,3 +38,7 @@ val ClassName.memberInjectorClassName: ClassName
packageName = packageName,
simpleNames.joinToString("$") + "__MemberInjector"
)

fun ClassName.withTypeArguments(arguments: List<TypeName>): TypeName {
return if (arguments.isEmpty()) this else parameterizedBy(arguments)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2022 Baptiste Candellier
* Copyright 2019 Stephane Nicolas
* Copyright 2019 Daniel Molinero Reguera
*
* 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 toothpick.compiler.common.generators

import com.google.devtools.ksp.isLocal
import com.google.devtools.ksp.symbol.KSDeclaration
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy

/**
* Copied from KotlinPoet [com.squareup.kotlinpoet.ksp.toClassNameInternal]
* With it, we can create a correct type name for typealias (using [parameterizedBy])
* Otherwise, we lose the generic parameters
*/
fun KSDeclaration.toClassName(): ClassName {
require(!isLocal()) { "Local/anonymous classes are not supported!" }
val pkgName = packageName.asString()
val simpleNames = checkNotNull(qualifiedName).asString()
.removePrefix("$pkgName.")
.split(".")
return ClassName(pkgName, simpleNames)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@ package toothpick.compiler.common.generators

import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSTypeParameter
import com.google.devtools.ksp.symbol.Variance
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.TypeParameterResolver
import com.squareup.kotlinpoet.ksp.toTypeName

/**
* Alternative to [com.google.devtools.ksp.getAnnotationsByType] that retrieves [KSAnnotation]s instead.
Expand All @@ -31,3 +42,32 @@ inline fun <reified T : Annotation> KSAnnotated.getAnnotationsByType(): Sequence
annotation.annotationType.resolve().declaration.qualifiedName?.asString() == className.canonicalName
}
}

val Variance.modifier
get() = when (this) {
Variance.COVARIANT -> KModifier.OUT
Variance.CONTRAVARIANT -> KModifier.IN
else -> null
}

fun TypeName.toTypeVariableName(bounds: List<TypeName>, modifier: KModifier?) =
TypeVariableName(buildShortName(), bounds, modifier)

fun TypeName.buildShortName(): String = when (this) {
is ClassName -> canonicalName
is TypeVariableName -> name
is WildcardTypeName -> when {
inTypes.size == 1 -> "in·${inTypes[0].buildShortName()}"
outTypes == STAR.outTypes -> "*"
else -> "out·${inTypes[0].buildShortName()}"
}
is ParameterizedTypeName ->
rawType.canonicalName + typeArguments.joinToString(", ", "<", ">") { it.buildShortName() }
else -> toString()
}

fun KSTypeParameter.toBoundTypeVariableName(typeParameterResolver: TypeParameterResolver): TypeVariableName {
val bounds = bounds.map { it.toTypeName(typeParameterResolver) }
return bounds.firstOrNull()?.toTypeVariableName(bounds.drop(1).toList(), variance.modifier)
?: TypeVariableName("*", STAR)
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ import com.google.devtools.ksp.symbol.KSTypeAlias
import com.google.devtools.ksp.symbol.KSValueParameter
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.ksp.TypeParameterResolver
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import toothpick.compiler.common.generators.error
import toothpick.compiler.common.generators.withTypeArguments
import javax.inject.Named
import javax.inject.Qualifier

Expand Down Expand Up @@ -74,74 +75,63 @@ sealed class VariableInjectionTarget(

companion object {

fun create(parameter: KSValueParameter, logger: KSPLogger? = null): VariableInjectionTarget =
create(
name = parameter.name!!,
type = parameter.type.resolve(),
qualifierName = parameter.findQualifierName(logger)
)

fun create(parameter: KSPropertyDeclaration, logger: KSPLogger? = null): VariableInjectionTarget =
create(
name = parameter.simpleName,
type = parameter.type.resolve(),
qualifierName = parameter.findQualifierName(logger)
)

private fun create(name: KSName, type: KSType, qualifierName: String?): VariableInjectionTarget =
when (type.declaration.qualifiedName?.asString()) {
javax.inject.Provider::class.qualifiedName -> {
val kindParamClass = type.getInjectedType()

Provider(
className = kindParamClass.toClassName(),
typeName = type.toParameterizedTypeName(kindParamClass),
memberName = name,
qualifierName = qualifierName
)
fun create(
parameter: KSValueParameter,
typeParameterResolver: TypeParameterResolver,
logger: KSPLogger,
): VariableInjectionTarget = create(
name = parameter.name!!,
type = parameter.type.resolve(),
qualifierName = parameter.findQualifierName(logger),
typeParameterResolver = typeParameterResolver,
)

fun create(
parameter: KSPropertyDeclaration,
typeParameterResolver: TypeParameterResolver,
logger: KSPLogger,
): VariableInjectionTarget = create(
name = parameter.simpleName,
type = parameter.type.resolve(),
qualifierName = parameter.findQualifierName(logger),
typeParameterResolver = typeParameterResolver,
)

private fun create(
name: KSName,
type: KSType,
qualifierName: String?,
typeParameterResolver: TypeParameterResolver,
): VariableInjectionTarget {
val typeQualifiedName = type.declaration.qualifiedName?.asString()
val providerQualifiedName = javax.inject.Provider::class.qualifiedName
val lazyQualifiedName = toothpick.Lazy::class.qualifiedName
val className = when (typeQualifiedName) {
providerQualifiedName, lazyQualifiedName -> type.getInjectedType().toClassName()
else -> (if (type.declaration is KSTypeAlias) type.findActualType() else type).declaration.toClassName()
}
val typeName = when (typeQualifiedName) {
providerQualifiedName, lazyQualifiedName -> {
val typeName = type.getInjectedType().toTypeName(typeParameterResolver)
type.toParameterizedTypeName(typeName)
}
toothpick.Lazy::class.qualifiedName -> {
val kindParamClass = type.getInjectedType()

Lazy(
className = kindParamClass.toClassName(),
typeName = type.toParameterizedTypeName(kindParamClass),
memberName = name,
qualifierName = qualifierName
)
else -> {
val arguments = type.arguments.map { it.toTypeName(typeParameterResolver) }
type.declaration.toClassName().withTypeArguments(arguments)
}
else -> createInstanceTarget(name, type, qualifierName)
}

private fun createInstanceTarget(name: KSName, type: KSType, qualifierName: String?): Instance {
return if (type.declaration is KSTypeAlias) {
val actualTypeClassName = type.findActualType().toClassName()
val argumentsTypeNames = type.arguments.map { it.type!!.resolve().toTypeName() }

val typeName = if (argumentsTypeNames.isNotEmpty()) {
type.declaration.toClassName().parameterizedBy(argumentsTypeNames)
} else {
type.toTypeName()
}

Instance(
className = actualTypeClassName,
typeName = typeName,
memberName = name,
qualifierName = qualifierName
)
} else {
Instance(
className = type.toClassName(),
typeName = type.toTypeName(),
memberName = name,
qualifierName = qualifierName
)
return when (typeQualifiedName) {
providerQualifiedName ->
Provider(className, typeName, name, qualifierName)
lazyQualifiedName ->
Lazy(className, typeName, name, qualifierName)
else ->
Instance(className, typeName, name, qualifierName)
}
}

private fun KSType.toParameterizedTypeName(kindParamClass: KSType): ParameterizedTypeName =
toClassName().parameterizedBy(kindParamClass.toTypeName())
private fun KSType.toParameterizedTypeName(typeName: TypeName) =
toClassName().parameterizedBy(typeName)

/**
* Lookup both [javax.inject.Qualifier] and [javax.inject.Named] to provide the name
Expand All @@ -150,7 +140,8 @@ sealed class VariableInjectionTarget(
* @receiver the node for which a qualifier is to be found.
* @return the name of this injection, or null if it has no qualifier annotations.
*/
private fun KSAnnotated.findQualifierName(logger: KSPLogger?): String? {
@OptIn(KspExperimental::class)
private fun KSAnnotated.findQualifierName(logger: KSPLogger): String? {
val qualifierAnnotationNames = annotations
.mapNotNull { annotation ->
val annotationClass = annotation.annotationType.resolve().declaration
Expand All @@ -168,7 +159,7 @@ sealed class VariableInjectionTarget(
val allNames = qualifierAnnotationNames + namedValues

if (allNames.count() > 1) {
logger?.error(this, "Only one javax.inject.Qualifier annotation is allowed to name injections.")
logger.error(this, "Only one javax.inject.Qualifier annotation is allowed to name injections.")
}

return allNames.firstOrNull()
Expand All @@ -185,12 +176,7 @@ sealed class VariableInjectionTarget(
private fun KSType.getInjectedType(): KSType = arguments.first().type!!.resolve()

private fun KSType.findActualType(): KSType {
val typeDeclaration = declaration
return if (typeDeclaration is KSTypeAlias) {
typeDeclaration.type.resolve().findActualType()
} else {
this
}
return (declaration as? KSTypeAlias)?.type?.resolve()?.findActualType() ?: this
}

/**
Expand Down
Loading