Skip to content

Commit

Permalink
Make FIR type parsing more robust (#2193)
Browse files Browse the repository at this point in the history
Do not use bestGuess when we can properly iterate the type hierarchy and extract what we need without ambiguity.
  • Loading branch information
JakeWharton authored Jul 22, 2024
1 parent ec14aaf commit af7e6ae
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ public data class FqType(
}
}

internal fun KClass<*>.toFqType(): FqType {
internal fun KClass<*>.toFqType(
vararg parameterTypes: FqType,
variance: FqType.Variance = Invariant,
nullable: Boolean = false,
): FqType {
// We only parse public API declarations on which it should be impossible to use
// function-local classes or anonymous classes.
var qualifiedName = qualifiedName ?: throw AssertionError(this)
Expand Down Expand Up @@ -178,7 +182,7 @@ internal fun KClass<*>.toFqType(): FqType {
}
}

return FqType(names)
return FqType(names, variance, parameterTypes.toList(), nullable)
}

internal fun KType.toFqType(): FqType {
Expand All @@ -192,10 +196,10 @@ internal fun KType.toFqType(): FqType {
}

private fun KTypeProjection.toFqType(): FqType {
val json = type?.toFqType() ?: FqType.Star
val fqType = type?.toFqType() ?: FqType.Star
return when (variance) {
null, INVARIANT -> json
IN -> json.copy(variance = In)
OUT -> json.copy(variance = Out)
null, INVARIANT -> fqType
IN -> fqType.copy(variance = In)
OUT -> fqType.copy(variance = Out)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,8 @@ private fun parseWidget(
if (type.annotations.any(ExtensionFunctionType::class::isInstance)) {
val receiverType = type.arguments.first().type
val receiverClassifier = receiverType?.classifier
require(receiverClassifier is KClass<*> && receiverType.arguments.isEmpty()) {
"@Children ${memberType.qualifiedName}#$name lambda receiver can only be a class. Found: $receiverType"
require(receiverClassifier is KClass<*> && receiverType.arguments.isEmpty() && !receiverType.isMarkedNullable) {
"@Children ${memberType.qualifiedName}#$name lambda receiver can only be a non-null class. Found: $receiverType"
}
scope = receiverClassifier.toFqType()
arguments = arguments.drop(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
package app.cash.redwood.tooling.schema

import app.cash.redwood.tooling.schema.Deprecation.Level
import app.cash.redwood.tooling.schema.FqType.Variance.In
import app.cash.redwood.tooling.schema.FqType.Variance.Invariant
import app.cash.redwood.tooling.schema.FqType.Variance.Out
import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolChildren
import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolProperty
import app.cash.redwood.tooling.schema.SchemaAnnotation.DependencyAnnotation
Expand Down Expand Up @@ -68,22 +71,31 @@ import org.jetbrains.kotlin.fir.expressions.arguments
import org.jetbrains.kotlin.fir.expressions.impl.FirResolvedArgumentList
import org.jetbrains.kotlin.fir.references.FirNamedReference
import org.jetbrains.kotlin.fir.resolve.fqName
import org.jetbrains.kotlin.fir.types.ConeClassLikeType
import org.jetbrains.kotlin.fir.types.ConeKotlinTypeProjection
import org.jetbrains.kotlin.fir.types.ConeStarProjection
import org.jetbrains.kotlin.fir.types.ConeTypeParameterType
import org.jetbrains.kotlin.fir.types.ConeTypeProjection
import org.jetbrains.kotlin.fir.types.classId
import org.jetbrains.kotlin.fir.types.isBasicFunctionType
import org.jetbrains.kotlin.fir.types.isNullable
import org.jetbrains.kotlin.fir.types.receiverType
import org.jetbrains.kotlin.fir.types.renderReadable
import org.jetbrains.kotlin.fir.types.toSymbol
import org.jetbrains.kotlin.fir.types.type
import org.jetbrains.kotlin.fir.types.variance
import org.jetbrains.kotlin.kdoc.lexer.KDocTokens
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil.DEFAULT_MODULE_NAME
import org.jetbrains.kotlin.modules.TargetId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.platform.CommonPlatforms
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.text
import org.jetbrains.kotlin.types.Variance.INVARIANT
import org.jetbrains.kotlin.types.Variance.IN_VARIANCE
import org.jetbrains.kotlin.types.Variance.OUT_VARIANCE

public fun parseSchema(
javaHome: File,
Expand Down Expand Up @@ -181,7 +193,7 @@ public fun parseProtocolSchema(

val types = firFiles
.flatMap { it.declarations.findRegularClassesRecursive() }
.associateBy { it.classId.asSingleFqName().toFqType() }
.associateBy { it.classId.toFqType() }

val firContext = FirContext(types, firSession)

Expand Down Expand Up @@ -432,7 +444,7 @@ private fun FirContext.parseWidget(
tag = propertyAnnotation.tag,
name = name,
documentation = documentation,
parameterTypes = arguments.map { it.type!!.classId!!.asSingleFqName().toFqType() },
parameterTypes = arguments.map { it.toFqType() },
isNullable = type.isNullable,
defaultExpression = defaultAnnotation?.expression,
deprecation = deprecation,
Expand All @@ -442,7 +454,7 @@ private fun FirContext.parseWidget(
tag = propertyAnnotation.tag,
name = name,
documentation = documentation,
type = type.classId!!.asSingleFqName().toFqType(),
type = type.toFqType(),
defaultExpression = defaultAnnotation?.expression,
deprecation = deprecation,
)
Expand All @@ -456,20 +468,20 @@ private fun FirContext.parseWidget(
var arguments = typeArguments.dropLast(1) // Drop Unit return type.
val scope = type.receiverType(firSession)
if (scope != null) {
require(scope.typeArguments.isEmpty() && scope !is ConeTypeParameterType) {
"@Children $memberType#$name lambda receiver can only be a class. Found: ${scope.renderReadable()}"
require(scope.typeArguments.isEmpty() && scope !is ConeTypeParameterType && !scope.isNullable) {
"@Children $memberType#$name lambda receiver can only be a non-null class. Found: ${scope.renderReadable()}"
}
arguments = arguments.drop(1)
}
require(arguments.isEmpty()) {
"@Children $memberType#$name lambda type must not have any arguments. " +
"Found: ${arguments.map { it.type!!.classId!!.asSingleFqName() }}"
"Found: ${arguments.map { it.toFqType() }}"
}
ParsedProtocolChildren(
tag = childrenAnnotation.tag,
name = name,
documentation = documentation,
scope = scope?.type?.classId?.asSingleFqName()?.toFqType(),
scope = scope?.toFqType(),
defaultExpression = defaultAnnotation?.expression,
deprecation = deprecation,
)
Expand Down Expand Up @@ -581,7 +593,7 @@ private fun FirContext.parseModifier(
} else if (firClass.isData) {
firClass.primaryConstructorIfAny(firSession)!!.valueParameterSymbols.map { parameter ->
val name = parameter.name.identifier
val parameterType = parameter.resolvedReturnType.classId!!.asSingleFqName().toFqType()
val parameterType = parameter.resolvedReturnType.toFqType()

val defaultAnnotation = findDefaultAnnotation(parameter.annotations)
val deprecation = findDeprecationAnnotation(parameter.annotations)
Expand Down Expand Up @@ -647,7 +659,7 @@ private fun FirContext.findSchemaAnnotation(
?: throw AssertionError(annotation.source?.text)
val classId = resolvedQualifier.classId
?: throw AssertionError(annotation.source?.text)
classId.asSingleFqName().toFqType()
classId.toFqType()
}

val dependenciesArray = annotation.argumentMapping
Expand All @@ -674,7 +686,7 @@ private fun FirContext.findSchemaAnnotation(
?: throw AssertionError(annotation.source?.text)
val classId = resolvedQualifier.classId
?: throw AssertionError(annotation.source?.text)
val fqType = classId.asSingleFqName().toFqType()
val fqType = classId.toFqType()

DependencyAnnotation(tag, fqType)
}
Expand Down Expand Up @@ -820,7 +832,7 @@ private fun FirContext.findModifierAnnotation(
?: throw AssertionError(annotation.source?.text)
val classId = resolvedQualifier.classId
?: throw AssertionError(annotation.source?.text)
classId.asSingleFqName().toFqType()
classId.toFqType()
}

return ModifierAnnotation(tagExpression.value, scopes)
Expand Down Expand Up @@ -878,7 +890,37 @@ private fun DeprecationAnnotation.toDeprecation(source: () -> String): ParsedDep
)
}

private fun FqName.toFqType() = FqType.bestGuess(asString())
private fun ConeTypeProjection.toFqType(): FqType {
return when (this) {
ConeStarProjection -> FqType.Star
is ConeKotlinTypeProjection -> {
when (val type = type) {
is ConeClassLikeType -> {
val parameterTypes = type.typeArguments.map { it.toFqType() }
val variance = when (variance) {
INVARIANT -> Invariant
IN_VARIANCE -> In
OUT_VARIANCE -> Out
}
type.lookupTag.classId.toFqType()
.copy(
nullable = type.isNullable,
parameterTypes = parameterTypes,
variance = variance,
)
}
else -> throw UnsupportedOperationException()
}
}
}
}

private fun ClassId.toFqType() = FqType(
buildList {
add(packageFqName.asString())
addAll(relativeClassName.asString().split('.'))
},
)

private object FqNames {
val Children = FqName("app.cash.redwood.schema.Children")
Expand Down
Loading

0 comments on commit af7e6ae

Please sign in to comment.