Skip to content

Commit

Permalink
fix: add proper support for typealiases (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
KatherineStrider authored Mar 28, 2023
1 parent f2e9b1b commit 71fc170
Show file tree
Hide file tree
Showing 8 changed files with 514 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
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.writeTo
import toothpick.compiler.common.generators.TPCodeGenerator
Expand All @@ -47,7 +48,7 @@ import javax.inject.Inject
abstract class ToothpickProcessor(
processorOptions: Map<String, String>,
private val codeGenerator: CodeGenerator,
protected val logger: KSPLogger
protected val logger: KSPLogger,
) : SymbolProcessor {

protected val options = processorOptions.readOptions()
Expand All @@ -65,13 +66,18 @@ abstract class ToothpickProcessor(
}

protected fun KSType.isValidInjectedType(node: KSNode, qualifiedName: String?): Boolean {
return if (!isValidInjectedClassKind()) false
else !isProviderOrLazy() || isValidProviderOrLazy(node, qualifiedName)
return when {
isError -> false
!isValidInjectedClassKind() && !isTypeAlias() -> false
else -> !isProviderOrLazy() || isValidProviderOrLazy(node, qualifiedName)
}
}

private fun KSType.isValidInjectedClassKind(): Boolean =
(declaration as? KSClassDeclaration)?.classKind in validInjectableTypes

private fun KSType.isTypeAlias(): Boolean = declaration is KSTypeAlias

private fun KSType.isValidProviderOrLazy(node: KSNode, qualifiedName: String?): Boolean {
// e.g. Provider<Foo<String>>
// -> Foo<String>
Expand All @@ -96,7 +102,8 @@ abstract class ToothpickProcessor(
val firstArgumentArgumentType = firstArgumentArgument.type?.resolve()?.declaration?.qualifiedName?.asString()

val isArgumentStar = firstArgumentArgument.variance == Variance.STAR
val isArgumentAny = firstArgumentArgument.variance == Variance.INVARIANT && firstArgumentArgumentType == Any::class.qualifiedName
val isArgumentAny =
firstArgumentArgument.variance == Variance.INVARIANT && firstArgumentArgumentType == Any::class.qualifiedName
val areValidArguments = firstArgumentArguments.size <= 1 && (isArgumentStar || isArgumentAny)

if (!areValidArguments) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ package toothpick.compiler.common.generators.targets
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.isLocal
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSName
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType
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.toClassName
Expand All @@ -42,29 +46,31 @@ import javax.inject.Qualifier
* Information necessary to identify the parameter of a method or a class's property.
*/
sealed class VariableInjectionTarget(
val memberType: KSType,
val className: ClassName,
val typeName: TypeName,
val memberName: KSName,
val qualifierName: Any?
val qualifierName: Any?,
) {
class Instance(
memberType: KSType,
className: ClassName,
typeName: TypeName,
memberName: KSName,
qualifierName: Any?
) : VariableInjectionTarget(memberType, memberName, qualifierName)
qualifierName: Any?,
) : VariableInjectionTarget(className, typeName, memberName, qualifierName)

class Lazy(
memberType: KSType,
className: ClassName,
typeName: TypeName,
memberName: KSName,
qualifierName: Any?,
val kindParamClass: KSType
) : VariableInjectionTarget(memberType, memberName, qualifierName)
) : VariableInjectionTarget(className, typeName, memberName, qualifierName)

class Provider(
memberType: KSType,
className: ClassName,
typeName: TypeName,
memberName: KSName,
qualifierName: Any?,
val kindParamClass: KSType
) : VariableInjectionTarget(memberType, memberName, qualifierName)
) : VariableInjectionTarget(className, typeName, memberName, qualifierName)

companion object {

Expand All @@ -84,26 +90,58 @@ sealed class VariableInjectionTarget(

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

Provider(
memberType = type,
className = kindParamClass.toClassName(),
typeName = type.toParameterizedTypeName(kindParamClass),
memberName = name,
qualifierName = qualifierName,
kindParamClass = type.getInjectedType()
qualifierName = qualifierName
)
toothpick.Lazy::class.qualifiedName ->
}
toothpick.Lazy::class.qualifiedName -> {
val kindParamClass = type.getInjectedType()

Lazy(
memberType = type,
className = kindParamClass.toClassName(),
typeName = type.toParameterizedTypeName(kindParamClass),
memberName = name,
qualifierName = qualifierName,
kindParamClass = type.getInjectedType()
qualifierName = qualifierName
)
else -> Instance(
memberType = type,
}
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
)
}
}

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

/**
* Lookup both [javax.inject.Qualifier] and [javax.inject.Named] to provide the name
Expand Down Expand Up @@ -145,6 +183,32 @@ sealed class VariableInjectionTarget(
* (e.g. in `Lazy<B>`, type is `B`, not `Lazy`).
*/
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
}
}

/**
* Copied from ksp [com.squareup.kotlinpoet.ksp.toClassNameInternal]
* With it, we can create a correct type name for typealias (using [parameterizedBy])
* Otherwise, we lose the generic parameters
*/
private fun KSDeclaration.toClassName(): ClassName {
require(!isLocal()) {
"Local/anonymous classes are not supported!"
}
val pkgName = packageName.asString()
val typesString = checkNotNull(qualifiedName).asString().removePrefix("$pkgName.")

val simpleNames = typesString
.split(".")
return ClassName(pkgName, simpleNames)
}
}
}

Expand All @@ -155,23 +219,11 @@ fun VariableInjectionTarget.getInvokeScopeGetMethodWithNameCodeBlock(): CodeBloc
is VariableInjectionTarget.Lazy -> "getLazy"
}

val className: ClassName = when (this) {
is VariableInjectionTarget.Instance -> memberType.toClassName()
is VariableInjectionTarget.Provider -> kindParamClass.toClassName()
is VariableInjectionTarget.Lazy -> kindParamClass.toClassName()
}

return CodeBlock.builder()
.add("%N(%T::class.java", scopeGetMethodName, className)
.apply { if (qualifierName != null) add(", %S", qualifierName) }
.add(")")
.build()
}

fun VariableInjectionTarget.getParamType(): TypeName = when (this) {
is VariableInjectionTarget.Instance -> memberType.toTypeName()
is VariableInjectionTarget.Provider -> memberType.toClassName()
.parameterizedBy(kindParamClass.toTypeName())
is VariableInjectionTarget.Lazy -> memberType.toClassName()
.parameterizedBy(kindParamClass.toTypeName())
}
fun VariableInjectionTarget.getParamType(): TypeName = typeName
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,21 @@ class FactoryProcessor(

if (parentClass.isNonStaticInnerClass()) return false

return parameters.all { param ->
val invalidParams = parameters.filterNot { param ->
param.type.resolve().isValidInjectedType(
node = this,
qualifiedName = param.name?.asString()
)
}

if (invalidParams.isNotEmpty()) {
logger.error(
this,
"Class ${parentClass.qualifiedName?.asString()} has invalid parameters: $invalidParams",
)
}

return invalidParams.isEmpty()
}

private fun KSFunctionDeclaration.createConstructorInjectionTarget(resolver: Resolver): ConstructorInjectionTarget {
Expand Down
Loading

0 comments on commit 71fc170

Please sign in to comment.