diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/classFileUtils.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/classFileUtils.kt index df5d53dd6f6..1887bffea73 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/classFileUtils.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/classFileUtils.kt @@ -25,6 +25,14 @@ fun ClassFileFactory.getClassFiles(): Iterable { return asList().filterClassFiles() } +fun ClassFileFactory.getKotlinModuleFile(): OutputFile? = asList().filter { it.relativePath.endsWith(".kotlin_module") }.run { + when (size) { + 0 -> null + 1 -> single() + else -> error("Module has non-unique .kotlin_metadata file") + } +} + fun List.filterClassFiles(): List { return filter { it.relativePath.endsWith(".class") } } diff --git a/compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.jvm_abi.txt b/compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.jvm_abi.txt index f7917bd1417..f9808835951 100644 --- a/compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.jvm_abi.txt +++ b/compiler/testData/codegen/box/multiplatform/optionalExpectationJvm.jvm_abi.txt @@ -9,3 +9,9 @@ MODULE main : kotlin/Int K2 value: kotlin/Int + MODULE METADATA + Property: module.metadata.optionalAnnotations + K1 + [Anno] + K2 + [] diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmAbiConsistencyHandler.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmAbiConsistencyHandler.kt index 482feb306d9..c06d322cf90 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmAbiConsistencyHandler.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/backend/handlers/JvmAbiConsistencyHandler.kt @@ -10,8 +10,10 @@ import org.jetbrains.kotlin.abicmp.defects.Location import org.jetbrains.kotlin.abicmp.reports.* import org.jetbrains.kotlin.abicmp.tag import org.jetbrains.kotlin.abicmp.tasks.ClassTask +import org.jetbrains.kotlin.abicmp.tasks.ModuleMetadataTask import org.jetbrains.kotlin.abicmp.tasks.checkerConfiguration import org.jetbrains.kotlin.codegen.getClassFiles +import org.jetbrains.kotlin.codegen.getKotlinModuleFile import org.jetbrains.kotlin.test.backend.ir.AbiCheckerSuppressor import org.jetbrains.kotlin.test.directives.CodegenTestDirectives import org.jetbrains.kotlin.test.model.* @@ -35,6 +37,7 @@ class JvmAbiConsistencyHandler(testServices: TestServices) : AnalysisHandler, val missingInK2: Set, val nonEmptyClassReports: MutableList, + val moduleMetadataReport: ModuleMetadataReport, ) private class TestReport { @@ -63,9 +66,14 @@ class JvmAbiConsistencyHandler(testServices: TestServices) : AnalysisHandler with(classReport) { appendClassReport() } } + + if (!report.moduleMetadataReport.isEmpty()) { + with(report.moduleMetadataReport) { appendModuleMetadataReport() } + } } } } @@ -164,27 +172,45 @@ class JvmAbiConsistencyHandler(testServices: TestServices) : AnalysisHandler() commonClasses.forEach { classInternalName -> val k1ClassNode = parseClassNode(classesFromK1[classInternalName]!!) val k2ClassNode = parseClassNode(classesFromK2[classInternalName]!!) val classReport = ClassReport(Location.Class("", classInternalName), classInternalName, "K1", "K2", DefectReport()) - ClassTask(checkerConfiguration { - // Indication of constants is different in K2 as expected - // We simply turn the checker off to avoid producing an enormous number of difference reports - disable("class.metadata.property.hasConstant") - }, k1ClassNode, k2ClassNode, classReport).run() + ClassTask(configuration, k1ClassNode, k2ClassNode, classReport).run() if (classReport.isNotEmpty()) { nonEmptyClassReports.add(classReport) } } - if (nonEmptyClassReports.isNotEmpty() || missingInK1.isNotEmpty() || missingInK2.isNotEmpty()) { - testReport.addModuleReport(module.name, ModuleReport(missingInK1, missingInK2, nonEmptyClassReports)) + val k1ModuleFile = info.fromK1.classFileFactory.getKotlinModuleFile() + val k2ModuleFile = info.fromK2.classFileFactory.getKotlinModuleFile() + + if (k1ModuleFile == null && k2ModuleFile != null) { + missingInK1.add(k2ModuleFile.relativePath) + } + + if (k1ModuleFile != null && k2ModuleFile == null) { + missingInK2.add(k1ModuleFile.relativePath) + } + + val moduleMetadataReport = ModuleMetadataReport("K1", "K2") + if (k1ModuleFile != null && k2ModuleFile != null) { + ModuleMetadataTask(configuration, k1ModuleFile.asByteArray(), k2ModuleFile.asByteArray(), moduleMetadataReport).run() + } + + if (nonEmptyClassReports.isNotEmpty() || missingInK1.isNotEmpty() || missingInK2.isNotEmpty() || !moduleMetadataReport.isEmpty()) { + testReport.addModuleReport(module.name, ModuleReport(missingInK1, missingInK2, nonEmptyClassReports, moduleMetadataReport)) } } diff --git a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/checkers/GenericMetadataChecker.kt b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/checkers/GenericMetadataChecker.kt index e715238d2da..4e560727e4d 100644 --- a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/checkers/GenericMetadataChecker.kt +++ b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/checkers/GenericMetadataChecker.kt @@ -5,19 +5,22 @@ package org.jetbrains.kotlin.abicmp.checkers +import org.jetbrains.kotlin.abicmp.reports.MetadataPropertyReport +import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry import kotlin.metadata.KmConstructor import kotlin.metadata.KmFunction import kotlin.metadata.KmProperty import kotlin.metadata.KmTypeAlias -import org.jetbrains.kotlin.abicmp.reports.MetadataPropertyReport -import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry +import kotlin.metadata.jvm.KmModule +import kotlin.metadata.jvm.KmPackageParts +import kotlin.metadata.jvm.UnstableMetadataApi interface GenericMetadataChecker : Checker { fun check(metadata1: T, metadata2: T, report: MetadataPropertyReport) } abstract class GenericMetadataPropertyChecker(name: String) : - PropertyChecker("class.metadata.$name"), + PropertyChecker(name), GenericMetadataChecker { override fun check(metadata1: T, metadata2: T, report: MetadataPropertyReport) { @@ -29,22 +32,34 @@ abstract class GenericMetadataPropertyChecker(name: String) : } } +@OptIn(UnstableMetadataApi::class) +fun moduleMetadataPropertyChecker(name: String, propertyGetter: (KmModule) -> String) = + object : GenericMetadataPropertyChecker("module.metadata.$name") { + override fun getProperty(node: KmModule) = propertyGetter(node) + } + +@OptIn(UnstableMetadataApi::class) +fun packagePartsPropertyChecker(name: String, propertyGetter: (KmPackageParts) -> String) = + object : GenericMetadataPropertyChecker("module.metadata.$name") { + override fun getProperty(node: KmPackageParts) = propertyGetter(node) + } + fun constructorMetadataPropertyChecker(name: String, propertyGetter: (KmConstructor) -> String) = - object : GenericMetadataPropertyChecker("constructor.$name") { + object : GenericMetadataPropertyChecker("class.metadata.constructor.$name") { override fun getProperty(node: KmConstructor) = propertyGetter(node) } fun functionMetadataPropertyChecker(name: String, propertyGetter: (KmFunction) -> String) = - object : GenericMetadataPropertyChecker("function.$name") { + object : GenericMetadataPropertyChecker("class.metadata.function.$name") { override fun getProperty(node: KmFunction) = propertyGetter(node) } fun typeAliasMetadataPropertyChecker(name: String, propertyGetter: (KmTypeAlias) -> String) = - object : GenericMetadataPropertyChecker("typeAlias.$name") { + object : GenericMetadataPropertyChecker("class.metadata.typeAlias.$name") { override fun getProperty(node: KmTypeAlias) = propertyGetter(node) } fun propertyMetadataPropertyChecker(name: String, propertyGetter: (KmProperty) -> String) = - object : GenericMetadataPropertyChecker("property.$name") { + object : GenericMetadataPropertyChecker("class.metadata.property.$name") { override fun getProperty(node: KmProperty) = propertyGetter(node) } diff --git a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/reports/MetadataPropertyReport.kt b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/reports/MetadataPropertyReport.kt index bdb297fa389..54f417901b6 100644 --- a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/reports/MetadataPropertyReport.kt +++ b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/reports/MetadataPropertyReport.kt @@ -8,9 +8,9 @@ package org.jetbrains.kotlin.abicmp.reports import org.jetbrains.kotlin.abicmp.tag import java.io.PrintWriter -class MetadataPropertyReport(val id: String, val header1: String, val header2: String) : ComparisonReport { +open class MetadataPropertyReport(val id: String, val header1: String, val header2: String) : ComparisonReport { - private val propertyDiffs = ArrayList() + protected val propertyDiffs = ArrayList() override fun isEmpty() = propertyDiffs.isEmpty() diff --git a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/reports/ModuleMetadataReport.kt b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/reports/ModuleMetadataReport.kt new file mode 100644 index 00000000000..5fa37f0c435 --- /dev/null +++ b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/reports/ModuleMetadataReport.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2024 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.abicmp.reports + +import java.io.PrintWriter + +class ModuleMetadataReport(header1: String, header2: String) : MetadataPropertyReport("MODULE METADATA", header1, header2) { + + private val packagePartsReports = ArrayList() + + override fun isEmpty() = propertyDiffs.isEmpty() && getFilteredPackagePartsReports().isEmpty() + + override fun writeAsHtml(output: PrintWriter) { + if (isEmpty()) return + super.writeAsHtml(output) + packagePartsReports.forEach { it.writeAsHtml(output) } + } + + fun TextTreeBuilderContext.appendModuleMetadataReport() { + node("MODULE METADATA") { + appendNamedDiffEntries(header1, header2, propertyDiffs, "Property") + + for (report in getFilteredPackagePartsReports()) { + with(report) { appendReport() } + } + } + } + + fun packagePartsReport(id: String) = MetadataPropertyReport(id, header1, header2).also { packagePartsReports.add(it) } + + private fun getFilteredPackagePartsReports() = packagePartsReports.filter { !it.isEmpty() }.sortedBy { it.id } + +} \ No newline at end of file diff --git a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/CheckerConfiguration.kt b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/CheckerConfiguration.kt index 06968bf1420..83aa96c4e23 100644 --- a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/CheckerConfiguration.kt +++ b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/CheckerConfiguration.kt @@ -153,6 +153,23 @@ private val allPackageMetadataCheckers = listOf( fileFacadeMetadataListChecker("localDelegatedProperties") { loadLocalDelegatedProperties(it).keys.toList() } ) +@OptIn(UnstableMetadataApi::class) +private val allPackagePartsMetadataCheckers = listOf( + packagePartsPropertyChecker("multiFileParts") { + it.multiFileClassParts.toList().map { filePart -> "(${filePart.first}, ${filePart.second})" }.toList().sorted() + .joinToString(prefix = "[", postfix = "]") + }, + packagePartsPropertyChecker("fileFacades") { it.fileFacades.toList().sorted().joinToString(prefix = "[", postfix = "]") } +) + +@OptIn(UnstableMetadataApi::class) +private val allModuleMetadataCheckers = listOf( + moduleMetadataPropertyChecker("optionalAnnotations") { + it.optionalAnnotationClasses.map { clazz -> clazz.name }.sorted().joinToString(prefix = "[", postfix = "]") + }, + moduleMetadataPropertyChecker("packageParts") { it.packageParts.keys.toList().sorted().joinToString(prefix = "[", postfix = "]") } +) + private val allMultifileClassFacadeMetadataCheckers = listOf( multiFileClassFacadeMetadataListChecker("partClassNames") { it.partClassNames } ) @@ -190,6 +207,7 @@ inline fun checkerConfiguration(b: CheckerConfigurationBuilder.() -> Unit): Chec return builder.build() } +@OptIn(UnstableMetadataApi::class) class CheckerConfiguration(private val enabledExclusively: Set, private val disabled: Set) { private fun List.filterOutDisabled() = filter { it.isEnabled() } @@ -206,6 +224,8 @@ class CheckerConfiguration(private val enabledExclusively: Set, private val enabledMultifileClassFacadeMetadataCheckers = allMultifileClassFacadeMetadataCheckers.filterOutDisabled() val enabledMultifileClassPartMetadataCheckers = allMultifileClassPartMetadataCheckers.filterOutDisabled() val enabledAllSyntheticClassMetadataCheckers = allSyntheticClassMetadataCheckers.filterOutDisabled() + val enabledModuleMetadataCheckers = allModuleMetadataCheckers.filterOutDisabled() + val enabledPackagePartsMetadataCheckers = allPackagePartsMetadataCheckers.filterOutDisabled() private fun Checker.isEnabled(): Boolean { if (enabledExclusively.isNotEmpty() && name !in enabledExclusively) return false diff --git a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/ModuleMetadataTask.kt b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/ModuleMetadataTask.kt new file mode 100644 index 00000000000..95bb9d81a82 --- /dev/null +++ b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/ModuleMetadataTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2010-2024 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.abicmp.tasks + +import org.jetbrains.kotlin.abicmp.reports.ModuleMetadataReport +import kotlin.metadata.jvm.KotlinModuleMetadata +import kotlin.metadata.jvm.UnstableMetadataApi + +@OptIn(UnstableMetadataApi::class) +class ModuleMetadataTask( + private val configuration: CheckerConfiguration, + metadata1Bytes: ByteArray, + metadata2Bytes: ByteArray, + private val report: ModuleMetadataReport, +) : Runnable { + + private val metadata1 = metadata1Bytes.toKmModule() + private val metadata2 = metadata2Bytes.toKmModule() + + + override fun run() { + for (checker in configuration.enabledModuleMetadataCheckers) { + checker.check(metadata1, metadata2, report) + } + + checkPackageParts() + } + + private fun checkPackageParts() { + val packageParts1 = metadata1.packageParts + val packageParts2 = metadata2.packageParts + + val commonIds = packageParts1.keys.intersect(packageParts2.keys).sorted() + for (id in commonIds) { + val packagePart1 = packageParts1[id]!! + val packagePart2 = packageParts2[id]!! + val packagePartsReport = report.packagePartsReport(id) + PackagePartsMetadataTask(configuration, packagePart1, packagePart2, packagePartsReport).run() + } + } +} + +@OptIn(UnstableMetadataApi::class) +private fun ByteArray.toKmModule() = KotlinModuleMetadata.read(this).kmModule diff --git a/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/PackagePartsMetadataTask.kt b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/PackagePartsMetadataTask.kt new file mode 100644 index 00000000000..422d4860142 --- /dev/null +++ b/libraries/tools/abi-comparator/src/main/kotlin/org/jetbrains/kotlin/abicmp/tasks/PackagePartsMetadataTask.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2024 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.abicmp.tasks + +import org.jetbrains.kotlin.abicmp.reports.MetadataPropertyReport +import kotlin.metadata.jvm.KmPackageParts +import kotlin.metadata.jvm.UnstableMetadataApi + +@OptIn(UnstableMetadataApi::class) +class PackagePartsMetadataTask( + private val configuration: CheckerConfiguration, + private val packagePart1: KmPackageParts, + private val packagePart2: KmPackageParts, + private val packagePartsReport: MetadataPropertyReport +): Runnable { + override fun run() { + for (checker in configuration.enabledPackagePartsMetadataCheckers) { + checker.check(packagePart1, packagePart2, packagePartsReport) + } + } +} \ No newline at end of file