diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java index 875abb9c763..1433380ff10 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated.java @@ -878,6 +878,24 @@ public class FirOldFrontendMPPDiagnosticsWithLightTreeTestGenerated extends Abst runTest("compiler/testData/diagnostics/tests/multiplatform/java/flexibleTypes.kt"); } + @Test + @TestMetadata("implicitJavaActualizationAllowed.kt") + public void testImplicitJavaActualizationAllowed() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.kt"); + } + + @Test + @TestMetadata("implicitJavaActualizationDisallowed.kt") + public void testImplicitJavaActualizationDisallowed() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.kt"); + } + + @Test + @TestMetadata("implicitJavaActualization_multipleActuals.kt") + public void testImplicitJavaActualization_multipleActuals() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.kt"); + } + @Test @TestMetadata("inheritedJavaMembers.kt") public void testInheritedJavaMembers() throws Exception { diff --git a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java index 87f7d4b0f35..cad3b6a3e56 100644 --- a/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java +++ b/compiler/fir/analysis-tests/tests-gen/org/jetbrains/kotlin/test/runners/FirOldFrontendMPPDiagnosticsWithPsiTestGenerated.java @@ -878,6 +878,24 @@ public class FirOldFrontendMPPDiagnosticsWithPsiTestGenerated extends AbstractFi runTest("compiler/testData/diagnostics/tests/multiplatform/java/flexibleTypes.kt"); } + @Test + @TestMetadata("implicitJavaActualizationAllowed.kt") + public void testImplicitJavaActualizationAllowed() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.kt"); + } + + @Test + @TestMetadata("implicitJavaActualizationDisallowed.kt") + public void testImplicitJavaActualizationDisallowed() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.kt"); + } + + @Test + @TestMetadata("implicitJavaActualization_multipleActuals.kt") + public void testImplicitJavaActualization_multipleActuals() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.kt"); + } + @Test @TestMetadata("inheritedJavaMembers.kt") public void testInheritedJavaMembers() throws Exception { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java index 211adac8175..f3967f6a7a0 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java @@ -817,6 +817,8 @@ public interface Errors { DiagnosticFactory3, Collection>> NO_ACTUAL_FOR_EXPECT = DiagnosticFactory3.create(ERROR, INCOMPATIBLE_DECLARATION); + DiagnosticFactory2 IMPLICIT_JVM_ACTUALIZATION = + DiagnosticFactory2.create(ERROR, INCOMPATIBLE_DECLARATION); DiagnosticFactory2, Collection>> ACTUAL_WITHOUT_EXPECT = DiagnosticFactory2.create(ERROR, INCOMPATIBLE_DECLARATION); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java index 499f5000419..17da228427c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java @@ -374,6 +374,10 @@ public class DefaultErrorMessages { MAP.put(NO_ACTUAL_FOR_EXPECT, "Expected {0} has no actual declaration in module {1}{2}", DECLARATION_NAME_WITH_KIND, MODULE_WITH_PLATFORM, adaptGenerics1(PlatformIncompatibilityDiagnosticRenderer.TEXT)); + MAP.put(IMPLICIT_JVM_ACTUALIZATION, "Expected {0} is implicitly actualized by Java declaration in module {1}. " + + "Please migrate to explicit ''actual typealias''. See: https://youtrack.jetbrains.com/issue/KT-58545", + DECLARATION_NAME_WITH_KIND, + MODULE_WITH_PLATFORM); MAP.put(ACTUAL_WITHOUT_EXPECT, "{0} has no corresponding expected declaration{1}", CAPITALIZED_DECLARATION_NAME_WITH_KIND_AND_PLATFORM, adaptGenerics1(PlatformIncompatibilityDiagnosticRenderer.TEXT)); MAP.put(AMBIGUOUS_ACTUALS, "{0} has several compatible actual declarations in modules {1}", CAPITALIZED_DECLARATION_NAME_WITH_KIND_AND_PLATFORM, CommonRenderers.commaSeparated( diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExpectedActualDeclarationChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExpectedActualDeclarationChecker.kt index ebedb3aae1a..ff3a5c2df1c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExpectedActualDeclarationChecker.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExpectedActualDeclarationChecker.kt @@ -23,6 +23,8 @@ import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.diagnostics.Errors import org.jetbrains.kotlin.incremental.components.ExpectActualTracker +import org.jetbrains.kotlin.mpp.MppJavaImplicitActualizatorMarker +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier import org.jetbrains.kotlin.resolve.* @@ -40,6 +42,8 @@ import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.utils.addToStdlib.cast import java.io.File +private val implicitlyActualizedAnnotationFqn = FqName("kotlin.jvm.ImplicitlyActualizedByJvmDeclaration") + class ExpectedActualDeclarationChecker( val moduleStructureOracle: ModuleStructureOracle, val argumentExtractors: Iterable @@ -64,7 +68,7 @@ class ExpectedActualDeclarationChecker( if (descriptor.isExpect) { checkExpectedDeclarationHasProperActuals( declaration, descriptor, context.trace, - checkActualModifier, context.expectActualTracker + checkActualModifier, context ) checkOptInAnnotation(declaration, descriptor, descriptor, context.trace) } @@ -93,7 +97,7 @@ class ExpectedActualDeclarationChecker( descriptor: MemberDescriptor, trace: BindingTrace, checkActualModifier: Boolean, - expectActualTracker: ExpectActualTracker + context: DeclarationCheckerContext ) { val allActualizationPaths = moduleStructureOracle.findAllReversedDependsOnPaths(descriptor.module) val allLeafModules = allActualizationPaths.map { it.nodes.last() }.toSet() @@ -102,15 +106,41 @@ class ExpectedActualDeclarationChecker( val actuals = ExpectedActualResolver.findActualForExpected(descriptor, leafModule) ?: return@forEach checkExpectedDeclarationHasAtLeastOneActual( - reportOn, descriptor, actuals, trace, leafModule, checkActualModifier, expectActualTracker + reportOn, descriptor, actuals, trace, leafModule, checkActualModifier, context.expectActualTracker ) + checkImplicitJavaActualization(reportOn, descriptor, actuals, leafModule, context) + checkExpectedDeclarationHasAtMostOneActual( reportOn, descriptor, actuals, allActualizationPaths, trace ) } } + private fun checkImplicitJavaActualization( + expectPsi: KtNamedDeclaration, + expect: MemberDescriptor, + actuals: ActualsMap, + module: ModuleDescriptor, + context: DeclarationCheckerContext + ) { + val actualMembers = actuals + .filter { (compatibility, _) -> compatibility.isCompatibleOrWeakCompatible() } + .flatMap { (_, members) -> members } + .takeIf(List::isNotEmpty) + ?: return + + if (actualMembers.any { + it is MppJavaImplicitActualizatorMarker && + with(OptInUsageChecker) { + !expectPsi.isDeclarationAnnotatedWith(implicitlyActualizedAnnotationFqn, context.trace.bindingContext) + } + } + ) { + context.trace.report(Errors.IMPLICIT_JVM_ACTUALIZATION.on(expectPsi, expect, module)) + } + } + private fun checkExpectedDeclarationHasAtMostOneActual( reportOn: KtNamedDeclaration, expectDescriptor: MemberDescriptor, diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/OptInUsageChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/OptInUsageChecker.kt index 78792ce5bb5..b63503e717c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/OptInUsageChecker.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/OptInUsageChecker.kt @@ -343,7 +343,7 @@ class OptInUsageChecker(project: Project) : CallChecker { } } - private fun PsiElement.isDeclarationAnnotatedWith(annotationFqName: FqName, bindingContext: BindingContext): Boolean { + internal fun PsiElement.isDeclarationAnnotatedWith(annotationFqName: FqName, bindingContext: BindingContext): Boolean { if (this !is KtDeclaration) return false val descriptor = bindingContext.get(BindingContext.DECLARATION_TO_DESCRIPTOR, this) diff --git a/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.fir.kt b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.fir.kt new file mode 100644 index 00000000000..73ce26bdcb6 --- /dev/null +++ b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.fir.kt @@ -0,0 +1,20 @@ +// WITH_STDLIB + +// MODULE: m1-common +// FILE: common.kt + +import kotlin.jvm.ImplicitlyActualizedByJvmDeclaration + +@OptIn(ExperimentalMultiplatform::class) +@ImplicitlyActualizedByJvmDeclaration +expect class Foo() { + fun foo() +} + +// MODULE: m2-jvm()()(m1-common) +// FILE: Foo.java + +public class Foo { + public void foo() { + } +} diff --git a/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.kt b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.kt new file mode 100644 index 00000000000..8581f8bf983 --- /dev/null +++ b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.kt @@ -0,0 +1,20 @@ +// WITH_STDLIB + +// MODULE: m1-common +// FILE: common.kt + +import kotlin.jvm.ImplicitlyActualizedByJvmDeclaration + +@OptIn(ExperimentalMultiplatform::class) +@ImplicitlyActualizedByJvmDeclaration +expect class Foo() { + fun foo() +} + +// MODULE: m2-jvm()()(m1-common) +// FILE: Foo.java + +public class Foo { + public void foo() { + } +} diff --git a/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.fir.kt b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.fir.kt new file mode 100644 index 00000000000..e2f36560827 --- /dev/null +++ b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.fir.kt @@ -0,0 +1,14 @@ +// MODULE: m1-common +// FILE: common.kt + +expect class Foo() { + fun foo() +} + +// MODULE: m2-jvm()()(m1-common) +// FILE: Foo.java + +public class Foo { + public void foo() { + } +} diff --git a/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.kt b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.kt new file mode 100644 index 00000000000..878e314923a --- /dev/null +++ b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.kt @@ -0,0 +1,14 @@ +// MODULE: m1-common +// FILE: common.kt + +expect class Foo() { + fun foo() +} + +// MODULE: m2-jvm()()(m1-common) +// FILE: Foo.java + +public class Foo { + public void foo() { + } +} diff --git a/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.fir.kt b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.fir.kt new file mode 100644 index 00000000000..89e6240030d --- /dev/null +++ b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.fir.kt @@ -0,0 +1,20 @@ +// MODULE: m1-common +// FILE: common.kt + +expect class Foo(i: Int) { + fun foo() +} + +// MODULE: m2-jvm()()(m1-common) +// FILE: Foo.java + +public class Foo { + public Foo(int i) {} + public void foo() {} +} + +// FILE: jvm.kt + +class Foo(t: T) { + fun foo() {} +} diff --git a/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.kt b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.kt new file mode 100644 index 00000000000..0b506a57077 --- /dev/null +++ b/compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.kt @@ -0,0 +1,20 @@ +// MODULE: m1-common +// FILE: common.kt + +expect class Foo(i: Int) { + fun foo() +} + +// MODULE: m2-jvm()()(m1-common) +// FILE: Foo.java + +public class Foo { + public Foo(int i) {} + public void foo() {} +} + +// FILE: jvm.kt + +class Foo(t: T) { + fun foo() {} +} diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java index 090ed47075f..cc5dcad264a 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/DiagnosticTestGenerated.java @@ -23441,6 +23441,24 @@ public class DiagnosticTestGenerated extends AbstractDiagnosticTest { runTest("compiler/testData/diagnostics/tests/multiplatform/java/flexibleTypes.kt"); } + @Test + @TestMetadata("implicitJavaActualizationAllowed.kt") + public void testImplicitJavaActualizationAllowed() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationAllowed.kt"); + } + + @Test + @TestMetadata("implicitJavaActualizationDisallowed.kt") + public void testImplicitJavaActualizationDisallowed() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualizationDisallowed.kt"); + } + + @Test + @TestMetadata("implicitJavaActualization_multipleActuals.kt") + public void testImplicitJavaActualization_multipleActuals() throws Exception { + runTest("compiler/testData/diagnostics/tests/multiplatform/java/implicitJavaActualization_multipleActuals.kt"); + } + @Test @TestMetadata("inheritedJavaMembers.kt") public void testInheritedJavaMembers() throws Exception { diff --git a/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/descriptors/JavaClassDescriptor.java b/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/descriptors/JavaClassDescriptor.java index 01f1d282008..f48cb4d97de 100644 --- a/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/descriptors/JavaClassDescriptor.java +++ b/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/descriptors/JavaClassDescriptor.java @@ -17,7 +17,8 @@ package org.jetbrains.kotlin.load.java.descriptors; import org.jetbrains.kotlin.descriptors.ClassDescriptor; +import org.jetbrains.kotlin.mpp.MppJavaImplicitActualizatorMarker; -public interface JavaClassDescriptor extends ClassDescriptor { +public interface JavaClassDescriptor extends ClassDescriptor, MppJavaImplicitActualizatorMarker { boolean isRecord(); } diff --git a/core/descriptors/src/org/jetbrains/kotlin/mpp/MppJavaImplicitActualizatorMarker.kt b/core/descriptors/src/org/jetbrains/kotlin/mpp/MppJavaImplicitActualizatorMarker.kt new file mode 100644 index 00000000000..48d5867638e --- /dev/null +++ b/core/descriptors/src/org/jetbrains/kotlin/mpp/MppJavaImplicitActualizatorMarker.kt @@ -0,0 +1,14 @@ +/* + * 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.mpp + +/** + * The marker interface marks all Java descriptors that can implicitly actualize expect declarations. + * + * The marker interface is needed for being able to access information about Java specific descriptors from platform-agnostic + * `:compiler:frontend` module + */ +interface MppJavaImplicitActualizatorMarker diff --git a/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt b/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt index bd072e6c0a8..6536780ecd2 100644 --- a/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt +++ b/libraries/stdlib/common/src/kotlin/JvmAnnotationsH.kt @@ -51,6 +51,30 @@ public expect annotation class JvmName(val name: String) @OptionalExpectation public expect annotation class JvmMultifileClass() +/** + * This annotation marks Kotlin `expect` declarations that are implicitly actualized by Java. + * + * ## Safety Risks + * + * Implicit actualization bypasses safety features, potentially leading to errors or unexpected behavior. If you use this annotation, some + * of the expect-actual invariants are not checked. + * + * Use this annotation only as a last resort. The annotation might stop working in future Kotlin versions without prior notice. + * + * If you use this annotation, consider describing your use cases in [KT-58545](https://youtrack.jetbrains.com/issue/KT-58545) comments. + * + * ## Migration + * + * Rewrite the code using explicit `actual typealias`. Unfortunately, it requires you to move your expect declarations into another + * package. Refer to [KT-58545](https://youtrack.jetbrains.com/issue/KT-58545) for more detailed migration example. + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS) +@ExperimentalMultiplatform +@MustBeDocumented +@SinceKotlin("1.9") +@OptionalExpectation +public expect annotation class ImplicitlyActualizedByJvmDeclaration() /** * Instructs the Kotlin compiler not to generate getters/setters for this property and expose it as a field. diff --git a/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt b/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt index 591c200f841..93ce1a23a6e 100644 --- a/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt +++ b/libraries/stdlib/jvm/runtime/kotlin/jvm/annotations/JvmPlatformAnnotations.kt @@ -98,6 +98,29 @@ public actual annotation class JvmSynthetic @Retention(AnnotationRetention.SOURCE) public annotation class Throws(vararg val exceptionClasses: KClass) +/** + * This annotation marks Kotlin `expect` declarations that are implicitly actualized by Java. + * + * # Safety Risks + * + * Implicit actualization bypasses safety features, potentially leading to errors or unexpected behavior. If you use this annotation, some + * of the expect-actual invariants are not checked. + * + * Use this annotation only as a last resort. The annotation might stop working in future Kotlin versions without prior notice. + * + * If you use this annotation, consider describing your use cases in [KT-58545](https://youtrack.jetbrains.com/issue/KT-58545) comments. + * + * # Migration + * + * Rewrite the code using explicit `actual typealias`. Unfortunately, it requires you to move your expect declarations into another + * package. Refer to [KT-58545](https://youtrack.jetbrains.com/issue/KT-58545) for more detailed migration example. + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS) +@ExperimentalMultiplatform +@MustBeDocumented +@SinceKotlin("1.9") +public actual annotation class ImplicitlyActualizedByJvmDeclaration /** * Instructs the Kotlin compiler not to generate getters/setters for this property and expose it as a field. diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 1522647fb6b..6421e1703c0 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -3393,6 +3393,9 @@ public abstract interface annotation class kotlin/js/ExperimentalJsFileName : ja public abstract interface annotation class kotlin/js/ExperimentalJsReflectionCreateInstance : java/lang/annotation/Annotation { } +public abstract interface annotation class kotlin/jvm/ImplicitlyActualizedByJvmDeclaration : java/lang/annotation/Annotation { +} + public final class kotlin/jvm/JvmClassMappingKt { public static final fun getAnnotationClass (Ljava/lang/annotation/Annotation;)Lkotlin/reflect/KClass; public static final fun getJavaClass (Ljava/lang/Object;)Ljava/lang/Class;