diff --git a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirWasmDiagnosticsList.kt b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirWasmDiagnosticsList.kt index 21197e79034..905a8e742b5 100644 --- a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirWasmDiagnosticsList.kt +++ b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirWasmDiagnosticsList.kt @@ -23,5 +23,9 @@ object WASM_DIAGNOSTICS_LIST : DiagnosticList("FirWasmErrors") { parameter("superType") } val CALL_TO_DEFINED_EXTERNALLY_FROM_NON_EXTERNAL_DECLARATION by error() + val WRONG_JS_INTEROP_TYPE by error(PositioningStrategy.DECLARATION_SIGNATURE_OR_DEFAULT) { + parameter("place") + parameter("type") + } } } diff --git a/compiler/fir/checkers/checkers.wasm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrors.kt b/compiler/fir/checkers/checkers.wasm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrors.kt index c6d7880a869..fc08e0cf07c 100644 --- a/compiler/fir/checkers/checkers.wasm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrors.kt +++ b/compiler/fir/checkers/checkers.wasm/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrors.kt @@ -23,6 +23,7 @@ object FirWasmErrors { val NON_EXTERNAL_TYPE_EXTENDS_EXTERNAL_TYPE by error1(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) val EXTERNAL_TYPE_EXTENDS_NON_EXTERNAL_TYPE by error1(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) val CALL_TO_DEFINED_EXTERNALLY_FROM_NON_EXTERNAL_DECLARATION by error0() + val WRONG_JS_INTEROP_TYPE by error2(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) init { RootDiagnosticRendererFactory.registerFactory(FirWasmErrorsDefaultMessages) diff --git a/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrorsDefaultMessages.kt b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrorsDefaultMessages.kt index 23037ebaba6..9dd4fec25f6 100644 --- a/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrorsDefaultMessages.kt +++ b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/diagnostics/wasm/FirWasmErrorsDefaultMessages.kt @@ -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, + ) } } diff --git a/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/FirWasmJsCodeHelpers.kt b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/FirWasmJsCodeHelpers.kt new file mode 100644 index 00000000000..adc168fd0a5 --- /dev/null +++ b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/FirWasmJsCodeHelpers.kt @@ -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 +} diff --git a/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/FirWasmJsExportHelpers.kt b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/FirWasmJsExportHelpers.kt new file mode 100644 index 00000000000..6700fcdc23c --- /dev/null +++ b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/FirWasmJsExportHelpers.kt @@ -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) +} \ No newline at end of file diff --git a/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/WasmDeclarationCheckers.kt b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/WasmDeclarationCheckers.kt index f1227e712aa..460c1fd587e 100644 --- a/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/WasmDeclarationCheckers.kt +++ b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/WasmDeclarationCheckers.kt @@ -13,4 +13,9 @@ object WasmDeclarationCheckers : DeclarationCheckers() { get() = setOf( FirWasmExternalInheritanceChecker, ) + + override val basicDeclarationCheckers: Set + get() = setOf( + FirWasmJsInteropTypesChecker, + ) } diff --git a/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/declaration/FirWasmJsInteropTypesChecker.kt b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/declaration/FirWasmJsInteropTypesChecker.kt new file mode 100644 index 00000000000..3eb57380f35 --- /dev/null +++ b/compiler/fir/checkers/checkers.wasm/src/org/jetbrains/kotlin/fir/analysis/wasm/checkers/declaration/FirWasmJsInteropTypesChecker.kt @@ -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 +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.fir.kt b/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.fir.kt index 704ca9ccf9d..19f5f243e13 100644 --- a/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.fir.kt +++ b/compiler/testData/diagnostics/wasmTests/jsInterop/jsCode.fir.kt @@ -9,7 +9,7 @@ fun funBlockBody(x: Int): Int { } fun returnTypeNotSepcified() = js("1") -val valTypeNotSepcified = js("1") +val valTypeNotSepcified = js("1") val a = "1" fun nonConst(): String = "1" diff --git a/compiler/testData/diagnostics/wasmTests/jsInterop/types.fir.kt b/compiler/testData/diagnostics/wasmTests/jsInterop/types.fir.kt index 69ce745e9ed..cbcd7dacbd6 100644 --- a/compiler/testData/diagnostics/wasmTests/jsInterop/types.fir.kt +++ b/compiler/testData/diagnostics/wasmTests/jsInterop/types.fir.kt @@ -67,45 +67,50 @@ external fun < T3 : T1? > supportedTypeParamtersUpperBounds(p1: T1, p2: T2): T3 +external fun supportedVararg(vararg p: Int) external fun wrongExternalTypes( - any: Any, - nany: Any?, - unit: Unit, - nunit: Unit?, - nothing: Nothing, - nnothing: Nothing?, - charSequence: CharSequence, - list: List, - array: Array, - intArray: IntArray, - pair: Pair, - number: Number, + any: Any, + nany: Any?, + unit: Unit, + nunit: Unit?, + nothing: Nothing, + nnothing: Nothing?, + charSequence: CharSequence, + list: List, + array: Array, + intArray: IntArray, + pair: Pair, + number: Number, ) -external fun supportedTypeParamtersUpperBounds(p: T): T where T : EI, T : Any +external fun wrongVararg(vararg p: Any) + +external fun supportedTypeParamtersUpperBounds(p: T): T where T : EI, T : Any external fun < - T1, - T2 : Number, - T3: List, + T1, + T2 : Number, + T3: List, > supportedTypeParamtersUpperBounds( p1: T1, p2: T2 ): T3 -fun jsCode1(x: Any): Any = js("x") -fun jsCode2(x: Any): Any { +fun jsCode1(x: Any): Any = js("x") +fun jsCode2(x: Any): Any { js("return x;") } -val jsProp: Any = js("1") +val jsProp: Any = js("1") -@JsExport -fun exported(x: Any): Any = x +@JsExport +fun exported(x: Any): Any = x typealias EI_alias = EI typealias Any_alias = Any +typealias Function_alias = (Int) -> Int external fun fooAlias( ei: EI_alias, - any: Any_alias, + any: Any_alias, + function: Function_alias, ) diff --git a/compiler/testData/diagnostics/wasmTests/jsInterop/types.kt b/compiler/testData/diagnostics/wasmTests/jsInterop/types.kt index 9e277058bec..dbb8d32b7c3 100644 --- a/compiler/testData/diagnostics/wasmTests/jsInterop/types.kt +++ b/compiler/testData/diagnostics/wasmTests/jsInterop/types.kt @@ -67,6 +67,7 @@ external fun < T3 : T1? > supportedTypeParamtersUpperBounds(p1: T1, p2: T2): T3 +external fun supportedVararg(vararg p: Int) external fun wrongExternalTypes( any: Any, @@ -83,6 +84,8 @@ external fun wrongExternalTypes( number: Number, ) +external fun wrongVararg(vararg p: Any) + external fun <T> supportedTypeParamtersUpperBounds(p: T): T where T : EI, T : Any external fun < @@ -105,7 +108,9 @@ fun exported(x: Any): Any = x typealias EI_alias = EI typealias Any_alias = Any +typealias Function_alias = (Int) -> Int external fun fooAlias( ei: EI_alias, any: Any_alias, + function: Function_alias, ) diff --git a/core/compiler.common.wasm/src/org/jetbrains/kotlin/name/WasmStandardClassIds.kt b/core/compiler.common.wasm/src/org/jetbrains/kotlin/name/WasmStandardClassIds.kt index 41e4c2593f8..cda2b4aaf32 100644 --- a/core/compiler.common.wasm/src/org/jetbrains/kotlin/name/WasmStandardClassIds.kt +++ b/core/compiler.common.wasm/src/org/jetbrains/kotlin/name/WasmStandardClassIds.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.name.StandardClassIds.BASE_KOTLIN_PACKAGE object WasmStandardClassIds { val BASE_JS_PACKAGE = BASE_KOTLIN_PACKAGE.child(Name.identifier("js")) + val BASE_WASM_PACKAGE = BASE_KOTLIN_PACKAGE.child(Name.identifier("wasm")) object Annotations { @JvmField @@ -22,14 +23,22 @@ object WasmStandardClassIds { @JvmField val JsExport = "JsExport".jsId() + + @JvmField + val WasmImport = "WasmImport".wasmId() } object Callables { @JvmField val JsDefinedExternally = "definedExternally".callableId(BASE_JS_PACKAGE) + + @JvmField + val Js = "js".callableId(BASE_JS_PACKAGE) } } private fun String.jsId() = ClassId(WasmStandardClassIds.BASE_JS_PACKAGE, Name.identifier(this)) +private fun String.wasmId() = ClassId(WasmStandardClassIds.BASE_WASM_PACKAGE, Name.identifier(this)) + private fun String.callableId(packageName: FqName) = CallableId(packageName, Name.identifier(this)) diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt index 81db8dbcc9c..12b13a27213 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmJsInteropTypesChecker.kt @@ -23,7 +23,6 @@ import org.jetbrains.kotlin.types.typeUtil.isUnit import org.jetbrains.kotlin.types.typeUtil.makeNotNullable import org.jetbrains.kotlin.wasm.util.hasValidJsCodeBody -// TODO: Implement in K2: KT-56849 object WasmJsInteropTypesChecker : DeclarationChecker { override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) { if (descriptor !is MemberDescriptor) @@ -119,7 +118,7 @@ private fun isTypeSupportedInJsInterop( return true } - // Interop type parameters upper bounds should are checked + // Interop type parameters upper bounds should be checked // on declaration site separately if (nonNullable.isTypeParameter()) { return true