From e8be3043cca483ba9b069f90a298ba9e177c2a9c Mon Sep 17 00:00:00 2001 From: Alexander Korepanov Date: Tue, 21 Mar 2023 15:35:52 +0100 Subject: [PATCH] [JS IR] Allow restriction of function argument by external type Add a special annotation @JsExternalTypeArgument for marking function parameters. The marked parameter accepts an argument with an external type only. ^KT-57479 Fixed --- .../diagnostics/KtFirDataClassConverters.kt | 7 + .../api/fir/diagnostics/KtFirDiagnostics.kt | 5 + .../fir/diagnostics/KtFirDiagnosticsImpl.kt | 6 + .../diagnostics/FirJsDiagnosticsList.kt | 3 + .../analysis/diagnostics/js/FirJsErrors.kt | 1 + .../js/FirJsErrorsDefaultMessages.kt | 6 + .../js/checkers/JsExpressionCheckers.kt | 5 + .../FirJsExternalArgumentCallChecker.kt | 42 +++++ .../testsWithJsStdLib/jsExternalArgument.kt | 143 ++++++++++++++++++ .../testsWithJsStdLib/jsExternalArgument.txt | 61 ++++++++ .../DiagnosticsTestWithJsStdLibGenerated.java | 6 + .../kotlin/name/JsStandardClassIds.kt | 3 + .../js/resolve/JsPlatformConfigurator.kt | 3 +- .../diagnostics/DefaultErrorMessagesJs.kt | 4 + .../js/resolve/diagnostics/ErrorsJs.java | 3 + .../JsExternalArgumentCallChecker.kt | 32 ++++ .../js/translate/utils/AnnotationsUtils.java | 5 + ...JsOldFrontendDiagnosticsTestGenerated.java | 6 + libraries/stdlib/api/js-v1/kotlin.js.kt | 8 + libraries/stdlib/api/js/kotlin.js.kt | 8 + libraries/stdlib/js/src/kotlin/annotations.kt | 38 +++++ 21 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/declaration/FirJsExternalArgumentCallChecker.kt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.kt create mode 100644 compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.txt create mode 100644 js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsExternalArgumentCallChecker.kt diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt index bb2d362017f..18cb425f02b 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDataClassConverters.kt @@ -5047,6 +5047,13 @@ internal val KT_DIAGNOSTIC_CONVERTER = KtDiagnosticConverterBuilder.buildConvert token, ) } + add(FirJsErrors.JS_EXTERNAL_ARGUMENT) { firDiagnostic -> + JsExternalArgumentImpl( + firSymbolBuilder.typeBuilder.buildKtType(firDiagnostic.a), + firDiagnostic as KtPsiDiagnostic, + token, + ) + } add(FirJsErrors.NESTED_JS_EXPORT) { firDiagnostic -> NestedJsExportImpl( firDiagnostic as KtPsiDiagnostic, diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt index 63ed02a1b9f..1b192d8d3bc 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnostics.kt @@ -3513,6 +3513,11 @@ sealed class KtFirDiagnostic : KtDiagnosticWithPsi { abstract val kid: KtClassLikeSymbol } + abstract class JsExternalArgument : KtFirDiagnostic() { + override val diagnosticClass get() = JsExternalArgument::class + abstract val argType: KtType + } + abstract class NestedJsExport : KtFirDiagnostic() { override val diagnosticClass get() = NestedJsExport::class } diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt index 2304a9b57e3..c848402d402 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/diagnostics/KtFirDiagnosticsImpl.kt @@ -4249,6 +4249,12 @@ internal class JsExternalInheritorsOnlyImpl( override val token: KtLifetimeToken, ) : KtFirDiagnostic.JsExternalInheritorsOnly(), KtAbstractFirDiagnostic +internal class JsExternalArgumentImpl( + override val argType: KtType, + override val firDiagnostic: KtPsiDiagnostic, + override val token: KtLifetimeToken, +) : KtFirDiagnostic.JsExternalArgument(), KtAbstractFirDiagnostic + internal class NestedJsExportImpl( override val firDiagnostic: KtPsiDiagnostic, override val token: KtLifetimeToken, diff --git a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt index e436a99c848..f14fd67993b 100644 --- a/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt +++ b/compiler/fir/checkers/checkers-component-generator/src/org/jetbrains/kotlin/fir/checkers/generator/diagnostics/FirJsDiagnosticsList.kt @@ -99,6 +99,9 @@ object JS_DIAGNOSTICS_LIST : DiagnosticList("FirJsErrors") { parameter>("parent") parameter>("kid") } + val JS_EXTERNAL_ARGUMENT by error(PositioningStrategy.DECLARATION_SIGNATURE_OR_DEFAULT) { + parameter("argType") + } } val EXPORT by object : DiagnosticGroup("Export") { diff --git a/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt b/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt index 2eee4bb2b72..317e0a82d91 100644 --- a/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt +++ b/compiler/fir/checkers/checkers.js/gen/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrors.kt @@ -80,6 +80,7 @@ object FirJsErrors { val UNCHECKED_CAST_TO_EXTERNAL_INTERFACE by warning2() val EXTERNAL_INTERFACE_AS_CLASS_LITERAL by error0() val JS_EXTERNAL_INHERITORS_ONLY by error2, FirClassLikeSymbol<*>>(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) + val JS_EXTERNAL_ARGUMENT by error1(SourceElementPositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT) // Export val NESTED_JS_EXPORT by error0() diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt index 56df8d43985..58318297f3e 100644 --- a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/diagnostics/js/FirJsErrorsDefaultMessages.kt @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.INLINE_CLASS import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.ENUM_CLASS_IN_EXTERNAL_DECLARATION_WARNING import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_EXTERNAL_INHERITORS_ONLY import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.INLINE_EXTERNAL_DECLARATION +import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_EXTERNAL_ARGUMENT import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_MODULE_PROHIBITED_ON_NON_NATIVE import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.JS_MODULE_PROHIBITED_ON_VAR import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors.NESTED_JS_MODULE_PROHIBITED @@ -163,6 +164,11 @@ object FirJsErrorsDefaultMessages : BaseDiagnosticRendererFactory() { FirDiagnosticRenderers.RENDER_CLASS_OR_OBJECT_NAME, FirDiagnosticRenderers.RENDER_CLASS_OR_OBJECT_NAME ) + map.put( + JS_EXTERNAL_ARGUMENT, + "Expected argument with external type, but type {0} is non-external", + FirDiagnosticRenderers.RENDER_TYPE + ) map.put(JS_NAME_PROHIBITED_FOR_EXTENSION_PROPERTY, "@JsName is prohibited for extension properties") map.put(JS_NAME_IS_NOT_ON_ALL_ACCESSORS, "@JsName should be on all the property accessors") map.put(JS_NAME_PROHIBITED_FOR_NAMED_NATIVE, "@JsName is prohibited for external declaration with explicit name") diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt index c4a4364e981..453e3d4239f 100644 --- a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/JsExpressionCheckers.kt @@ -25,4 +25,9 @@ object JsExpressionCheckers : ExpressionCheckers() { get() = setOf( FirJsDynamicCallChecker, ) + + override val callCheckers: Set + get() = setOf( + FirJsExternalArgumentCallChecker + ) } diff --git a/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/declaration/FirJsExternalArgumentCallChecker.kt b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/declaration/FirJsExternalArgumentCallChecker.kt new file mode 100644 index 00000000000..1d778baad4f --- /dev/null +++ b/compiler/fir/checkers/checkers.js/src/org/jetbrains/kotlin/fir/analysis/js/checkers/declaration/FirJsExternalArgumentCallChecker.kt @@ -0,0 +1,42 @@ +/* + * 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.js.checkers.declaration + +import org.jetbrains.kotlin.diagnostics.DiagnosticReporter +import org.jetbrains.kotlin.diagnostics.reportOn +import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext +import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirCallChecker +import org.jetbrains.kotlin.fir.analysis.diagnostics.js.FirJsErrors +import org.jetbrains.kotlin.fir.analysis.js.checkers.isEffectivelyExternal +import org.jetbrains.kotlin.fir.declarations.hasAnnotation +import org.jetbrains.kotlin.fir.expressions.FirCall +import org.jetbrains.kotlin.fir.expressions.resolvedArgumentMapping +import org.jetbrains.kotlin.fir.expressions.unwrapArgument +import org.jetbrains.kotlin.fir.types.ConeDynamicType +import org.jetbrains.kotlin.fir.types.coneTypeOrNull +import org.jetbrains.kotlin.fir.types.toRegularClassSymbol +import org.jetbrains.kotlin.name.JsStandardClassIds.Annotations.JsExternalArgument + +object FirJsExternalArgumentCallChecker : FirCallChecker() { + override fun check(expression: FirCall, context: CheckerContext, reporter: DiagnosticReporter) { + val arguments = expression.resolvedArgumentMapping ?: return + for ((argument, parameter) in arguments) { + if (parameter.hasAnnotation(JsExternalArgument, context.session)) { + val unwrappedArg = argument.unwrapArgument() + val type = unwrappedArg.typeRef.coneTypeOrNull ?: continue + val symbol = type.toRegularClassSymbol(context.session) + if (symbol?.isEffectivelyExternal(context.session) == false || type is ConeDynamicType) { + reporter.reportOn( + unwrappedArg.source, + FirJsErrors.JS_EXTERNAL_ARGUMENT, + type, + context + ) + } + } + } + } +} diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.kt b/compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.kt new file mode 100644 index 00000000000..91975052aa6 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.kt @@ -0,0 +1,143 @@ +// FIR_IDENTICAL + +@OptIn(ExperimentalStdlibApi::class) +fun anyExample(@JsExternalArgument x : Any) = x + +@OptIn(ExperimentalStdlibApi::class) +fun anyOrNullExample(@JsExternalArgument x : Any?) = x + +@OptIn(ExperimentalStdlibApi::class) +fun genericExample(@JsExternalArgument x : T) = x + +@OptIn(ExperimentalStdlibApi::class) +fun genericOrNullExample(@JsExternalArgument x : T?) = x + +@OptIn(ExperimentalStdlibApi::class) +fun dynamicExample(@JsExternalArgument x : dynamic) = x + +@OptIn(ExperimentalStdlibApi::class) +fun severalParams(@JsExternalArgument x : Any?, @JsExternalArgument y : Any?, @JsExternalArgument z : dynamic) = x ?: y ?: z + +external interface ExternalInterface + +fun boxExternalInterface(i: ExternalInterface) { + anyExample(i) + anyOrNullExample(i) + genericExample(i) + genericOrNullExample(i) + dynamicExample(i) +} + +fun boxExternalInterfaceOrNull(iOrNull: ExternalInterface?) { + anyOrNullExample(iOrNull) + genericExample(iOrNull) + genericOrNullExample(iOrNull) + dynamicExample(iOrNull) +} + +external class ExternalClass + +fun boxExternalClass(c: ExternalClass) { + anyExample(c) + anyOrNullExample(c) + genericExample(c) + genericOrNullExample(c) + dynamicExample(c) +} + +external object ExternalObject + +fun boxExternalObject() { + anyExample(ExternalObject) + anyOrNullExample(ExternalObject) + genericExample(ExternalObject) + genericOrNullExample(ExternalObject) + dynamicExample(ExternalObject) +} + +interface Interface + +fun boxInterface(i: Interface) { + anyExample(i) + anyOrNullExample(i) + genericExample(i) + genericOrNullExample(i) + dynamicExample(i) +} + +fun boxInterfaceOrNull(iOrNull: Interface?) { + anyOrNullExample(iOrNull) + genericExample(iOrNull) + genericOrNullExample(iOrNull) + dynamicExample(iOrNull) +} + +class Class + +fun boxInterface(c: Class) { + anyExample(c) + anyOrNullExample(c) + genericExample(c) + genericOrNullExample(c) + dynamicExample(c) +} + +object Object + +fun boxObject() { + anyExample(Object) + anyOrNullExample(Object) + genericExample(Object) + genericOrNullExample(Object) + dynamicExample(Object) +} + +fun boxDynamic(d: dynamic) { + anyExample(d) + anyOrNullExample(d) + genericExample(d) + genericOrNullExample(d) + dynamicExample(d) +} + +fun boxPrimitiveTypes() { + anyExample(1) + anyExample(1.2) + anyExample(true) + anyExample("hello") + anyExample({}) + anyExample({}()) + anyOrNullExample(null) +} + +fun boxArgExpression(i: Interface, iOrNull: Interface?) { + anyExample(iOrNull ?: i) +} + +fun boxNamedArgExpression(i: Interface, iOrNull: Interface?, d: dynamic) { + anyExample(x = iOrNull ?: i) + + severalParams( + x = i, + y = iOrNull ?: i, + z = iOrNull ?: d + ) + + severalParams( + z = iOrNull ?: d, + x = i, + y = iOrNull ?: i + ) + + severalParams( + i, + z = iOrNull ?: d, + y = iOrNull ?: i + ) + + severalParams( + x = i, + y = ExternalObject, + z = iOrNull ?: d + ) +} diff --git a/compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.txt b/compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.txt new file mode 100644 index 00000000000..8545b5159de --- /dev/null +++ b/compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.txt @@ -0,0 +1,61 @@ +package + +@kotlin.OptIn(markerClass = {kotlin.ExperimentalStdlibApi::class}) public fun anyExample(/*0*/ @kotlin.js.JsExternalArgument x: kotlin.Any): kotlin.Any +@kotlin.OptIn(markerClass = {kotlin.ExperimentalStdlibApi::class}) public fun anyOrNullExample(/*0*/ @kotlin.js.JsExternalArgument x: kotlin.Any?): kotlin.Any? +public fun boxArgExpression(/*0*/ i: Interface, /*1*/ iOrNull: Interface?): kotlin.Unit +public fun boxDynamic(/*0*/ d: dynamic): kotlin.Unit +public fun boxExternalClass(/*0*/ c: ExternalClass): kotlin.Unit +public fun boxExternalInterface(/*0*/ i: ExternalInterface): kotlin.Unit +public fun boxExternalInterfaceOrNull(/*0*/ iOrNull: ExternalInterface?): kotlin.Unit +public fun boxExternalObject(): kotlin.Unit +public fun boxInterface(/*0*/ c: Class): kotlin.Unit +public fun boxInterface(/*0*/ i: Interface): kotlin.Unit +public fun boxInterfaceOrNull(/*0*/ iOrNull: Interface?): kotlin.Unit +public fun boxNamedArgExpression(/*0*/ i: Interface, /*1*/ iOrNull: Interface?, /*2*/ d: dynamic): kotlin.Unit +public fun boxObject(): kotlin.Unit +public fun boxPrimitiveTypes(): kotlin.Unit +@kotlin.OptIn(markerClass = {kotlin.ExperimentalStdlibApi::class}) public fun dynamicExample(/*0*/ @kotlin.js.JsExternalArgument x: dynamic): dynamic +@kotlin.OptIn(markerClass = {kotlin.ExperimentalStdlibApi::class}) public fun genericExample(/*0*/ @kotlin.js.JsExternalArgument x: T): T +@kotlin.OptIn(markerClass = {kotlin.ExperimentalStdlibApi::class}) public fun genericOrNullExample(/*0*/ @kotlin.js.JsExternalArgument x: T?): T? +@kotlin.OptIn(markerClass = {kotlin.ExperimentalStdlibApi::class}) public fun severalParams(/*0*/ @kotlin.js.JsExternalArgument x: kotlin.Any?, /*1*/ @kotlin.js.JsExternalArgument y: kotlin.Any?, /*2*/ @kotlin.js.JsExternalArgument z: dynamic): dynamic + +public final class Class { + public constructor Class() + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public final external class ExternalClass { + public constructor ExternalClass() + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public external interface ExternalInterface { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public external object ExternalObject { + private constructor ExternalObject() + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public interface Interface { + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + +public object Object { + private constructor Object() + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} + diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticsTestWithJsStdLibGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticsTestWithJsStdLibGenerated.java index fd33c84bf9b..c31eca31af0 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticsTestWithJsStdLibGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticsTestWithJsStdLibGenerated.java @@ -48,6 +48,12 @@ public class DiagnosticsTestWithJsStdLibGenerated extends AbstractDiagnosticsTes runTest("compiler/testData/diagnostics/testsWithJsStdLib/implementingFunction.kt"); } + @Test + @TestMetadata("jsExternalArgument.kt") + public void testJsExternalArgument() throws Exception { + runTest("compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.kt"); + } + @Test @TestMetadata("jsExternalInheritorsOnly.kt") public void testJsExternalInheritorsOnly() throws Exception { diff --git a/core/compiler.common.js/src/org/jetbrains/kotlin/name/JsStandardClassIds.kt b/core/compiler.common.js/src/org/jetbrains/kotlin/name/JsStandardClassIds.kt index f1bb191cf3d..39885bb4e49 100644 --- a/core/compiler.common.js/src/org/jetbrains/kotlin/name/JsStandardClassIds.kt +++ b/core/compiler.common.js/src/org/jetbrains/kotlin/name/JsStandardClassIds.kt @@ -44,6 +44,9 @@ object JsStandardClassIds { @JvmField val JsExternalInheritorsOnly = "JsExternalInheritorsOnly".jsId() + @JvmField + val JsExternalArgument = "JsExternalArgument".jsId() + @JvmField val JsExportIgnore = JsExport.createNestedClassId(Name.identifier("Ignore")) diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt index fbce0136f15..a8aa0b8ea17 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/JsPlatformConfigurator.kt @@ -32,7 +32,8 @@ object JsPlatformConfigurator : PlatformConfiguratorBase( JsModuleCallChecker, JsDynamicCallChecker, JsDefinedExternallyCallChecker, - LateinitIntrinsicApplicabilityChecker(isWarningInPre19 = true) + LateinitIntrinsicApplicabilityChecker(isWarningInPre19 = true), + JsExternalArgumentCallChecker ), ) { override fun configureModuleComponents(container: StorageComponentContainer) { diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt index b8fe119fb79..bdd589f171a 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/DefaultErrorMessagesJs.kt @@ -114,6 +114,10 @@ private val DIAGNOSTIC_FACTORY_TO_RENDERER by lazy { Renderers.DECLARATION_NAME_WITH_KIND, Renderers.DECLARATION_NAME_WITH_KIND) + put(ErrorsJs.JS_EXTERNAL_ARGUMENT, + "Expected argument with external type, but type {0} is non-external", + Renderers.RENDER_TYPE) + this } } diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java index 088a3ceaa7a..57af1520796 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/ErrorsJs.java @@ -117,6 +117,9 @@ public interface ErrorsJs { DiagnosticFactory2 JS_EXTERNAL_INHERITORS_ONLY = DiagnosticFactory2.create( ERROR, DECLARATION_SIGNATURE_OR_DEFAULT); + DiagnosticFactory1 JS_EXTERNAL_ARGUMENT = DiagnosticFactory1.create( + ERROR, DECLARATION_SIGNATURE_OR_DEFAULT); + @SuppressWarnings("UnusedDeclaration") Object _initializer = new Object() { { diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsExternalArgumentCallChecker.kt b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsExternalArgumentCallChecker.kt new file mode 100644 index 00000000000..0fbc8717cd0 --- /dev/null +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/resolve/diagnostics/JsExternalArgumentCallChecker.kt @@ -0,0 +1,32 @@ +/* + * 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.js.resolve.diagnostics + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.js.translate.utils.AnnotationsUtils +import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker +import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.calls.util.getParameterForArgument +import org.jetbrains.kotlin.resolve.descriptorUtil.isEffectivelyExternal +import org.jetbrains.kotlin.types.typeUtil.makeNotNullable + +object JsExternalArgumentCallChecker : CallChecker { + override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { + for (argument in resolvedCall.call.valueArguments) { + val parameter = resolvedCall.getParameterForArgument(argument) ?: continue + if (AnnotationsUtils.isJsExternalArgument(parameter)) { + val argExpression = argument.getArgumentExpression() ?: continue + val argumentType = context.trace.bindingContext.getType(argExpression) ?: continue + val declaration = argumentType.makeNotNullable().constructor.declarationDescriptor as? ClassDescriptor ?: continue + if (!declaration.isEffectivelyExternal()) { + context.trace.report(ErrorsJs.JS_EXTERNAL_ARGUMENT.on(argExpression, argumentType)) + } + } + } + } +} diff --git a/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java b/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java index 08b9e866ebd..8f2c7360b68 100644 --- a/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java +++ b/js/js.frontend/src/org/jetbrains/kotlin/js/translate/utils/AnnotationsUtils.java @@ -47,6 +47,7 @@ public final class AnnotationsUtils { private static final FqName JS_NON_MODULE_ANNOTATION = Annotations.JsNonModule.asSingleFqName(); private static final FqName JS_QUALIFIER_ANNOTATION = Annotations.JsQualifier.asSingleFqName(); private static final FqName JS_EXTERNAL_INHERITORS_ONLY = Annotations.JsExternalInheritorsOnly.asSingleFqName(); + private static final FqName JS_EXTERNAL_ARGUMENT = Annotations.JsExternalArgument.asSingleFqName(); private AnnotationsUtils() { } @@ -263,6 +264,10 @@ public final class AnnotationsUtils { return declaration.getAnnotations().hasAnnotation(JS_EXTERNAL_INHERITORS_ONLY); } + public static boolean isJsExternalArgument(@NotNull ValueParameterDescriptor declaration) { + return declaration.getAnnotations().hasAnnotation(JS_EXTERNAL_ARGUMENT); + } + @Nullable private static String extractSingleStringArgument(@NotNull AnnotationDescriptor annotation) { if (annotation.getAllValueArguments().isEmpty()) return null; diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirPsiJsOldFrontendDiagnosticsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirPsiJsOldFrontendDiagnosticsTestGenerated.java index 5f22496a6a6..746e95c0469 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirPsiJsOldFrontendDiagnosticsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirPsiJsOldFrontendDiagnosticsTestGenerated.java @@ -49,6 +49,12 @@ public class FirPsiJsOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiJ runTest("compiler/testData/diagnostics/testsWithJsStdLib/implementingFunction.kt"); } + @Test + @TestMetadata("jsExternalArgument.kt") + public void testJsExternalArgument() throws Exception { + runTest("compiler/testData/diagnostics/testsWithJsStdLib/jsExternalArgument.kt"); + } + @Test @TestMetadata("jsExternalInheritorsOnly.kt") public void testJsExternalInheritorsOnly() throws Exception { diff --git a/libraries/stdlib/api/js-v1/kotlin.js.kt b/libraries/stdlib/api/js-v1/kotlin.js.kt index b6a44ac99a5..88b996fd708 100644 --- a/libraries/stdlib/api/js-v1/kotlin.js.kt +++ b/libraries/stdlib/api/js-v1/kotlin.js.kt @@ -250,6 +250,14 @@ public final annotation class JsExport : kotlin.Annotation { } } +@kotlin.ExperimentalStdlibApi +@kotlin.annotation.Retention(value = AnnotationRetention.BINARY) +@kotlin.annotation.Target(allowedTargets = {AnnotationTarget.VALUE_PARAMETER}) +@kotlin.SinceKotlin(version = "1.9") +public final annotation class JsExternalArgument : kotlin.Annotation { + public constructor JsExternalArgument() +} + @kotlin.ExperimentalStdlibApi @kotlin.annotation.Retention(value = AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets = {AnnotationTarget.CLASS}) diff --git a/libraries/stdlib/api/js/kotlin.js.kt b/libraries/stdlib/api/js/kotlin.js.kt index f241341995b..1a7c76a73ee 100644 --- a/libraries/stdlib/api/js/kotlin.js.kt +++ b/libraries/stdlib/api/js/kotlin.js.kt @@ -249,6 +249,14 @@ public final annotation class JsExport : kotlin.Annotation { } } +@kotlin.ExperimentalStdlibApi +@kotlin.annotation.Retention(value = AnnotationRetention.BINARY) +@kotlin.annotation.Target(allowedTargets = {AnnotationTarget.VALUE_PARAMETER}) +@kotlin.SinceKotlin(version = "1.9") +public final annotation class JsExternalArgument : kotlin.Annotation { + public constructor JsExternalArgument() +} + @kotlin.ExperimentalStdlibApi @kotlin.annotation.Retention(value = AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets = {AnnotationTarget.CLASS}) diff --git a/libraries/stdlib/js/src/kotlin/annotations.kt b/libraries/stdlib/js/src/kotlin/annotations.kt index 517975a8f8d..a613d6e1e93 100644 --- a/libraries/stdlib/js/src/kotlin/annotations.kt +++ b/libraries/stdlib/js/src/kotlin/annotations.kt @@ -252,3 +252,41 @@ public annotation class EagerInitialization @Target(CLASS) @SinceKotlin("1.9") public annotation class JsExternalInheritorsOnly + + +/** + * When placed on a function parameter, requires the type of the passed argument to be external. + * + * The compiler mangles identifiers of properties from non-external interfaces and classes, + * and doesn't mangle from external ones. Requiring a type of the passing argument being external is necessary + * to avoid non-obvious bugs when identifier has an unstable and unpredictable name in generated JS code. + * + * Example: + * + * ```kotlin + * + * @OptIn(ExperimentalStdlibApi::class) + * fun extractUuid(@JsExternalArgument x: dynamic) = x.uuid as String + * + * external interface User { + * val uuid: String + * } + * + * interface Owner { + * val uuid: String + * } + * + * fun checkUser(user: User, owner: Owner): Boolean { + * val userUuid = extractUuid(user) // OK + * val ownerUuid = extractUuid(owner) // Compilation error! Possible bug + * return userUuid == ownerUuid + * } + * ``` + * + * This annotation is experimental, meaning that the restrictions mentioned above are subject to change. + */ +@ExperimentalStdlibApi +@Retention(AnnotationRetention.BINARY) +@Target(VALUE_PARAMETER) +@SinceKotlin("1.9") +public annotation class JsExternalArgument