[Wasm] Port JS interop type checker to K2 (KT-56849)

This commit is contained in:
Svyatoslav Kuzmich
2023-10-31 12:34:32 +00:00
committed by Space Team
parent 0c46cfd761
commit 15d3bf5e25
12 changed files with 306 additions and 27 deletions
@@ -23,6 +23,7 @@ object FirWasmErrors {
val NON_EXTERNAL_TYPE_EXTENDS_EXTERNAL_TYPE by error1<KtElement, ConeKotlinType>(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT)
val EXTERNAL_TYPE_EXTENDS_NON_EXTERNAL_TYPE by error1<KtElement, ConeKotlinType>(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT)
val CALL_TO_DEFINED_EXTERNALLY_FROM_NON_EXTERNAL_DECLARATION by error0<PsiElement>()
val WRONG_JS_INTEROP_TYPE by error2<KtElement, String, ConeKotlinType>(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT)
init {
RootDiagnosticRendererFactory.registerFactory(FirWasmErrorsDefaultMessages)
@@ -6,17 +6,33 @@
package org.jetbrains.kotlin.fir.analysis.diagnostics.wasm
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactoryToRendererMap
import org.jetbrains.kotlin.diagnostics.KtDiagnosticRenderers
import org.jetbrains.kotlin.diagnostics.KtDiagnosticRenderers.TO_STRING
import org.jetbrains.kotlin.diagnostics.rendering.BaseDiagnosticRendererFactory
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirDiagnosticRenderers
import org.jetbrains.kotlin.fir.analysis.diagnostics.wasm.FirWasmErrors.CALL_TO_DEFINED_EXTERNALLY_FROM_NON_EXTERNAL_DECLARATION
import org.jetbrains.kotlin.fir.analysis.diagnostics.wasm.FirWasmErrors.EXTERNAL_TYPE_EXTENDS_NON_EXTERNAL_TYPE
import org.jetbrains.kotlin.fir.analysis.diagnostics.wasm.FirWasmErrors.NON_EXTERNAL_TYPE_EXTENDS_EXTERNAL_TYPE
import org.jetbrains.kotlin.fir.analysis.diagnostics.wasm.FirWasmErrors.WRONG_JS_INTEROP_TYPE
@Suppress("unused")
object FirWasmErrorsDefaultMessages : BaseDiagnosticRendererFactory() {
override val MAP = KtDiagnosticFactoryToRendererMap("FIR").also { map ->
map.put(NON_EXTERNAL_TYPE_EXTENDS_EXTERNAL_TYPE, "Non-external type extends external type {0}", FirDiagnosticRenderers.RENDER_TYPE)
map.put(EXTERNAL_TYPE_EXTENDS_NON_EXTERNAL_TYPE, "External type extends non-external type {0}", FirDiagnosticRenderers.RENDER_TYPE)
map.put(
NON_EXTERNAL_TYPE_EXTENDS_EXTERNAL_TYPE,
"Non-external type extends external type ''{0}''",
FirDiagnosticRenderers.RENDER_TYPE
)
map.put(
EXTERNAL_TYPE_EXTENDS_NON_EXTERNAL_TYPE,
"External type extends non-external type ''{0}''",
FirDiagnosticRenderers.RENDER_TYPE
)
map.put(CALL_TO_DEFINED_EXTERNALLY_FROM_NON_EXTERNAL_DECLARATION, "This property can only be used from external declarations.")
map.put(
WRONG_JS_INTEROP_TYPE,
"Type ''{0}'' cannot be used in {1}. Only external, primitive, string and function types are supported in Kotlin/Wasm JS interop.",
TO_STRING, FirDiagnosticRenderers.RENDER_TYPE,
)
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.fir.analysis.wasm.checkers
import org.jetbrains.kotlin.fir.declarations.FirProperty
import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction
import org.jetbrains.kotlin.fir.expressions.FirBlock
import org.jetbrains.kotlin.fir.expressions.FirExpression
import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.expressions.FirReturnExpression
import org.jetbrains.kotlin.fir.expressions.impl.FirSingleExpressionBlock
import org.jetbrains.kotlin.fir.references.toResolvedCallableSymbol
import org.jetbrains.kotlin.name.WasmStandardClassIds
fun FirSimpleFunction.hasValidJsCodeBody(): Boolean =
body?.isValidJsCodeBody() == true
fun FirProperty.hasValidJsCodeBody(): Boolean =
this.initializer?.isJsCodeCall() == true
private fun FirBlock.isValidJsCodeBody(): Boolean {
val singleStatement = statements.singleOrNull()
?: return false
return when {
singleStatement is FirFunctionCall ->
singleStatement.isJsCodeCall()
singleStatement is FirReturnExpression && this is FirSingleExpressionBlock ->
singleStatement.result.isJsCodeCall()
else ->
false
}
}
private fun FirExpression.isJsCodeCall(): Boolean {
if (this !is FirFunctionCall)
return false
val symbol = calleeReference.toResolvedCallableSymbol()
?: return false
return symbol.callableId == WasmStandardClassIds.Callables.Js
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.fir.analysis.wasm.checkers
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.declarations.FirDeclaration
import org.jetbrains.kotlin.fir.declarations.FirSimpleFunction
import org.jetbrains.kotlin.fir.declarations.hasAnnotation
import org.jetbrains.kotlin.fir.declarations.utils.visibility
import org.jetbrains.kotlin.fir.resolve.providers.firProvider
import org.jetbrains.kotlin.name.WasmStandardClassIds
fun isJsExportedDeclaration(declaration: FirDeclaration, session: FirSession): Boolean {
if (declaration !is FirSimpleFunction)
return false
if (declaration.visibility != Visibilities.Public)
return false
if (declaration.hasAnnotation(WasmStandardClassIds.Annotations.JsExport, session))
return true
val containerFile = session.firProvider.getFirCallableContainerFile(declaration.symbol)
return containerFile != null && containerFile.hasAnnotation(WasmStandardClassIds.Annotations.JsExport, session)
}
@@ -13,4 +13,9 @@ object WasmDeclarationCheckers : DeclarationCheckers() {
get() = setOf(
FirWasmExternalInheritanceChecker,
)
override val basicDeclarationCheckers: Set<FirBasicDeclarationChecker>
get() = setOf(
FirWasmJsInteropTypesChecker,
)
}
@@ -0,0 +1,158 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.fir.analysis.wasm.checkers.declaration
import org.jetbrains.kotlin.KtSourceElement
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.declaration.FirBasicDeclarationChecker
import org.jetbrains.kotlin.fir.analysis.diagnostics.wasm.FirWasmErrors
import org.jetbrains.kotlin.fir.analysis.wasm.checkers.hasValidJsCodeBody
import org.jetbrains.kotlin.fir.analysis.wasm.checkers.isJsExportedDeclaration
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.utils.isEffectivelyExternal
import org.jetbrains.kotlin.fir.declarations.utils.isEnumClass
import org.jetbrains.kotlin.fir.resolve.fullyExpandedType
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.name.WasmStandardClassIds
object FirWasmJsInteropTypesChecker : FirBasicDeclarationChecker() {
override fun check(declaration: FirDeclaration, context: CheckerContext, reporter: DiagnosticReporter) {
val session = context.session
fun isExternalJsInteropDeclaration() =
declaration.symbol.isEffectivelyExternal(session) &&
!declaration.annotations.hasAnnotation(WasmStandardClassIds.Annotations.WasmImport, session)
fun isJsCodeDeclaration() =
(declaration is FirSimpleFunction && declaration.hasValidJsCodeBody()) ||
(declaration is FirProperty && declaration.hasValidJsCodeBody())
fun isJsExportDeclaration() =
declaration is FirSimpleFunction && isJsExportedDeclaration(declaration, session)
// Interop type restriction rules apply uniformly to external, js("code"), and @JsExport declarations
if (
!isExternalJsInteropDeclaration() &&
!isJsCodeDeclaration() &&
!isJsExportDeclaration()
) {
return
}
// Skip enums to avoid reporting errors for synthetic static members with unsupported interop types.
// External enums errors are reported in a separate checker.
if (context.containingDeclarations.any { it is FirClass && it.isEnumClass }) {
return
}
fun ConeKotlinType.checkJsInteropType(
typePositionDescription: String,
source: KtSourceElement?,
isInFunctionReturnPosition: Boolean = false,
) {
if (!isTypeSupportedInJsInterop(this, isInFunctionReturnPosition, session)) {
reporter.reportOn(
source,
FirWasmErrors.WRONG_JS_INTEROP_TYPE,
typePositionDescription,
this,
context
)
}
}
fun FirTypeParameterRef.checkJsInteropTypeParameter() {
for (upperBound in this.symbol.resolvedBounds) {
upperBound.type.checkJsInteropType(
"JS interop type parameter upper bound",
upperBound.source ?: this.source
)
}
}
if (declaration is FirMemberDeclaration) {
for (typeParameter in declaration.typeParameters) {
typeParameter.checkJsInteropTypeParameter()
}
}
when (declaration) {
is FirProperty -> {
declaration.returnTypeRef.coneType.checkJsInteropType(
"JS interop property",
declaration.source
)
}
is FirFunction -> {
for (parameter in declaration.valueParameters) {
val type = parameter.returnTypeRef.coneType
val varargElementTypeOrType = if (parameter.isVararg) type.varargElementType() else type
varargElementTypeOrType.checkJsInteropType(
"JS interop function parameter",
parameter.source
)
}
declaration.returnTypeRef.coneType.checkJsInteropType(
"JS interop function return",
declaration.source,
isInFunctionReturnPosition = true
)
}
else -> {}
}
}
}
private fun isTypeSupportedInJsInterop(
unexpandedType: ConeKotlinType,
isInFunctionReturnPosition: Boolean,
session: FirSession,
): Boolean {
val type = unexpandedType.fullyExpandedType(session)
if (type.isUnit || type.isNothing) {
return isInFunctionReturnPosition
}
val nonNullable = type.withNullability(ConeNullability.NOT_NULL, session.typeContext)
if (nonNullable.isPrimitive || nonNullable.isString) {
return true
}
// Interop type parameters upper bounds should be checked
// on declaration site separately
if (nonNullable is ConeTypeParameterType) {
return true
}
val regularClassSymbol = nonNullable.toRegularClassSymbol(session)
if (regularClassSymbol?.isEffectivelyExternal(session) == true) {
return true
}
if (type.isBasicFunctionType(session)) {
val arguments = type.typeArguments
for (i in 0 until arguments.lastIndex) {
val argType = arguments[i].type ?: return false
if (!isTypeSupportedInJsInterop(argType, isInFunctionReturnPosition = false, session)) {
return false
}
}
val returnType = arguments.last().type ?: return false
return isTypeSupportedInJsInterop(
returnType,
isInFunctionReturnPosition = true,
session
)
}
return false
}