diff --git a/libraries/kotlinx-metadata/jvm/ChangeLog.md b/libraries/kotlinx-metadata/jvm/ChangeLog.md index 04bd482fc71..3cfd77db7e9 100644 --- a/libraries/kotlinx-metadata/jvm/ChangeLog.md +++ b/libraries/kotlinx-metadata/jvm/ChangeLog.md @@ -1,5 +1,15 @@ # kotlinx-metadata-jvm +## 0.6.0 + +This release features several significant API changes. To help with migration, we've prepared a special [guide](Migration.md). + +- Update to Kotlin 1.8 with metadata version 1.8, support reading/writing metadata of version 1.9 which will be used in Kotlin 1.9 +- Deprecate Visitors API +- Replace usages of `KotlinClassHeader` with direct usage of `kotlin.Metadata` annotation. Former reserved exclusively for use from Java clients. +- `impl` package renamed to `internal` +- Writers are deprecated. Special function family `KotlinClassMetadata.write` is introduced instead. + ## 0.5.0 - Update to Kotlin 1.7 with metadata version 1.7, support reading/writing metadata of version 1.8 which will be used in Kotlin 1.8. diff --git a/libraries/kotlinx-metadata/jvm/Migration.md b/libraries/kotlinx-metadata/jvm/Migration.md new file mode 100644 index 00000000000..f34d2e336a6 --- /dev/null +++ b/libraries/kotlinx-metadata/jvm/Migration.md @@ -0,0 +1,94 @@ +# Kotlinx-metadata migration guide + +Starting with 0.6.0 release, Kotlin team is focused on revisiting and improving kotlinx-metadata API, with an aim to provide a stable release +in the near future. As a result, the API was reshaped, with cuts here and there, so we've provided a migration guide to help you with updates. + +## Migrating from 0.5.0 to 0.6.0+ + +There are several significant changes between 0.5.0 and 0.6.0: + +### Visitors are deprecated + +There are two major APIs that allow introspecting Kotlin metadata: Visitor API (`KmClassVisitor`, `KmFunctionVisitor`, etc) and Nodes API +(`KmClass`, `KmFunction`, etc). After careful consideration, we've decided to deprecate Visitor API completely. +It is a more verbose and hard-to-use API that does not have any significant advantages. Everything that can be done using visitors can also be achieved with Nodes, usually with shorter and more idiomatic code. + +As these APIs represent different paradigms, migration can't be automated and requires some manual work. In short, replace every `visitXxx` with access to corresponding property, e.g. +`KmClassVisitor.visitFunction(flags, name)` with `KmClass.functions`: + +**Before:** +```kotlin +/** + * Visitor that gets names of all public functions which start with 'test' + */ +class TestFunctionFinder : KmClassVisitor() { + val result = mutableListOf() + + override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor? { + if (Flag.Common.IS_PUBLIC(flags) && name.startsWith("test")) result.add(name) + return null + } +} + +// Helper function to invoke visitor +fun KmClass.testFunctions(): List = TestFunctionFinder().also { this.accept(it) }.result.toList() +``` +**After:** +```kotlin +/** + * Extension function that gets names of all public functions which start with 'test' + */ +fun KmClass.testFunctions(): List = this.functions.mapNotNull { f -> + if (Flag.Common.IS_PUBLIC(f.flags) && f.name.startsWith("test")) f.name else null +} +``` + +As a result, complexity and line count are significantly reduced. +For a more sophisticated example, take a look at [this commit](https://github.com/JetBrains/kotlin/commit/c4d409608cf6b246a24f095bc7b30ff8d2efc373) that refactors an internal utility `kotlinp` which renders Kotlin metadata to text. + +Note that for now, Km nodes still implement visitors (e.g. `KmClass` implements `KmClassVisitor`). This relation will be removed in the future. + +### Metadata annotation class can be used directly + +Parameter type of `KotlinClassMetadata.read` function was changed to `kotlin.Metadata` from `kotlinx.metadata.jvm.KotlinClassHeader`. +It allows writing less boilerplate code as there's no need to copy parameters `d1`, `d2`, etc. + +If you have obtained `Metadata` instance reflectively, you +can use it right away. In case you are reading binary metadata, you can create `Metadata` instance using +a [helper function](https://github.com/JetBrains/kotlin/blob/3d679b76bce04a9bfbb7c0a2f769d5838d2c3bf9/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/jvmMetadataUtil.kt#L27) +or by [directly calling the annotation constructor](https://kotlinlang.org/docs/annotations.html#instantiation). + +> Note: as annotation instantiation is not available for Java clients, `KotlinClassHeader` is still present and reserved for construction from Java. +It implements `kotlin.Metadata` annotation interface, so they can be used interchangeably. + +Additionally, the property `KotlinClassMetadata.header: KotlinClassHeader` was changed into `KotlinClassMetadata.annotationData: Metadata`, so it is +also possible to use `Metadata` directly to write metadata back (see example in the section below). + +### Writers are streamlined + +To ease metadata manipulation, writing API was simplified. `KotlinClassMetadata.Class.Writer()` and other writers are deprecated; +Appropriate writer functions (e.g. `KotlinClassMetadata.writeClass`) should be used instead. + +To migrate, simply replace calls to writers with new functions: + +**Before:** +```kotlin +fun saveClass(kmClass: KmClass) { + val writer = KotlinClassMetadata.Class.Writer() + kmClass.accept(writer) + val classMetadata: KotlinClassMetadata.Class = writer.write() + val kotlinClassHeader: KotlinClassHeader = classMetadata.header + + // Write kotlinClassHeader.data1, data2, etc using ASM +} +``` + +**After:** +```kotlin +fun saveClass(kmClass: KmClass) { + val classMetadata: KotlinClassMetadata.Class = KotlinClassMetadata.writeClass(kmClass) + val metadata: Metadata = classMetadata.annotationData + + // Write Metadata.data1, data2, etc using ASM +} +``` \ No newline at end of file diff --git a/libraries/kotlinx-metadata/jvm/ReadMe.md b/libraries/kotlinx-metadata/jvm/ReadMe.md index d38a29bec0c..1f6d9bb11b7 100644 --- a/libraries/kotlinx-metadata/jvm/ReadMe.md +++ b/libraries/kotlinx-metadata/jvm/ReadMe.md @@ -35,17 +35,20 @@ dependencies { ## Overview -The entry point for reading the Kotlin metadata of a `.class` file is [`KotlinClassMetadata.read`](src/kotlinx/metadata/jvm/KotlinClassMetadata.kt). The data it takes is encapsulated in [`KotlinClassHeader`](src/kotlinx/metadata/jvm/KotlinClassHeader.kt) which is basically what is written in the [`kotlin.Metadata`](../../stdlib/jvm/runtime/kotlin/Metadata.kt) annotation on the class file generated by the Kotlin compiler. Construct `KotlinClassHeader` by reading the values from `kotlin.Metadata` reflectively or from some other resource, and then use `KotlinClassMetadata.read` to obtain the correct instance of the class metadata. +The entry point for reading the Kotlin metadata of a `.class` file is [`KotlinClassMetadata.read`](src/kotlinx/metadata/jvm/KotlinClassMetadata.kt). +The data it takes is the [`kotlin.Metadata`](../../stdlib/jvm/runtime/kotlin/Metadata.kt) annotation on the class file generated by the Kotlin compiler. +Obtain the `kotlin.Metadata` annotation reflectively or construct it from binary representation (e.g. by reading classfile with `org.objectweb.asm.ClassReader`), +and then use `KotlinClassMetadata.read` to obtain the correct instance of the class metadata. ```kotlin -val header = KotlinClassHeader( - ... - /* pass Metadata.k, Metadata.d1, Metadata.d2, etc as arguments ... */ +val metadataAnnotation = Metadata( + // pass arguments here ) -val metadata = KotlinClassMetadata.read(header) +val metadata = KotlinClassMetadata.read(metadataAnnotation) ``` -`KotlinClassMetadata` is a sealed class, with subclasses representing all the different kinds of classes generated by the Kotlin compiler. Unless you're sure that you're reading a class of a specific kind and can do a simple cast, a `when` is a good choice to handle all the possibilities: +`KotlinClassMetadata` is a sealed class, with subclasses representing all the different kinds of classes generated by the Kotlin compiler. +Unless you're sure that you're reading a class of a specific kind and can do a simple cast, a `when` is a good choice to handle all the possibilities: ```kotlin when (metadata) { @@ -58,7 +61,8 @@ when (metadata) { } ``` -Let's assume we've obtained an instance of `KotlinClassMetadata.Class`; other kinds of classes are handled similarly, except some of them have metadata in a slightly different form. The main way to make sense of the underlying metadata is to invoke `toKmClass`, which returns an instance of `KmClass` (`Km` is a shorthand for “Kotlin metadata”): +Let's assume we've obtained an instance of `KotlinClassMetadata.Class`; other kinds of classes are handled similarly, except some of them have metadata in a slightly different form. +The main way to make sense of the underlying metadata is to invoke `toKmClass()`, which returns an instance of `KmClass` (`Km` is a shorthand for “Kotlin metadata”): ```kotlin val klass = metadata.toKmClass() @@ -70,7 +74,10 @@ Please refer to [`MetadataSmokeTest.listInlineFunctions`](test/kotlinx/metadata/ ## Flags -Numerous objects have a property named `flags` of type `Flags`. These flags represent modifiers or other boolean attributes of a declaration or a type. To check if a certain flag is present, call one of the flags in [`Flag`](../src/kotlinx/metadata/Flag.kt) on the given integer value. The set of applicable flags is documented on each property or the corresponding `visit*` method. For example, for functions, this is common declaration flags (visibility, modality) plus `Flag.Function` flags: +Numerous objects have a property named `flags` of type `Flags`. These flags represent modifiers or other boolean attributes of a declaration or a type. +To check if a certain flag is present, call one of the flags in [`Flag`](../src/kotlinx/metadata/Flag.kt) on the given integer value. +The set of applicable flags is documented for each Node property which has type `Flags`. +For example, functions have common declaration flags (visibility, modality) plus `Flag.Function` flags: ```kotlin val function: KmFunction = ... @@ -84,7 +91,8 @@ if (Flag.Function.IS_SUSPEND(function.flags)) { ## Writing metadata -To create metadata of a Kotlin class file from scratch, construct an instance of `KmClass`/`KmPackage`/`KmLambda`, fill it with the data and call `accept` with the `Writer` class declared in the corresponding `KotlinClassMetadata` subclass. Finally, use `KotlinClassMetadata.header` to obtain the raw data and write it to the `kotlin.Metadata` annotation on a class file. +To create metadata of a Kotlin class file from scratch, construct an instance of `KmClass`/`KmPackage`/`KmLambda`, fill it with the data and call corresponding `KotlinClassMetadata.write` function. +Resulting `KotlinClassMetadata.annotationData` can be used to write `kotlin.Metadata` annotation on a class file. When using metadata writers from Kotlin source code, it's very convenient to use Kotlin scoping functions such as `apply` to reduce boilerplate: @@ -105,18 +113,19 @@ val klass = KmClass().apply { ... } -// Finally writing everything to arrays of bytes -val header = KotlinClassMetadata.Class.Writer().apply(klass::accept).write().header +val annotation = KotlinClassMetadata.writeClass(klass).annotationData -// Use header.kind, header.data1, header.data2, etc. to write values to kotlin.Metadata -... +// Write annotation directly or use annotation.kind, annotation.data1, annotation.data2, etc. ``` -Please refer to [`MetadataSmokeTest.produceKotlinClassFile`](test/kotlinx/metadata/test/MetadataSmokeTest.kt) for an example where metadata of a simple Kotlin class is created, and then the class file is produced with ASM and loaded by Kotlin reflection. +Please refer to [`MetadataSmokeTest.produceKotlinClassFile`](test/kotlinx/metadata/test/MetadataSmokeTest.kt) for an example where metadata of a simple Kotlin class is created, +and then the class file is produced with ASM and loaded by Kotlin reflection. ## Module metadata -Similarly to how `KotlinClassMetadata` is used to read/write metadata of Kotlin `.class` files, [`KotlinModuleMetadata`](src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt) is the entry point for reading/writing `.kotlin_module` files. Use `KotlinModuleMetadata.read` or `KotlinModuleMetadata.Writer` in very much the same fashion as with the class files. The only difference is that the source for the reader (and the result of the writer) is a simple byte array, not the structured data loaded from `kotlin.Metadata`: +Similarly to how `KotlinClassMetadata` is used to read/write metadata of Kotlin `.class` files, [`KotlinModuleMetadata`](src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt) +is the entry point for reading/writing `.kotlin_module` files. Use `KotlinModuleMetadata.read` or `KotlinModuleMetadata.write` in very much the same fashion as with the class files. +The only difference is that the source for the reader (and the result of the writer) is a simple byte array, not the structured data loaded from `kotlin.Metadata`: ```kotlin // Read the module metadata @@ -126,13 +135,14 @@ val module = metadata.toKmModule() ... // Write the module metadata -val bytes = KotlinModuleMetadata.Writer().apply(module::accept).write().bytes +val bytes = KotlinModuleMetadata.write(module).bytes File("META-INF/main.kotlin_module").writeBytes(bytes) ``` ## Laziness -Note that until you load the actual underlying data of a `KotlinClassMetadata` or `KotlinModuleMetadata` instance by invoking `accept` or one of the `toKm...` methods, the data is not completely parsed and verified. If you need to check if the data is not horribly corrupted before proceeding, ensure that either of those is called: +Note that until you load the actual underlying data of a `KotlinClassMetadata` or `KotlinModuleMetadata` instance by invoking one of the `toKm...` methods, +the data is not completely parsed and verified. If you need to check if the data is not horribly corrupted before proceeding, ensure that either of those is called: ```kotlin val metadata: KotlinClassMetadata.Class = ... diff --git a/libraries/kotlinx-metadata/jvm/Releasing.md b/libraries/kotlinx-metadata/jvm/Releasing.md new file mode 100644 index 00000000000..9d7ec8a8d6c --- /dev/null +++ b/libraries/kotlinx-metadata/jvm/Releasing.md @@ -0,0 +1,21 @@ +# kotlinx-metadata-jvm releasing guide + +Release is usually done from `master` branch, unless specific circumstances occur (e.g. incompatibility of protobuf between master and current Kotlin's release branch). + +To release version `N` of `kotlinx-metadata-jvm`: + +1. Update `ChangeLog.md` and other documentation (`ReadMe.md`, `Migration.md`) if necessary. +If release is done with different version of Kotlin than the previous one, add note about metadata version update. + +2. If changes are large: Send changes for the review as a separate branch and merge them into `master` after approve. + +3. Run the [TeamCity build](https://teamcity.jetbrains.com/buildConfiguration/KotlinTools_KotlinxMetadata_PublishJvm?mode=builds) with parameters: + * `deployVersion` is a version of Kotlin and kotlin-stdlib which `kotlinx-metadata-jvm` should depend on. + * `kotlinxMetadataDeployVersion` is a version `N` you want to release. + +4. In [Sonatype](https://oss.sonatype.org/#stagingRepositories) admin interface: + * Close the repository and wait for it to verify. + * Release it. + +5. Announce new version in [forum topic](https://discuss.kotlinlang.org/t/announcing-kotlinx-metadata-jvm-library-for-reading-modifying-metadata-of-kotlin-jvm-class-files/7980). +Additionally, you may announce it in the #compiler [public Slack channel](https://kotlinlang.slack.com) and in the internal channel #ext-google-compiler. \ No newline at end of file diff --git a/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt b/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt index f222b0cc396..70c97fb3f30 100644 --- a/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt +++ b/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt @@ -140,7 +140,12 @@ class KotlinModuleMetadata(@Suppress("CanBeParameter", "MemberVisibilityCanBePri } } - // TODO: docs + /** + * Writes the metadata of the Kotlin module file. + * + * @param metadataVersion metadata version to be written to the metadata (see [Metadata.metadataVersion]), + * [KotlinClassMetadata.COMPATIBLE_METADATA_VERSION] by default + */ fun write(kmModule: KmModule, metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata = Writer().also { kmModule.accept(it) }.write(metadataVersion) }