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:
Alexander Udalov
2018-06-21 18:29:19 +02:00
parent 4e217b180a
commit 1951d38f40
17 changed files with 196 additions and 43 deletions
@@ -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
@@ -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"
}
@@ -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
^
@@ -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)
@@ -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)
@@ -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");
@@ -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)
@@ -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)
@@ -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)
@@ -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)