Skip to content
This repository was archived by the owner on Nov 6, 2019. It is now read-only.

Handle union of string values #96

Open
wants to merge 3 commits into
base: master
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
145 changes: 89 additions & 56 deletions src/main/kotlin/converter/typeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,117 +8,144 @@ import typescriptServices.ts.*
import kotlin.collections.Map

fun ObjectTypeToKotlinTypeMapper.mapType(type: Node): KtType {
val resolvedType: Type? = typeChecker.getTypeAtLocation(type)
return if (resolvedType != null) {
mapType(resolvedType, type)
}
else {
(type.unsafeCast<TypeNode>()).toKotlinType(this)
}
return mapToEnhancedType(type).singleType
}

fun ObjectTypeToKotlinTypeMapper.mapTypeToUnion(type: Node): KtTypeUnion {
fun ObjectTypeToKotlinTypeMapper.mapToEnhancedType(type: Node): EnhancedKtType {
val resolvedType: Type? = typeChecker.getTypeAtLocation(type)
return if (resolvedType != null) {
mapTypeToUnion(resolvedType, type)
mapToEnhancedType(resolvedType, type)
}
else {
(type.unsafeCast<TypeNode>()).toKotlinTypeUnion(this)
(type.unsafeCast<TypeNode>()).toEnhancedType(this)
}
}

private fun ObjectTypeToKotlinTypeMapper.mapType(type: Type, declaration: Node?): KtType =
mapTypeToUnion(type, declaration).singleType
mapToEnhancedType(type, declaration).singleType

private fun ObjectTypeToKotlinTypeMapper.mapTypeToUnion(type: Type, declaration: Node?): KtTypeUnion {
private fun ObjectTypeToKotlinTypeMapper.mapToEnhancedType(type: Type, declaration: Node?): EnhancedKtType {
val resultingDeclaration = declaration ?: type.symbol?.declarations?.singleOrNull()

val flags = type.getFlags()
if (resultingDeclaration != null && type.symbol == null && TypeFlags.Any in flags) {
return resultingDeclaration.unsafeCast<TypeNode>().toKotlinTypeUnion(this)
return resultingDeclaration.unsafeCast<TypeNode>().toEnhancedType(this)
}

if (resultingDeclaration?.kind == SyntaxKind.UnionType && TypeFlags.Union !in flags) {
return resultingDeclaration.unsafeCast<UnionTypeNode>().toKotlinTypeUnion(this)
if (resultingDeclaration?.kind == SyntaxKind.UnionType) {
return resultingDeclaration.unsafeCast<UnionTypeNode>().toEnhancedType(this)
}

if (TypeFlags.Union in flags) {
return mapUnionType(type.unsafeCast<UnionType>())
}

if (type in typesInMappingProcess) {
report(
"Recursion is detected when resolve type: \"${type.symbol?.name}\" for the declaration at ${declaration?.location()}",
maxLevelToShow = DiagnosticLevel.WARNING_WITH_STACKTRACE
)
return KtTypeUnion(KtType(DYNAMIC))
return SingleKtType(KtType(DYNAMIC))
}

typesInMappingProcess += type

val mappedType = when {
declaration?.kind == SyntaxKind.ThisType -> {
val possibleTypes = mapTypeToUnion(type.unsafeCast<TypeParameter>().constraint!!, null).possibleTypes
.map { it.copy(comment = "this") }
KtTypeUnion(possibleTypes)
}
declaration?.kind == SyntaxKind.ThisType -> mapToEnhancedType(type.unsafeCast<TypeParameter>().constraint!!, null).withComment("this")

TypeFlags.Any in flags -> KtTypeUnion(KtType(ANY))
TypeFlags.String in flags -> KtTypeUnion(KtType(STRING))
TypeFlags.Boolean in flags -> KtTypeUnion(KtType(BOOLEAN))
TypeFlags.Number in flags -> KtTypeUnion(KtType(NUMBER))
TypeFlags.Void in flags -> KtTypeUnion(KtType(UNIT))
TypeFlags.Any in flags -> SingleKtType(KtType(ANY))
TypeFlags.String in flags -> SingleKtType(KtType(STRING))
TypeFlags.Boolean in flags -> SingleKtType(KtType(BOOLEAN))
TypeFlags.Number in flags -> SingleKtType(KtType(NUMBER))
TypeFlags.Void in flags -> SingleKtType(KtType(UNIT))

TypeFlags.Undefined in flags ||
TypeFlags.Null in flags -> KtTypeUnion(KtType(NOTHING, isNullable = true))
TypeFlags.Null in flags -> SingleKtType(KtType(NOTHING, isNullable = true))

TypeFlags.StringLiteral in flags -> {
KtTypeUnion(KtType(STRING, comment = "\"" + type.unsafeCast<LiteralType>().value + "\""))
SingleKtType(KtType(STRING, comment = "\"" + type.unsafeCast<LiteralType>().value + "\""))
}
// TODO: add test if it's allowed
TypeFlags.NumberLiteral in flags -> {
KtTypeUnion(KtType(NUMBER, comment = type.unsafeCast<LiteralType>().value))
SingleKtType(KtType(NUMBER, comment = type.unsafeCast<LiteralType>().value))
}
// TODO: add test if it's allowed
TypeFlags.BooleanLiteral in flags -> {
KtTypeUnion(KtType(BOOLEAN, comment = type.unsafeCast<LiteralType>().value))
SingleKtType(KtType(BOOLEAN, comment = type.unsafeCast<LiteralType>().value))
}

TypeFlags.Union in flags -> mapUnionType(type.unsafeCast<UnionType>())
TypeFlags.Intersection in flags -> mapIntersectionType(type.unsafeCast<IntersectionType>())

TypeFlags.TypeParameter in flags -> KtTypeUnion(KtType(KtQualifiedName(unescapeIdentifier(type.getSymbol()!!.name))))
TypeFlags.TypeParameter in flags -> SingleKtType(KtType(KtQualifiedName(unescapeIdentifier(type.getSymbol()!!.name))))

TypeFlags.Object in flags -> {
val objectFlags = (type as ObjectType).objectFlags
when {
ObjectFlags.Anonymous in objectFlags -> KtTypeUnion(mapAnonymousType(type, declaration))
ObjectFlags.ClassOrInterface in objectFlags -> KtTypeUnion(mapInterfaceType(type.unsafeCast<InterfaceType>(), declaration))
ObjectFlags.Reference in objectFlags -> KtTypeUnion(mapTypeReference(type.unsafeCast<TypeReference>(), declaration))
ObjectFlags.Anonymous in objectFlags -> SingleKtType(mapAnonymousType(type, declaration))
ObjectFlags.ClassOrInterface in objectFlags -> SingleKtType(mapInterfaceType(type.unsafeCast<InterfaceType>(), declaration))
ObjectFlags.Reference in objectFlags -> SingleKtType(mapTypeReference(type.unsafeCast<TypeReference>(), declaration))

else -> KtTypeUnion(KtType(ANY, isNullable = true))
else -> SingleKtType(KtType(ANY, isNullable = true))
}
}

TypeFlags.Enum in flags -> KtTypeUnion(mapObjectType(type.unsafeCast<ObjectType>()))
TypeFlags.Enum in flags -> SingleKtType(mapObjectType(type.unsafeCast<ObjectType>()))

else -> KtTypeUnion(KtType(ANY, isNullable = true))
else -> SingleKtType(KtType(ANY, isNullable = true))
}

typesInMappingProcess -= type

return mappedType
}

private fun ObjectTypeToKotlinTypeMapper.mapUnionType(type: UnionOrIntersectionType): KtTypeUnion {
val notNullTypes = type.types.filter {
TypeFlags.Undefined !in it.getFlags() &&
TypeFlags.Null !in it.getFlags()
private fun ObjectTypeToKotlinTypeMapper.mapUnionType(type: UnionType): EnhancedKtType {
val forceNullable = type.containsNull || type.containsUndefined
return mapUnionType(type.types.map { mapToEnhancedType(it, null) }).forceNullable(forceNullable)
}

/**
* Normalize to a KtTypeUnion such that equals will be true if the KotlinJS compiler would consider them
* conflicting if both were the only parameter to identically named functions.
*/
private fun EnhancedKtType.normalizeToDetectCompilationConflicts(): EnhancedKtType {
return when (this) {
is KtTypeUnion -> copy(possibleTypes = possibleTypes.map { it.normalizeToDetectCompilationConflicts() })
is KtTypeIntersection -> copy(requiredTypes = requiredTypes.map { it.normalizeToDetectCompilationConflicts() })
is SingleKtType -> copy(singleType = singleType.normalizeToDetectCompilationConflicts())
}
val nullable = notNullTypes.size != type.types.size || type.containsNull || type.containsUndefined
}

val mappedTypes = notNullTypes.map { mapType(it, null) }
return KtTypeUnion(when {
!nullable -> mappedTypes.distinct()
notNullTypes.size == 1 -> mappedTypes.map { it.copy(isNullable = true) }
else -> (mappedTypes + KtType(NOTHING, isNullable = true)).distinct()
})
/**
* Normalize to a KtType such that equals will be true if the KotlinJS compiler would consider them
* conflicting if both were the only parameter to identically named functions.
*/
private fun KtType.normalizeToDetectCompilationConflicts(): KtType {
return copy(comment = null, typeArgs = typeArgs.map { it.normalizeToDetectCompilationConflicts() })
}

/**
* Handle the case where a function is overloaded with the same Kotlin parameter types by merging them.
* Comments are not taken into account for the comparison, but are preserved by concatenation using "|" since a "union".
* This is especially useful for Typescript string literal unions.
*/
fun List<EnhancedKtType>.mergeToPreventCompilationConflicts(): List<EnhancedKtType> {
return groupBy { it.normalizeToDetectCompilationConflicts() }.map { entry ->
val comments = entry.value.mapNotNull { it.comment }
entry.key.withComment(comment = if (comments.isEmpty()) null else comments.joinToString(" | "))
}
}

/**
* Handle the case where a function is overloaded with the same Kotlin parameter types by merging them.
* Comments are not taken into account for the comparison, but are preserved by concatenation using "|" since a "union".
* This is especially useful for Typescript string literal unions.
*/
fun List<KtType>.mergeToPreventCompilationConflicts(): List<KtType> {
return groupBy { it.normalizeToDetectCompilationConflicts() }.map { entry ->
val comments = entry.value.mapNotNull { it.comment }
entry.key.copy(comment = if (comments.isEmpty()) null else comments.joinToString(" | "))
}
}

private inline val UnionOrIntersectionType.containsUndefined: Boolean
Expand All @@ -133,10 +160,8 @@ private inline val UnionOrIntersectionType.containsNull: Boolean
return jsTypeOf(array.containsNull) == "boolean" && array.containsNull.unsafeCast<Boolean>()
}

private fun ObjectTypeToKotlinTypeMapper.mapIntersectionType(type: IntersectionType): KtTypeUnion {
return KtTypeUnion(mapType(type.types.first(), null).copy(
comment = type.types.joinToString(" & ") { mapType(it, null).stringify() }
))
private fun ObjectTypeToKotlinTypeMapper.mapIntersectionType(type: IntersectionType): EnhancedKtType {
return KtTypeIntersection(type.types.map { SingleKtType(mapType(it, null)) })
}

private fun ObjectTypeToKotlinTypeMapper.mapTypeReference(type: TypeReference, declaration: Node?): KtType {
Expand Down Expand Up @@ -164,7 +189,7 @@ private fun ObjectTypeToKotlinTypeMapper.mapInterfaceType(type: InterfaceType, d

private fun ObjectTypeToKotlinTypeMapper.mapTypeArguments(
typeArguments: Array<Type>?, declaration: Node?
): Sequence<KtType> {
): Sequence<EnhancedKtType> {
val typeArgsFromDeclaration = if (declaration != null) {
when (declaration.kind as Any) {
SyntaxKind.ExpressionWithTypeArguments,
Expand All @@ -191,14 +216,14 @@ private fun ObjectTypeToKotlinTypeMapper.mapTypeArguments(
.asSequence()
.zip(typeArgsFromDeclaration + generateSequence { 0 }.map { null })
return typeArgsWithDeclarations.map { (argType, arg) ->
mapType(argType, arg)
mapToEnhancedType(argType, arg)
}
}

// TODO: is it correct name???
private fun ObjectTypeToKotlinTypeMapper.mapObjectType(type: Type): KtType {
val fqn = buildFqn(type.getSymbol()!!)
if (fqn == KtQualifiedName("Function")) return KtType(KtQualifiedName("Function"), typeArgs = listOf(starType()))
if (fqn == KtQualifiedName("Function")) return KtType(KtQualifiedName("Function"), typeArgs = listOf(SingleKtType(starType())))
return KtType(when (fqn) {
KtQualifiedName("Object") -> ANY
else -> {
Expand Down Expand Up @@ -306,6 +331,14 @@ fun KtType.replaceTypeParameters(substitution: kotlin.collections.Map<String, Kt
)
}

fun EnhancedKtType.replaceTypeParameters(substitution: kotlin.collections.Map<String, KtType>): EnhancedKtType {
return when (this) {
is KtTypeUnion -> this.copy(possibleTypes = this.possibleTypes.map { it.replaceTypeParameters(substitution) })
is KtTypeIntersection -> this.copy(requiredTypes = this.requiredTypes.map { it.replaceTypeParameters(substitution) })
is SingleKtType -> copy(singleType = singleType.replaceTypeParameters(substitution))
}
}

private fun KtCallSignature.replaceTypeParameters(substitution: Map<String, KtType>): KtCallSignature =
copy(
params = params.map { it.replaceTypeParameters(substitution) },
Expand Down
15 changes: 10 additions & 5 deletions src/main/kotlin/ts2kt/TsClassifierToKt.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ts2kt

import converter.mapType
import converter.mapTypeToUnion
import converter.mapToEnhancedType
import ts2kt.kotlin.ast.*
import ts2kt.utils.assert
import typescriptServices.ts.*
Expand Down Expand Up @@ -32,12 +32,17 @@ abstract class TsClassifierToKt(
private fun translateAccessor(node: IndexSignatureDeclaration, isGetter: Boolean, extendsType: KtType?) {
// TODO type params?
node.parameters.toKotlinParamsOverloads(typeMapper).forEach { params ->
val propTypeUnion = if (isGetter) {
KtTypeUnion(node.type?.let { typeMapper.mapType(it) } ?: KtType(ANY))
val propEnhancedType = if (isGetter) {
SingleKtType(node.type?.let { typeMapper.mapType(it) } ?: KtType(ANY))
} else {
node.type?.let { typeMapper.mapTypeToUnion(it) } ?: KtTypeUnion(KtType(ANY))
node.type?.let { typeMapper.mapToEnhancedType(it) } ?: SingleKtType(KtType(ANY))
}
propTypeUnion.possibleTypes.forEach { propType ->
val possibleTypes = if (propEnhancedType is KtTypeUnion) {
propEnhancedType.possibleTypes
} else {
listOf(propEnhancedType.singleType)
}
possibleTypes.forEach { propType ->
val callSignature: KtCallSignature
val accessorName: String
val annotation: KtAnnotation?
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/ts2kt/TsInterfaceToKtExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TsInterfaceToKtExtensions(

val cachedExtendsType by lazy { getExtendsType(typeParams) }

private fun getExtendsType(typeParams: List<KtTypeParam>?) = KtType(KtQualifiedName(name!!), typeParams?.map { KtType(KtQualifiedName(it.name)) } ?: emptyList())
private fun getExtendsType(typeParams: List<KtTypeParam>?) = KtType(KtQualifiedName(name!!), typeParams?.map { SingleKtType(KtType(KtQualifiedName(it.name))) } ?: emptyList())

fun List<KtTypeParam>?.fixIfClashWith(another: List<KtTypeParam>?): List<KtTypeParam>? {
if (this == null || another == null) return this
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/ts2kt/kotlin/ast/KtVisitor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface KtVisitor {
fun visitTypeParam(typeParam: KtTypeParam)
fun visitTypeAnnotation(typeAnnotation: KtTypeAnnotation)
fun visitType(type: KtType)
fun visitTypeIntersection(typeIntersection: KtTypeIntersection)
fun visitTypeUnion(typeUnion: KtTypeUnion)
fun visitHeritageType(heritageType: KtHeritageType)
fun visitArgument(argument: KtArgument)
Expand Down
23 changes: 20 additions & 3 deletions src/main/kotlin/ts2kt/kotlin/ast/Stringify.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Stringify(
private val topLevel: Boolean,
private val additionalImports: List<String> = listOf(),
private val suppressedDiagnostics: List<String> = listOf(),
private val allowEnhanced: Boolean = false,
private val out: Output = Output()
) : KtVisitor {
val result: String
Expand Down Expand Up @@ -346,9 +347,11 @@ class Stringify(
enumEntry.value?.let { out.print(" /* = $it */") }
}

private val EnhancedKtType.singleTypeIfNeeded: KtNode get() = if (allowEnhanced) this else singleType

override fun visitTypeParam(typeParam: KtTypeParam) {
out.print(typeParam.name.asString())
typeParam.upperBound?.let {
typeParam.upperBound?.singleTypeIfNeeded?.let {
out.print(" : ")
it.accept(this)
}
Expand Down Expand Up @@ -383,7 +386,7 @@ class Stringify(
else {
out.print(qualifiedName.asString())

typeArgs.acceptForEach(this@Stringify, ", ", startWithIfNotEmpty = "<", endWithIfNotEmpty = ">")
typeArgs.map { it.singleTypeIfNeeded }.acceptForEach(this@Stringify, ", ", startWithIfNotEmpty = "<", endWithIfNotEmpty = ">")
}

if (isNullable && qualifiedName != DYNAMIC) {
Expand All @@ -396,6 +399,20 @@ class Stringify(
}
}

override fun visitTypeIntersection(typeIntersection: KtTypeIntersection) {
if (typeIntersection.requiredTypes.size > 1 && typeIntersection.isNullable) {
out.print("(")
typeIntersection.requiredTypes.acceptForEach(this, " & ")
out.print(")?")
} else {
typeIntersection.requiredTypes.acceptForEach(this, " & ")
if (typeIntersection.isNullable) {
out.print("?")
}
}
}


override fun visitTypeUnion(typeUnion: KtTypeUnion) {
typeUnion.possibleTypes.acceptForEach(this, " | ")
}
Expand All @@ -415,5 +432,5 @@ class Stringify(
}

private fun innerStringifier() =
Stringify(packagePartPrefix, /*topLevel = */false, additionalImports, suppressedDiagnostics, out)
Stringify(packagePartPrefix, /*topLevel = */false, additionalImports, suppressedDiagnostics, allowEnhanced, out)
}
Loading