Retain optional expected annotations when compiling platform code
After this change, optional expected annotations will be compiled to
physical class files on JVM, and stored to metadata on other platforms,
to allow their usages from dependent platform modules. For example:
@OptionalExpectation
expect annotation class A
When compiling this code on JVM, A.class will be produced as if the
class A did neither have the 'expect' modifier, nor had it been
annotated with OptionalExpectation. Note that if there's no actual
annotation class for A, then usages (which can only be usages as
annotation entries) are simply skipped.
Class A will be public from Kotlin's point of view (since it should
be possible to use it in Kotlin sources), but _package-private_ in Java
to disallow its usages outside of the declaring module.
#KT-18882 Fixed
#KT-24617 Fixed
This commit is contained in:
@@ -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<KtDeclaration> =
|
||||
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? {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-2
@@ -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<MemberDescriptor>) {
|
||||
|
||||
@@ -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("<init>") 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<Annotation>) {
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -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("<init>") 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<Annotation>) {
|
||||
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"
|
||||
}
|
||||
+7
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
^
|
||||
|
||||
Generated
+5
@@ -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)
|
||||
|
||||
+5
@@ -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)
|
||||
|
||||
+5
@@ -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");
|
||||
|
||||
+5
@@ -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)
|
||||
|
||||
+11
-4
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
+5
@@ -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)
|
||||
|
||||
+5
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user