[K/JS] fix(Interface Export): prevent structural identity between two interfaces.

This commit is contained in:
Artem Kobzar
2022-06-28 15:44:38 +00:00
committed by Space
parent c3c09aa95a
commit 133c6713ca
17 changed files with 133 additions and 122 deletions
@@ -51,14 +51,14 @@ data class ExportedConstructSignature(
class ExportedProperty(
val name: String,
val type: ExportedType,
val mutable: Boolean,
val mutable: Boolean = true,
val isMember: Boolean = false,
val isStatic: Boolean = false,
val isAbstract: Boolean,
val isProtected: Boolean,
val isField: Boolean,
val irGetter: IrFunction?,
val irSetter: IrFunction?,
val isAbstract: Boolean = false,
val isProtected: Boolean = false,
val isField: Boolean = false,
val irGetter: IrFunction? = null,
val irSetter: IrFunction? = null,
) : ExportedDeclaration()
@@ -117,6 +117,7 @@ sealed class ExportedType {
object Any : Primitive("any")
object Unit : Primitive("void")
object Nothing : Primitive("never")
object UniqueSymbol : Primitive("unique symbol")
}
sealed class LiteralType<T : Any>(val value: T) : ExportedType() {
@@ -144,6 +145,8 @@ sealed class ExportedType {
class IntersectionType(val lhs: ExportedType, val rhs: ExportedType) : ExportedType()
class PropertyType(val container: ExportedType, val propertyName: ExportedType) : ExportedType()
class ImplicitlyExportedType(val type: ExportedType) : ExportedType() {
override fun withNullability(nullable: Boolean) =
ImplicitlyExportedType(type.withNullability(nullable))
@@ -22,11 +22,12 @@ import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol
import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.serialization.js.ModuleKind
import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.kotlin.utils.keysToMap
private const val magicPropertyName = "__doNotUseOrImplementIt"
class ExportModelGenerator(
val context: JsIrBackendContext,
val generateNamespacesForPackages: Boolean
@@ -133,11 +134,10 @@ class ExportModelGenerator(
val parentClass = property.parent as? IrClass
return ExportedProperty(
property.getExportedIdentifier(),
specializeType ?: exportType(property.getter!!.returnType),
name = property.getExportedIdentifier(),
type = specializeType ?: exportType(property.getter!!.returnType),
mutable = property.isVar,
isMember = parentClass != null,
isStatic = false,
isAbstract = parentClass?.isInterface == false && property.modality == Modality.ABSTRACT,
isProtected = property.visibility == DescriptorVisibilities.PROTECTED,
isField = parentClass?.isInterface == true,
@@ -156,18 +156,7 @@ class ExportModelGenerator(
val ordinal = enumEntries.getValue(irEnumEntry)
fun fakeProperty(name: String, type: ExportedType) =
ExportedProperty(
name = name,
type = type,
mutable = false,
isMember = true,
isStatic = false,
isAbstract = false,
isProtected = false,
irGetter = null,
irSetter = null,
isField = false,
)
ExportedProperty(name = name, type = type, mutable = false, isMember = true)
val nameProperty = fakeProperty(
name = "name",
@@ -189,12 +178,9 @@ class ExportModelGenerator(
mutable = false,
isMember = true,
isStatic = true,
isAbstract = false,
isProtected = parentClass.visibility == DescriptorVisibilities.PROTECTED,
irGetter = context.mapping.enumEntryToGetInstanceFun[irEnumEntry]
?: error("Unable to find get instance fun for ${field.fqNameWhenAvailable}"),
irSetter = null,
isField = false,
)
}
@@ -324,12 +310,69 @@ class ExportModelGenerator(
}
}
if (klass.shouldContainImplementationOfMagicProperty()) {
members.addMagicPropertyForInterfaceImplementation(klass)
} else if (klass.shouldNotBeImplemented()) {
members.addMagicInterfaceProperty(klass)
}
return ExportedClassDeclarationsInfo(
members,
nestedClasses
)
}
private fun IrClass.shouldNotBeImplemented(): Boolean {
return isInterface && !isExternal
}
private fun IrClass.shouldContainImplementationOfMagicProperty(): Boolean {
return !isExternal && superTypes.any {
val superClass = it.classOrNull?.owner ?: return@any false
superClass.isInterface && superClass.isExported(context)
}
}
private fun MutableList<ExportedDeclaration>.addMagicInterfaceProperty(klass: IrClass) {
add(ExportedProperty(name = magicPropertyName, type = klass.generateTagType(), mutable = false, isMember = true, isField = true))
}
private fun MutableList<ExportedDeclaration>.addMagicPropertyForInterfaceImplementation(klass: IrClass) {
val superTypesToInheritanceMagicProperty = klass.superTypes.filter { it.shouldAddMagicPropertyOfSuper() }
if (superTypesToInheritanceMagicProperty.isEmpty()) return
val intersectionOfTypes = superTypesToInheritanceMagicProperty
.map { ExportedType.PropertyType(exportType(it), ExportedType.LiteralType.StringLiteralType(magicPropertyName)) }
.reduce(ExportedType::IntersectionType)
.let { if (klass.shouldNotBeImplemented()) ExportedType.IntersectionType(klass.generateTagType(), it) else it }
add(ExportedProperty(name = magicPropertyName, type = intersectionOfTypes, mutable = false, isMember = true, isField = true))
}
private fun IrType.shouldAddMagicPropertyOfSuper(): Boolean {
return classOrNull?.owner?.isOwnMagicPropertyAdded() ?: false
}
private fun IrClass.isOwnMagicPropertyAdded(): Boolean {
if (!isExported(context)) return false
return isInterface && !isExternal || superTypes.any { it.classOrNull?.owner?.isOwnMagicPropertyAdded() ?: false }
}
private fun IrClass.generateTagType(): ExportedType {
return ExportedType.InlineInterfaceType(
listOf(
ExportedProperty(
name = getFqNameWithJsNameWhenAvailable(true).asString(),
type = ExportedType.Primitive.UniqueSymbol,
mutable = false,
isMember = true,
isField = true,
)
)
)
}
private fun exportClass(
klass: IrClass,
members: List<ExportedDeclaration>,
@@ -15,9 +15,7 @@ import org.jetbrains.kotlin.ir.util.parentAsClass
import org.jetbrains.kotlin.js.common.isValidES5Identifier
import org.jetbrains.kotlin.serialization.js.ModuleKind
private const val declare = "declare"
private const val Nullable = "Nullable"
private const val doNotImplementIt = "__doNotImplementIt"
private const val objects = "_objects_"
private const val syntheticObjectNameSeparator = '$'
@@ -37,15 +35,9 @@ class ExportModelToTsDeclarations {
get() = if (this == ModuleKind.PLAIN) " " else ""
fun generateTypeScript(name: String, module: ExportedModule): String {
val declareKeyword = when (module.moduleKind) {
ModuleKind.PLAIN -> ""
else -> "$declare "
}
val types = """
type $Nullable<T> = T | null | undefined
${declareKeyword}const $doNotImplementIt: unique symbol
type $doNotImplementIt = typeof $doNotImplementIt
""".trimIndent().prependIndent(module.moduleKind.indent) + "\n"
type $Nullable<T> = T | null | undefined
""".trimIndent().prependIndent(module.moduleKind.indent) + "\n"
val declarationsDts = types + module.declarations.toTypeScript(module.moduleKind)
@@ -152,6 +144,7 @@ class ExportModelToTsDeclarations {
isAbstract -> "abstract "
else -> ""
}
else -> "function "
}
@@ -209,11 +202,8 @@ class ExportModelToTsDeclarations {
mutable = false,
isMember = maybeParentClass != null && !shouldRenderSeparatedAbstractClass,
isStatic = !ir.isInner && maybeParentClass?.isObject == false,
isAbstract = false,
isProtected = ir.visibility == DescriptorVisibilities.PROTECTED,
irGetter = irGetter,
irSetter = null,
isField = false,
)
return if (!shouldRenderSeparatedAbstractClass) {
@@ -247,16 +237,14 @@ class ExportModelToTsDeclarations {
val (memberObjects, nestedDeclarations) = nestedClasses.partition { it.couldBeProperty() }
val members = members
.let { if (shouldNotBeImplemented()) it.withMagicProperty() else it }
.map {
if (!ir.isInner || it !is ExportedFunction || !it.isStatic) {
it
} else {
// Remove $outer argument from secondary constructors of inner classes
it.copy(parameters = it.parameters.drop(1))
}
} + memberObjects
val members = members.map {
if (!ir.isInner || it !is ExportedFunction || !it.isStatic) {
it
} else {
// Remove $outer argument from secondary constructors of inner classes
it.copy(parameters = it.parameters.drop(1))
}
} + memberObjects
val (innerClasses, nonInnerClasses) = nestedDeclarations.partition { it.ir.isInner }
val innerClassesProperties = innerClasses.map { it.toReadonlyProperty() }
@@ -308,41 +296,16 @@ class ExportModelToTsDeclarations {
return when {
exportedInterfaces.isEmpty() && nonExportedInterfaces.isNotEmpty() ->
" /* $superInterfacesKeyword $listOfNonExportedInterfaces */"
exportedInterfaces.isNotEmpty() -> {
val nonExportedInterfacesTsString = if (nonExportedInterfaces.isNotEmpty()) "/*, $listOfNonExportedInterfaces */" else ""
" $superInterfacesKeyword " + exportedInterfaces.joinToString(", ") { it.toTypeScript(indent) } + nonExportedInterfacesTsString
}
else -> ""
}
}
private fun ExportedRegularClass.shouldNotBeImplemented(): Boolean {
return (isInterface && !ir.isExternal) || superInterfaces.any { it is ExportedType.ClassType && !it.ir.isExternal }
}
private fun List<ExportedDeclaration>.withMagicProperty(): List<ExportedDeclaration> {
return plus(
ExportedProperty(
"__doNotUseIt",
ExportedType.TypeParameter(doNotImplementIt),
mutable = false,
isMember = true,
isStatic = false,
isAbstract = false,
isProtected = false,
isField = true,
irGetter = null,
irSetter = null,
)
)
}
private fun IrClass.asNestedClassAccess(): String {
val name = getJsNameOrKotlinName().identifier
if (parent !is IrClass) return name
return "${parentAsClass.asNestedClassAccess()}.$name"
}
private fun ExportedClass.withProtectedConstructors(): ExportedRegularClass {
return (this as ExportedRegularClass).copy(members = members.map {
if (it !is ExportedConstructor || it.isProtected) {
@@ -371,18 +334,7 @@ class ExportModelToTsDeclarations {
ExportedType.TypeOf(innerClassReference)
)
return ExportedProperty(
name = name,
type = type,
mutable = false,
isMember = true,
isStatic = false,
isAbstract = false,
isProtected = false,
isField = false,
irGetter = null,
irSetter = null
)
return ExportedProperty(name = name, type = type, mutable = false, isMember = true)
}
private fun ExportedParameter.toTypeScript(indent: String): String {
@@ -392,6 +344,12 @@ class ExportModelToTsDeclarations {
return "$name$questionMark: $type"
}
private fun IrClass.asNestedClassAccess(): String {
val name = getJsNameOrKotlinName().identifier
if (parent !is IrClass) return name
return "${parentAsClass.asNestedClassAccess()}.$name"
}
private fun ExportedType.toTypeScript(indent: String, isInCommentContext: Boolean = false): String = when (this) {
is ExportedType.Primitive -> typescript
is ExportedType.Array -> "Array<${elementType.toTypeScript(indent, isInCommentContext)}>"
@@ -403,6 +361,7 @@ class ExportModelToTsDeclarations {
is ExportedType.ClassType ->
name + if (arguments.isNotEmpty()) "<${arguments.joinToString(", ") { it.toTypeScript(indent, isInCommentContext) }}>" else ""
is ExportedType.TypeOf ->
"typeof $name"
@@ -411,18 +370,29 @@ class ExportModelToTsDeclarations {
is ExportedType.InlineInterfaceType -> {
members.joinToString(prefix = "{\n", postfix = "$indent}", separator = "") { it.toTypeScript("$indent ") + "\n" }
}
is ExportedType.IntersectionType -> {
lhs.toTypeScript(indent) + " & " + rhs.toTypeScript(indent, isInCommentContext)
}
is ExportedType.UnionType -> {
lhs.toTypeScript(indent) + " | " + rhs.toTypeScript(indent, isInCommentContext)
}
is ExportedType.LiteralType.StringLiteralType -> "\"$value\""
is ExportedType.LiteralType.NumberLiteralType -> value.toString()
is ExportedType.ImplicitlyExportedType -> {
val typeString = type.toTypeScript("", true)
if (isInCommentContext) typeString else ExportedType.Primitive.Any.toTypeScript(indent) + "/* $typeString */"
}
is ExportedType.PropertyType -> "${container.toTypeScript(indent, isInCommentContext)}[${
propertyName.toTypeScript(
indent,
isInCommentContext
)
}]"
is ExportedType.TypeParameter -> if (constraint == null) {
name
} else {
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
class TestInner {
constructor(a: string);
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
class ClassWithDefaultCtor {
constructor();
get x(): string;
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
const _val: number;
let _var: number;
@@ -138,13 +136,24 @@ declare namespace JS_TESTS {
interface TestInterface {
readonly value: string;
getOwnerName(): string;
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: {
readonly "foo.TestInterface": unique symbol;
};
}
interface AnotherExportedInterface {
readonly __doNotUseOrImplementIt: {
readonly "foo.AnotherExportedInterface": unique symbol;
};
}
class TestInterfaceImpl implements foo.TestInterface {
constructor(value: string);
get value(): string;
getOwnerName(): string;
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: foo.TestInterface["__doNotUseOrImplementIt"];
}
class ChildTestInterfaceImpl extends foo.TestInterfaceImpl implements foo.AnotherExportedInterface {
constructor();
readonly __doNotUseOrImplementIt: foo.TestInterfaceImpl["__doNotUseOrImplementIt"] & foo.AnotherExportedInterface["__doNotUseOrImplementIt"];
}
function processInterface(test: foo.TestInterface): string;
class OuterClass {
@@ -202,10 +202,16 @@ interface TestInterface {
}
@JsExport
class TestInterfaceImpl(override val value: String) : TestInterface {
interface AnotherExportedInterface
@JsExport
open class TestInterfaceImpl(override val value: String) : TestInterface {
override fun getOwnerName() = "TestInterfaceImpl"
}
@JsExport
class ChildTestInterfaceImpl(): TestInterfaceImpl("Test"), AnotherExportedInterface
@JsExport
fun processInterface(test: TestInterface): String {
return "Owner ${test.getOwnerName()} has value '${test.value}'"
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
@@ -1,10 +1,10 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
interface ExportedInterface {
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: {
readonly "foo.ExportedInterface": unique symbol;
};
}
function producer(value: number): any/* foo.NonExportedType */;
function consumer(value: any/* foo.NonExportedType */): number;
@@ -22,11 +22,11 @@ declare namespace JS_TESTS {
}
class D implements foo.ExportedInterface/*, foo.NonExportedInterface */ {
constructor();
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: foo.ExportedInterface["__doNotUseOrImplementIt"];
}
class E /* extends foo.NonExportedType */ implements foo.ExportedInterface {
constructor();
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: foo.ExportedInterface["__doNotUseOrImplementIt"];
}
class F extends foo.A /* implements foo.NonExportedInterface */ {
constructor();
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
interface I<T, S, U> {
x: T;
@@ -43,7 +41,9 @@ declare namespace JS_TESTS {
bar: string;
readonly baz: string;
bay(): string;
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: {
readonly "foo.I3": unique symbol;
};
}
function getI3(): foo.I3;
function getA(): foo.I3;
@@ -56,7 +56,7 @@ declare namespace JS_TESTS {
abstract set bar(value: string);
abstract get baz(): string;
abstract bay(): string;
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: foo.I3["__doNotUseOrImplementIt"];
}
class B2 extends foo.A2 {
constructor();
@@ -98,7 +98,7 @@ declare namespace JS_TESTS {
get name(): "EC1" | "EC2" | "EC3";
get ordinal(): 0 | 1 | 2;
abstract get baz(): string;
readonly __doNotUseIt: __doNotImplementIt;
readonly __doNotUseOrImplementIt: foo.I3["__doNotUseOrImplementIt"];
}
}
}
@@ -1,6 +1,4 @@
type Nullable<T> = T | null | undefined
declare const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
export namespace foo {
const prop: number;
class C {
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
const prop: number;
class C {
@@ -1,6 +1,4 @@
type Nullable<T> = T | null | undefined
declare const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
export namespace foo {
const prop: number;
class C {
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo.bar.baz {
class C1 {
constructor(value: string);
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
const _any: any;
const _throwable: Error;
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
namespace foo {
interface ExportedInternalInterface {
}
@@ -1,7 +1,5 @@
declare namespace JS_TESTS {
type Nullable<T> = T | null | undefined
const __doNotImplementIt: unique symbol
type __doNotImplementIt = typeof __doNotImplementIt
interface publicInterface {
}
const publicVal: number;