diff --git a/compiler/backend-common/src/org/jetbrains/kotlin/backend/common/CodegenUtil.kt b/compiler/backend-common/src/org/jetbrains/kotlin/backend/common/CodegenUtil.kt index 7e75076b077..c14dd4d8b21 100644 --- a/compiler/backend-common/src/org/jetbrains/kotlin/backend/common/CodegenUtil.kt +++ b/compiler/backend-common/src/org/jetbrains/kotlin/backend/common/CodegenUtil.kt @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.checkers.ExpectedActualDeclarationChecker import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver import org.jetbrains.kotlin.types.KotlinType @@ -169,11 +170,22 @@ object CodegenUtil { /** * Returns declarations in the given [file] which should be generated by the back-end. This includes all declarations - * minus all expected declarations. + * minus all expected declarations (except annotation classes annotated with @OptionalExpectation). */ @JvmStatic fun getDeclarationsToGenerate(file: KtFile, bindingContext: BindingContext): List = - file.declarations.filterNot(KtDeclaration::hasExpectModifier) + file.declarations.filter(fun(declaration: KtDeclaration): Boolean { + if (!declaration.hasExpectModifier()) return true + + if (declaration is KtClass) { + val descriptor = bindingContext.get(BindingContext.CLASS, declaration) + if (descriptor != null && ExpectedActualDeclarationChecker.shouldGenerateExpectClass(descriptor)) { + return true + } + } + + return false + }) @JvmStatic fun findExpectedFunctionForActual(descriptor: FunctionDescriptor): FunctionDescriptor? { diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/AnnotationCodegen.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/AnnotationCodegen.java index 6620485cfd0..5b9614abc84 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/AnnotationCodegen.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/AnnotationCodegen.java @@ -28,6 +28,7 @@ import org.jetbrains.kotlin.load.java.JvmAnnotationNames; import org.jetbrains.kotlin.name.FqName; import org.jetbrains.kotlin.name.Name; import org.jetbrains.kotlin.resolve.AnnotationChecker; +import org.jetbrains.kotlin.resolve.checkers.ExpectedActualDeclarationChecker; import org.jetbrains.kotlin.resolve.constants.*; import org.jetbrains.kotlin.resolve.descriptorUtil.DescriptorUtilsKt; import org.jetbrains.kotlin.types.FlexibleType; @@ -300,7 +301,10 @@ public abstract class AnnotationCodegen { return null; } - if (classDescriptor.isExpect()) { + // We do not generate annotations whose classes are optional (annotated with `@OptionalExpectation`) because if an annotation entry + // is resolved to the expected declaration, this means that annotation has no actual class, and thus should not be generated. + // (Otherwise we would've resolved the entry to the actual annotation class.) + if (ExpectedActualDeclarationChecker.isOptionalAnnotationClass(classDescriptor)) { return null; } diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java index e65e7dce0c0..2901e7ff1ac 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/AsmUtil.java @@ -35,6 +35,7 @@ import org.jetbrains.kotlin.renderer.DescriptorRenderer; import org.jetbrains.kotlin.resolve.DescriptorUtils; import org.jetbrains.kotlin.resolve.InlineClassesUtilsKt; import org.jetbrains.kotlin.resolve.annotations.AnnotationUtilKt; +import org.jetbrains.kotlin.resolve.checkers.ExpectedActualDeclarationChecker; import org.jetbrains.kotlin.resolve.inline.InlineUtil; import org.jetbrains.kotlin.resolve.jvm.JvmClassName; import org.jetbrains.kotlin.resolve.jvm.JvmPrimitiveType; @@ -278,6 +279,9 @@ public class AsmUtil { if (descriptor instanceof SyntheticClassDescriptorForLambda) { return getVisibilityAccessFlagForAnonymous(descriptor); } + if (ExpectedActualDeclarationChecker.isOptionalAnnotationClass(descriptor)) { + return NO_FLAG_PACKAGE_PRIVATE; + } if (descriptor.getVisibility() == Visibilities.PUBLIC || descriptor.getVisibility() == Visibilities.PROTECTED || // TODO: should be package private, but for now Kotlin's reflection can't access members of such classes 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 47b999be5af..3496f6ea0bf 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExpectedActualDeclarationChecker.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/ExpectedActualDeclarationChecker.kt @@ -92,8 +92,25 @@ object ExpectedActualDeclarationChecker : DeclarationChecker { } } - internal fun isOptionalAnnotationClass(descriptor: DeclarationDescriptor): Boolean { - return descriptor.annotations.hasAnnotation(OPTIONAL_EXPECTATION_FQ_NAME) + @JvmStatic + fun isOptionalAnnotationClass(descriptor: DeclarationDescriptor): Boolean = + descriptor is ClassDescriptor && + descriptor.kind == ClassKind.ANNOTATION_CLASS && + descriptor.isExpect && + descriptor.annotations.hasAnnotation(OPTIONAL_EXPECTATION_FQ_NAME) + + // TODO: move to some other place which is accessible both from backend-common and js.serializer + @JvmStatic + fun shouldGenerateExpectClass(descriptor: ClassDescriptor): Boolean { + assert(descriptor.isExpect) { "Not an expected class: $descriptor" } + + if (ExpectedActualDeclarationChecker.isOptionalAnnotationClass(descriptor)) { + with(ExpectedActualResolver) { + return descriptor.findCompatibleActualForExpected(descriptor.module).isEmpty() + } + } + + return false } private fun ExpectActualTracker.reportExpectActual(expected: MemberDescriptor, actualMembers: Sequence) { diff --git a/compiler/testData/codegen/box/multiplatform/optionalExpectation.kt b/compiler/testData/codegen/box/multiplatform/optionalExpectation.kt index f707f4a305d..28f38c30821 100644 --- a/compiler/testData/codegen/box/multiplatform/optionalExpectation.kt +++ b/compiler/testData/codegen/box/multiplatform/optionalExpectation.kt @@ -1,49 +1,34 @@ // !LANGUAGE: +MultiPlatformProjects // !USE_EXPERIMENTAL: kotlin.ExperimentalMultiplatform -// TARGET_BACKEND: JVM +// IGNORE_BACKEND: JS_IR // WITH_RUNTIME -// FILE: common.kt +// MODULE: library +// FILE: expected.kt + +package a @OptionalExpectation -expect annotation class Anno(val s: String) +expect annotation class A(val x: Int) -// FILE: jvm.kt +@OptionalExpectation +expect annotation class B(val s: String) -import java.lang.reflect.AnnotatedElement +// FILE: actual.kt -@Anno("Foo") -class Foo @Anno("") constructor(@Anno("x") x: Int) { - @Anno("bar") - fun bar() {} +package a - @Anno("getX") - var x = x - @Anno("setX") - set +actual annotation class A(actual val x: Int) - @Anno("Nested") - interface Nested -} +// MODULE: main(library) +// FILE: main.kt -private fun check(element: AnnotatedElement) { - check(element.annotations) -} +package usage -private fun check(annotations: Array) { - val filtered = annotations.filterNot { it.annotationClass.java.name == "kotlin.Metadata" } - if (filtered.isNotEmpty()) { - throw AssertionError("Annotations should be empty: $filtered") - } -} +import a.A +import a.B +@A(42) +@B("OK") fun box(): String { - val foo = Foo::class.java - check(foo) - check(Foo.Nested::class.java) - check(foo.declaredMethods.single { it.name == "bar" }) - check(foo.declaredMethods.single { it.name == "getX" }) - check(foo.declaredMethods.single { it.name == "setX" }) - check(foo.constructors.single()) - check(foo.constructors.single().parameterAnnotations.single()) return "OK" } diff --git a/compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.kt b/compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.kt new file mode 100644 index 00000000000..f707f4a305d --- /dev/null +++ b/compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.kt @@ -0,0 +1,49 @@ +// !LANGUAGE: +MultiPlatformProjects +// !USE_EXPERIMENTAL: kotlin.ExperimentalMultiplatform +// TARGET_BACKEND: JVM +// WITH_RUNTIME +// FILE: common.kt + +@OptionalExpectation +expect annotation class Anno(val s: String) + +// FILE: jvm.kt + +import java.lang.reflect.AnnotatedElement + +@Anno("Foo") +class Foo @Anno("") constructor(@Anno("x") x: Int) { + @Anno("bar") + fun bar() {} + + @Anno("getX") + var x = x + @Anno("setX") + set + + @Anno("Nested") + interface Nested +} + +private fun check(element: AnnotatedElement) { + check(element.annotations) +} + +private fun check(annotations: Array) { + val filtered = annotations.filterNot { it.annotationClass.java.name == "kotlin.Metadata" } + if (filtered.isNotEmpty()) { + throw AssertionError("Annotations should be empty: $filtered") + } +} + +fun box(): String { + val foo = Foo::class.java + check(foo) + check(Foo.Nested::class.java) + check(foo.declaredMethods.single { it.name == "bar" }) + check(foo.declaredMethods.single { it.name == "getX" }) + check(foo.declaredMethods.single { it.name == "setX" }) + check(foo.constructors.single()) + check(foo.constructors.single().parameterAnnotations.single()) + return "OK" +} diff --git a/compiler/testData/codegen/bytecodeListing/multiplatform/optionalExpectation.txt b/compiler/testData/codegen/bytecodeListing/multiplatform/optionalExpectation.txt index f697431ca92..a176442165d 100644 --- a/compiler/testData/codegen/bytecodeListing/multiplatform/optionalExpectation.txt +++ b/compiler/testData/codegen/bytecodeListing/multiplatform/optionalExpectation.txt @@ -1,3 +1,10 @@ +@java.lang.annotation.Retention +@kotlin.Metadata +@kotlin.OptionalExpectation +annotation class Anno { + public abstract method s(): java.lang.String +} + @kotlin.Metadata public interface Foo$Nested { inner class Foo$Nested diff --git a/compiler/testData/compileKotlinAgainstKotlin/optionalAnnotation.kt b/compiler/testData/compileKotlinAgainstKotlin/optionalAnnotation.kt new file mode 100644 index 00000000000..208bf99ef02 --- /dev/null +++ b/compiler/testData/compileKotlinAgainstKotlin/optionalAnnotation.kt @@ -0,0 +1,35 @@ +// !LANGUAGE: +MultiPlatformProjects +// !USE_EXPERIMENTAL: kotlin.ExperimentalMultiplatform +// IGNORE_BACKEND: NATIVE +// FULL_JDK +// FILE: A.kt +package a + +@OptionalExpectation +expect annotation class A(val x: Int) + +@OptionalExpectation +expect annotation class B(val s: String) + +actual annotation class A(actual val x: Int) + +// FILE: B.kt +import a.A +import a.B +import java.lang.reflect.Modifier + +class Test { + @A(42) + @B("OK") + fun test() {} +} + +fun box(): String { + val annotations = Test::class.java.declaredMethods.single().annotations.toList() + if (annotations.toString() != "[@a.A(x=42)]") return "Fail 1: $annotations" + + // Can't use B::class.java because "Declaration annotated with '@OptionalExpectation' can only be used inside an annotation entry" + if (Modifier.isPublic(Class.forName("a.B").modifiers)) return "Fail 2: optional annotation class should not be public in the bytecode" + + return "OK" +} diff --git a/compiler/testData/multiplatform/optionalExpectationIncorrectUse/output.txt b/compiler/testData/multiplatform/optionalExpectationIncorrectUse/output.txt index ed1f94881ab..4ea3cd7dd9d 100644 --- a/compiler/testData/multiplatform/optionalExpectationIncorrectUse/output.txt +++ b/compiler/testData/multiplatform/optionalExpectationIncorrectUse/output.txt @@ -26,6 +26,9 @@ fun useInSignature(a: A) = a.toString() compiler/testData/multiplatform/optionalExpectationIncorrectUse/common.kt:9:1: error: this annotation is not applicable to target 'class' @OptionalExpectation ^ +compiler/testData/multiplatform/optionalExpectationIncorrectUse/common.kt:10:14: error: expected class 'NotAnAnnotationClass' has no actual declaration in module +expect class NotAnAnnotationClass + ^ compiler/testData/multiplatform/optionalExpectationIncorrectUse/common.kt:12:1: error: '@OptionalExpectation' can only be used on an expected annotation class @OptionalExpectation ^ diff --git a/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java b/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java index 248a49343ec..41422ebfb4d 100644 --- a/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java +++ b/compiler/tests-ir-jvm/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java @@ -13492,6 +13492,11 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes runTest("compiler/testData/codegen/box/multiplatform/optionalExpectation.kt"); } + @TestMetadata("optionalExpectationJvm.kt") + public void testOptionalExpectationJvm() throws Exception { + runTest("compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.kt"); + } + @TestMetadata("compiler/testData/codegen/box/multiplatform/defaultArguments") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index 53d8668a96a..70a9cec1a94 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -13492,6 +13492,11 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { runTest("compiler/testData/codegen/box/multiplatform/optionalExpectation.kt"); } + @TestMetadata("optionalExpectationJvm.kt") + public void testOptionalExpectationJvm() throws Exception { + runTest("compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.kt"); + } + @TestMetadata("compiler/testData/codegen/box/multiplatform/defaultArguments") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/CompileKotlinAgainstKotlinTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/CompileKotlinAgainstKotlinTestGenerated.java index f8d92f6e4ff..23a397f8523 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/CompileKotlinAgainstKotlinTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/CompileKotlinAgainstKotlinTestGenerated.java @@ -211,6 +211,11 @@ public class CompileKotlinAgainstKotlinTestGenerated extends AbstractCompileKotl runTest("compiler/testData/compileKotlinAgainstKotlin/nestedObject.kt"); } + @TestMetadata("optionalAnnotation.kt") + public void testOptionalAnnotation() throws Exception { + runTest("compiler/testData/compileKotlinAgainstKotlin/optionalAnnotation.kt"); + } + @TestMetadata("platformTypes.kt") public void testPlatformTypes() throws Exception { runTest("compiler/testData/compileKotlinAgainstKotlin/platformTypes.kt"); diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java index 521111fd46f..8ccb1383d31 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java @@ -13492,6 +13492,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes runTest("compiler/testData/codegen/box/multiplatform/optionalExpectation.kt"); } + @TestMetadata("optionalExpectationJvm.kt") + public void testOptionalExpectationJvm() throws Exception { + runTest("compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.kt"); + } + @TestMetadata("compiler/testData/codegen/box/multiplatform/defaultArguments") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/KotlinJavascriptSerializationUtil.kt b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/KotlinJavascriptSerializationUtil.kt index 6a3d48f04a9..05bf2192b0a 100644 --- a/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/KotlinJavascriptSerializationUtil.kt +++ b/js/js.serializer/src/org/jetbrains/kotlin/serialization/js/KotlinJavascriptSerializationUtil.kt @@ -26,9 +26,10 @@ import org.jetbrains.kotlin.metadata.js.JsProtoBuf import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.protobuf.CodedInputStream import org.jetbrains.kotlin.resolve.BindingContext -import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.checkers.ExpectedActualDeclarationChecker import org.jetbrains.kotlin.resolve.descriptorUtil.filterOutSourceAnnotations import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter import org.jetbrains.kotlin.resolve.scopes.MemberScope import org.jetbrains.kotlin.serialization.AnnotationSerializer @@ -190,9 +191,15 @@ object KotlinJavascriptSerializationUtil { ): ProtoBuf.PackageFragment { val builder = ProtoBuf.PackageFragment.newBuilder() - // TODO: ModuleDescriptor should be able to return the package only with the contents of that module, without dependencies - val skip: (DeclarationDescriptor) -> Boolean = { - DescriptorUtils.getContainingModule(it) != module || (it is MemberDescriptor && it.isExpect) + val skip = fun(descriptor: DeclarationDescriptor): Boolean { + // TODO: ModuleDescriptor should be able to return the package only with the contents of that module, without dependencies + if (descriptor.module != module) return true + + if (descriptor is MemberDescriptor && descriptor.isExpect) { + return !(descriptor is ClassDescriptor && ExpectedActualDeclarationChecker.shouldGenerateExpectClass(descriptor)) + } + + return false } val fileRegistry = KotlinFileRegistry() diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt index a1d40e07d79..9648d5a35a7 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicBoxTest.kt @@ -132,7 +132,7 @@ abstract class BasicBoxTest( } val mainModuleName = if (TEST_MODULE in modules) TEST_MODULE else DEFAULT_MODULE - val mainModule = modules[mainModuleName]!! + val mainModule = modules[mainModuleName] ?: error("No module with name \"$mainModuleName\"") val globalCommonFiles = JsTestUtils.getFilesInDirectoryByExtension( TEST_DATA_DIR_PATH + COMMON_FILES_DIR, JavaScript.EXTENSION) diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/IrJsCodegenBoxTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/IrJsCodegenBoxTestGenerated.java index d33b53ed56a..5d2f1e12409 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/IrJsCodegenBoxTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/IrJsCodegenBoxTestGenerated.java @@ -12842,6 +12842,11 @@ public class IrJsCodegenBoxTestGenerated extends AbstractIrJsCodegenBoxTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/multiplatform"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JS_IR, true); } + @TestMetadata("optionalExpectation.kt") + public void testOptionalExpectation() throws Exception { + runTest("compiler/testData/codegen/box/multiplatform/optionalExpectation.kt"); + } + @TestMetadata("compiler/testData/codegen/box/multiplatform/defaultArguments") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java index beceda14df1..a3d019e4fae 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java @@ -11714,6 +11714,11 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/multiplatform"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.JS, true); } + @TestMetadata("optionalExpectation.kt") + public void testOptionalExpectation() throws Exception { + runTest("compiler/testData/codegen/box/multiplatform/optionalExpectation.kt"); + } + @TestMetadata("compiler/testData/codegen/box/multiplatform/defaultArguments") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)