Add JVM ABI K1/K2 consistency tests

This commit is contained in:
vladislav.grechko
2023-12-26 10:18:19 +00:00
committed by Space Team
parent 5a1fb78fcd
commit 34bac48541
1104 changed files with 88413 additions and 66 deletions
@@ -0,0 +1,17 @@
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
implementation(commonDependency("org.jetbrains.intellij.deps:asm-all"))
implementation("org.apache.commons:commons-text:1.10.0")
implementation(project(":kotlin-stdlib-jdk8"))
implementation(project(":tools:kotlinp"))
implementation(project(":kotlinx-metadata-jvm"))
implementation(project(":kotlinx-metadata"))
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
@@ -0,0 +1,92 @@
/*
* Copyright 2010-2023 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
import org.jetbrains.org.objectweb.asm.Opcodes
class AndFlag(private val flag: Int, private val name: String) : (Int) -> String? {
override fun invoke(p1: Int): String? {
return if (p1 and flag != 0) name else null
}
}
class AndNotFlag(private val flag: Int, private val name: String) : (Int) -> String? {
override fun invoke(p1: Int): String? {
return if (p1 and flag == 0) name else null
}
}
fun Int.isPrivate() =
this and Opcodes.ACC_PRIVATE != 0
fun Int.isSynthetic() =
this and Opcodes.ACC_SYNTHETIC != 0
fun Int.isBridge() =
this and Opcodes.ACC_BRIDGE != 0
fun Int.classFlags() =
flagsList(CLASS_FLAGS)
fun Int.methodFlags() =
flagsList(METHOD_FLAGS)
fun Int.fieldFlags() =
flagsList(FIELD_FLAGS)
private fun Int.flagsList(flags: List<(Int) -> String?>) =
flags.mapNotNull { flag ->
flag(this)
}.joinToString(prefix = "[", postfix = "]") { it }
val CLASS_FLAGS = listOf(
AndNotFlag(Opcodes.ACC_PUBLIC + Opcodes.ACC_PROTECTED + Opcodes.ACC_PRIVATE, "package-private"),
AndFlag(Opcodes.ACC_PUBLIC, "public"),
AndFlag(Opcodes.ACC_PRIVATE, "private"),
AndFlag(Opcodes.ACC_PROTECTED, "protected"),
AndFlag(Opcodes.ACC_STATIC, "static"),
AndFlag(Opcodes.ACC_FINAL, "final"),
AndFlag(Opcodes.ACC_SUPER, "super"),
AndFlag(Opcodes.ACC_INTERFACE, "interface"),
AndFlag(Opcodes.ACC_ABSTRACT, "abstract"),
AndFlag(Opcodes.ACC_SYNTHETIC, "synthetic"),
AndFlag(Opcodes.ACC_ANNOTATION, "annotation"),
AndFlag(Opcodes.ACC_ENUM, "enum"),
AndFlag(Opcodes.ACC_MODULE, "module)"),
AndFlag(Opcodes.ACC_DEPRECATED, "deprecated")
)
val METHOD_FLAGS = listOf(
AndNotFlag(Opcodes.ACC_PUBLIC + Opcodes.ACC_PROTECTED + Opcodes.ACC_PRIVATE, "package-private"),
AndFlag(Opcodes.ACC_PUBLIC, "public"),
AndFlag(Opcodes.ACC_PRIVATE, "private"),
AndFlag(Opcodes.ACC_PROTECTED, "protected"),
AndFlag(Opcodes.ACC_STATIC, "static"),
AndFlag(Opcodes.ACC_FINAL, "final"),
AndFlag(Opcodes.ACC_SYNCHRONIZED, "synchronized"),
AndFlag(Opcodes.ACC_BRIDGE, "bridge"),
AndFlag(Opcodes.ACC_VARARGS, "vararg"),
AndFlag(Opcodes.ACC_NATIVE, "native"),
AndFlag(Opcodes.ACC_ABSTRACT, "abstract"),
AndFlag(Opcodes.ACC_STRICT, "strict"),
AndFlag(Opcodes.ACC_SYNTHETIC, "synthetic"),
AndFlag(Opcodes.ACC_DEPRECATED, "deprecated")
)
val FIELD_FLAGS = listOf(
AndNotFlag(Opcodes.ACC_PUBLIC + Opcodes.ACC_PROTECTED + Opcodes.ACC_PRIVATE, "package-private"),
AndFlag(Opcodes.ACC_PUBLIC, "public"),
AndFlag(Opcodes.ACC_PRIVATE, "private"),
AndFlag(Opcodes.ACC_PROTECTED, "protected"),
AndFlag(Opcodes.ACC_STATIC, "static"),
AndFlag(Opcodes.ACC_FINAL, "final"),
AndFlag(Opcodes.ACC_VOLATILE, "volatile"),
AndFlag(Opcodes.ACC_TRANSIENT, "transient"),
AndFlag(Opcodes.ACC_SYNTHETIC, "synthetic"),
AndFlag(Opcodes.ACC_ENUM, "enum"),
AndFlag(Opcodes.ACC_DEPRECATED, "deprecated")
)
@@ -0,0 +1,93 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.ListEntryDiff
import org.jetbrains.org.objectweb.asm.Type
val IGNORED_ANNOTATIONS = listOf("Lkotlin/Metadata;", "Lkotlin/coroutines/jvm/internal/DebugMetadata;")
fun compareAnnotations(
annotations1: List<AnnotationEntry>,
annotations2: List<AnnotationEntry>,
): List<ListEntryDiff>? {
val anns1Sorted = annotations1.preprocessAnnotations()
val anns2Sorted = annotations2.preprocessAnnotations()
val result = ArrayList<ListEntryDiff>()
var i1 = 0
var i2 = 0
val size1 = anns1Sorted.size
val size2 = anns2Sorted.size
while (i1 < size1 || i2 < size2) {
if (i1 < size1 && i2 < size2) {
val ann1 = anns1Sorted[i1]
val ann2 = anns2Sorted[i2]
// TODO proper comparison for annotation argument values?
if (ann1.fullString() == ann2.fullString()) {
++i1
++i2
} else {
when {
ann1.desc == ann2.desc -> {
++i1
++i2
result.add(ListEntryDiff(ann1.fullString(), ann2.fullString()))
}
ann1.desc < ann2.desc -> {
++i1
result.add(ListEntryDiff(ann1.fullString(), null))
}
else -> {
++i2
result.add(ListEntryDiff(null, ann2.fullString()))
}
}
}
} else {
if (i1 < size1) {
val ann1 = anns1Sorted[i1]
++i1
result.add(ListEntryDiff(ann1.fullString(), null))
} else {
val ann2 = anns2Sorted[i2]
++i2
result.add(ListEntryDiff(null, ann2.fullString()))
}
}
}
return if (result.isEmpty()) null else result
}
private fun List<AnnotationEntry>.preprocessAnnotations() =
filter { it.desc !in IGNORED_ANNOTATIONS }.sortedBy { it.fullString() }
private fun AnnotationEntry.shortString(): String =
if (values.isEmpty())
"@$desc"
else
"@$desc(...)"
private fun AnnotationEntry.fullString(): String =
if (values.isEmpty())
"@$desc"
else
"@$desc( ${values.joinToString { it.toValueString() }} )"
private fun Pair<String, Any?>.toValueString(): String =
"$first: ${second.toValueString()}"
private fun Any?.toValueString(): String =
when (this) {
null -> "NULL"
is Type -> "<$descriptor>"
is List<*> -> joinToString(separator = ", ", prefix = "#{ ", postfix = " }") { it.toValueString() }
is Array<*> -> toList().toValueString()
else -> toString()
}
@@ -0,0 +1,38 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.org.objectweb.asm.tree.AnnotationNode
data class AnnotationEntry(val desc: String, val values: List<Pair<String, Any?>>)
fun List<Any?>?.toAnnotations() =
this?.run {
mapNotNull { it.toAnnotation() }
.sortedBy { it.desc }
} ?: emptyList()
fun Any?.toAnnotation(): AnnotationEntry? {
val ann = this as? AnnotationNode ?: return null
val annValues = ann.values.orEmpty()
val iter = annValues.iterator()
val values = ArrayList<Pair<String, Any?>>()
while (iter.hasNext()) {
val key = iter.next() as String
val value = iter.next()!!.toAnnotationArgumentValue()
values.add(key to value)
}
return AnnotationEntry(ann.desc, values.sortedBy { it.first })
}
fun Any.toAnnotationArgumentValue(): Any =
when (this) {
is Array<*> -> map { it!!.toAnnotationArgumentValue() }
is List<*> -> map { it!!.toAnnotationArgumentValue() }
is AnnotationNode -> this.toAnnotation()!!
else -> this
}
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.defects.DefectType
import org.jetbrains.kotlin.abicmp.defects.VALUE1_A
import org.jetbrains.kotlin.abicmp.defects.VALUE2_A
import kotlin.reflect.KProperty1
interface Checker {
val name: String
}
abstract class PropertyChecker<T, E>(final override val name: String) : Checker {
protected val defectType = DefectType(this.name, "Attribute value mismatch", VALUE1_A, VALUE2_A)
protected open fun areEqual(value1: T, value2: T) =
value1 == value2
protected open fun valueToHtml(value: T, other: T): String = value?.toString() ?: "NULL"
protected abstract fun getProperty(node: E): T
}
abstract class AnnotationsChecker<N>(
final override val name: String,
private val annotationsProperty: KProperty1<N, List<Any?>?>,
) : Checker {
protected fun getAnnotations(node: N) =
annotationsProperty.get(node).orEmpty().toAnnotations()
val mismatchDefect = DefectType("${name}.mismatch", "Annotation value mismatch", VALUE1_A, VALUE2_A)
val missing1Defect = DefectType("${name}.missing1", "Missing annotation in #1", VALUE2_A)
val missing2Defect = DefectType("${name}.missing2", "Missing annotation in #2", VALUE1_A)
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.ClassReport
import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import kotlin.reflect.KProperty1
interface ClassChecker : Checker {
fun check(class1: ClassNode, class2: ClassNode, report: ClassReport)
}
abstract class ClassPropertyChecker<T>(name: String) :
PropertyChecker<T, ClassNode>("class.$name"),
ClassChecker {
override fun check(class1: ClassNode, class2: ClassNode, report: ClassReport) {
val value1 = getProperty(class1)
val value2 = getProperty(class2)
if (!areEqual(value1, value2)) {
report.addPropertyDiff(
defectType,
NamedDiffEntry(
name,
valueToHtml(value1, value2),
valueToHtml(value2, value1)
)
)
}
}
}
inline fun <T> classPropertyChecker(name: String, crossinline get: (ClassNode) -> T) =
object : ClassPropertyChecker<T>(name) {
override fun getProperty(node: ClassNode): T =
get(node)
}
fun <T> classPropertyChecker(classProperty: KProperty1<ClassNode, T>) =
classPropertyChecker(classProperty.name) { classProperty.get(it) }
inline fun <T> classPropertyChecker(classProperty: KProperty1<ClassNode, T>, crossinline html: (T) -> String) =
object : ClassPropertyChecker<T>(classProperty.name) {
override fun getProperty(node: ClassNode): T =
classProperty.get(node)
override fun valueToHtml(value: T, other: T): String =
html(value)
}
fun <T> classPropertyChecker(name: String, classProperty: KProperty1<ClassNode, T>) =
classPropertyChecker(name) { classProperty.get(it) }
class ClassAnnotationsChecker(annotationsProperty: KProperty1<ClassNode, List<Any?>?>) :
AnnotationsChecker<ClassNode>("class.${annotationsProperty.name}", annotationsProperty),
ClassChecker {
override fun check(class1: ClassNode, class2: ClassNode, report: ClassReport) {
val anns1 = getAnnotations(class1)
val anns2 = getAnnotations(class2)
report.addAnnotationDiffs(this, compareAnnotations(anns1, anns2) ?: return)
}
}
@@ -0,0 +1,13 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.ClassMetadataReport
interface ClassMetadataChecker : Checker {
fun check(metadata1: KotlinClassMetadata.Class, metadata2: KotlinClassMetadata.Class, report: ClassMetadataReport)
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.ClassMetadataReport
abstract class ClassMetadataListChecker(name: String) : ClassMetadataChecker {
override fun check(metadata1: KotlinClassMetadata.Class, metadata2: KotlinClassMetadata.Class, report: ClassMetadataReport) {
val list1 = getList(metadata1)
val list2 = getList(metadata2)
val diff = compareLists(list1.sorted(), list2.sorted()) ?: return
report.addMembersListDiffs(diff)
}
abstract fun getList(metadata: KotlinClassMetadata.Class): List<String>
override val name: String = "class.metadata.$name"
}
fun classMetadataListChecker(name: String, listGetter: (KotlinClassMetadata.Class) -> List<String>) =
object : ClassMetadataListChecker(name) {
override fun getList(metadata: KotlinClassMetadata.Class) = listGetter(metadata)
}
@@ -0,0 +1,27 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.ClassMetadataReport
import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry
abstract class ClassMetadataPropertyChecker(name: String) : PropertyChecker<String, KotlinClassMetadata.Class>("class.metadata.$name"),
ClassMetadataChecker {
override fun check(metadata1: KotlinClassMetadata.Class, metadata2: KotlinClassMetadata.Class, report: ClassMetadataReport) {
val value1 = getProperty(metadata1)
val value2 = getProperty(metadata2)
if (!areEqual(value1, value2)) {
report.addPropertyDiff(NamedDiffEntry(name, valueToHtml(value1, value2), valueToHtml(value2, value1)))
}
}
}
fun classMetadataPropertyChecker(name: String, propertyGetter: (KotlinClassMetadata.Class) -> String) =
object : ClassMetadataPropertyChecker(name) {
override fun getProperty(node: KotlinClassMetadata.Class) = propertyGetter(node)
}
@@ -0,0 +1,22 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.FieldReport
import org.jetbrains.org.objectweb.asm.tree.FieldNode
import kotlin.reflect.KProperty1
class FieldAnnotationsChecker(annotationsProperty: KProperty1<FieldNode, List<Any?>?>) :
AnnotationsChecker<FieldNode>("field.${annotationsProperty.name}", annotationsProperty),
FieldChecker {
override fun check(field1: FieldNode, field2: FieldNode, report: FieldReport) {
val anns1 = getAnnotations(field1)
val anns2 = getAnnotations(field2)
val annDiff = compareAnnotations(anns1, anns2) ?: return
report.addAnnotationDiffs(this, annDiff)
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.FieldReport
import org.jetbrains.org.objectweb.asm.tree.FieldNode
import kotlin.reflect.KProperty1
interface FieldChecker : Checker {
fun check(field1: FieldNode, field2: FieldNode, report: FieldReport)
}
inline fun <T> fieldPropertyChecker(name: String, crossinline get: (FieldNode) -> T) =
object : FieldPropertyChecker<T>(name) {
override fun getProperty(node: FieldNode): T =
get(node)
}
fun <T> fieldPropertyChecker(fieldProperty: KProperty1<FieldNode, T>) =
fieldPropertyChecker(fieldProperty.name) { fieldProperty.get(it) }
fun <T> fieldPropertyChecker(name: String, fieldProperty: KProperty1<FieldNode, T>) =
fieldPropertyChecker(name) { fieldProperty.get(it) }
inline fun <T> fieldPropertyChecker(fieldProperty: KProperty1<FieldNode, T>, crossinline html: (T) -> String) =
object : FieldPropertyChecker<T>(fieldProperty.name) {
override fun getProperty(node: FieldNode): T =
fieldProperty.get(node)
override fun valueToHtml(value: T, other: T): String =
html(value)
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.FieldReport
import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry
import org.jetbrains.org.objectweb.asm.tree.FieldNode
abstract class FieldPropertyChecker<T>(name: String) :
PropertyChecker<T, FieldNode>("field.$name"),
FieldChecker {
override fun check(field1: FieldNode, field2: FieldNode, report: FieldReport) {
val value1 = getProperty(field1)
val value2 = getProperty(field2)
if (!areEqual(value1, value2)) {
report.addPropertyDiff(
defectType,
NamedDiffEntry(
name,
valueToHtml(value1, value2),
valueToHtml(value2, value1)
)
)
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.defects.DefectType
import org.jetbrains.kotlin.abicmp.defects.FIELD_A
import org.jetbrains.kotlin.abicmp.fieldFlags
import org.jetbrains.kotlin.abicmp.isSynthetic
import org.jetbrains.kotlin.abicmp.listOfNotNull
import org.jetbrains.kotlin.abicmp.reports.ClassReport
import org.jetbrains.kotlin.abicmp.reports.ListEntryDiff
import org.jetbrains.kotlin.abicmp.tasks.fieldId
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.FieldNode
class FieldsListChecker : ClassChecker {
override val name = "class.fields"
val missing1Defect = DefectType("${name}.missing1", "Missing field in #1", FIELD_A)
val missing2Defect = DefectType("${name}.missing2", "Missing field in #2", FIELD_A)
override fun check(class1: ClassNode, class2: ClassNode, report: ClassReport) {
val fields1 = class1.loadFields()
val fields2 = class2.loadFields()
val relevantFieldIds = fields1.keys.union(fields2.keys)
.filter {
val field1 = fields1[it]
val field2 = fields2[it]
!(field1 != null && !field1.access.isSynthetic() ||
field2 != null && !field2.access.isSynthetic())
}.toSet()
val fieldIds1 = fields1.keys.intersect(relevantFieldIds).sorted()
val fieldIds2 = fields2.keys.intersect(relevantFieldIds).sorted()
val listDiff = compareLists(fieldIds1, fieldIds2) ?: return
report.addFieldListDiffs(
this,
listDiff.map {
ListEntryDiff(
it.value1?.toFieldWithFlags(fields1),
it.value2?.toFieldWithFlags(fields2)
)
}
)
}
private fun String.toFieldWithFlags(fields: Map<String, FieldNode>): String {
val field = fields[this] ?: return this
return "$this ${field.access.fieldFlags()}"
}
}
fun ClassNode.loadFields(): Map<String, FieldNode> =
fields.listOfNotNull<FieldNode>().filter {
(it.access and Opcodes.ACC_PUBLIC) != 0 ||
(it.access and Opcodes.ACC_PROTECTED) != 0
}.associateBy { it.fieldId() }
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmFunction
import kotlinx.metadata.KmProperty
import kotlinx.metadata.KmTypeAlias
import org.jetbrains.kotlin.abicmp.reports.MetadataPropertyReport
import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry
interface GenericMetadataChecker<T> : Checker {
fun check(metadata1: T, metadata2: T, report: MetadataPropertyReport)
}
abstract class GenericMetadataPropertyChecker<T>(name: String) :
PropertyChecker<String, T>("class.metadata.$name"),
GenericMetadataChecker<T> {
override fun check(metadata1: T, metadata2: T, report: MetadataPropertyReport) {
val value1 = getProperty(metadata1)
val value2 = getProperty(metadata2)
if (!areEqual(value1, value2)) {
report.addPropertyDiff(NamedDiffEntry(name, value1, value2))
}
}
}
fun constructorMetadataPropertyChecker(name: String, propertyGetter: (KmConstructor) -> String) =
object : GenericMetadataPropertyChecker<KmConstructor>("constructor.$name") {
override fun getProperty(node: KmConstructor) = propertyGetter(node)
}
fun functionMetadataPropertyChecker(name: String, propertyGetter: (KmFunction) -> String) =
object : GenericMetadataPropertyChecker<KmFunction>("function.$name") {
override fun getProperty(node: KmFunction) = propertyGetter(node)
}
fun typeAliasMetadataPropertyChecker(name: String, propertyGetter: (KmTypeAlias) -> String) =
object : GenericMetadataPropertyChecker<KmTypeAlias>("typeAlias.$name") {
override fun getProperty(node: KmTypeAlias) = propertyGetter(node)
}
fun propertyMetadataPropertyChecker(name: String, propertyGetter: (KmProperty) -> String) =
object : GenericMetadataPropertyChecker<KmProperty>("property.$name") {
override fun getProperty(node: KmProperty) = propertyGetter(node)
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.classFlags
import org.jetbrains.kotlin.abicmp.defects.DefectType
import org.jetbrains.kotlin.abicmp.defects.INNER_CLASS_A
import org.jetbrains.kotlin.abicmp.isSynthetic
import org.jetbrains.kotlin.abicmp.listOfNotNull
import org.jetbrains.kotlin.abicmp.reports.ClassReport
import org.jetbrains.kotlin.abicmp.reports.ListEntryDiff
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.InnerClassNode
class InnerClassesListChecker : ClassChecker {
override val name = "class.innerClasses"
val missing1Defect = DefectType("${name}.missing1", "Missing inner class in #1", INNER_CLASS_A)
val missing2Defect = DefectType("${name}.missing2", "Missing inner class in #2", INNER_CLASS_A)
override fun check(class1: ClassNode, class2: ClassNode, report: ClassReport) {
val innerClasses1 = class1.loadInnerClasses()
val innerClasses2 = class2.loadInnerClasses()
val relevantInnerClassNames =
innerClasses1.keys.union(innerClasses2.keys).filter {
val ic1 = innerClasses1[it]
val ic2 = innerClasses2[it]
ic1 != null && !ic1.access.isSynthetic() ||
ic2 != null && ic2.access.isSynthetic()
}
val innerClassNames1 = innerClasses1.keys.filter { it in relevantInnerClassNames }.sorted()
val innerClassNames2 = innerClasses2.keys.filter { it in relevantInnerClassNames }.sorted()
val listDiff = compareLists(innerClassNames1, innerClassNames2) ?: return
report.addInnerClassesDiffs(
this,
listDiff.map {
ListEntryDiff(
it.value1?.let { v1 -> innerClasses1[v1]?.toInnerClassLine() },
it.value2?.let { v2 -> innerClasses2[v2]?.toInnerClassLine() }
)
}
)
}
private fun ClassNode.loadInnerClasses(): Map<String, InnerClassNode> =
innerClasses.listOfNotNull<InnerClassNode>()
.filterNot {
it.innerName == null || it.innerName == "WhenMappings" || isSamAdapterName(it.name)
}
.associateBy { it.name }
private fun InnerClassNode.toInnerClassLine(): String =
"INNER_CLASS $name $outerName $innerName ${access.toString(2)} ${access.classFlags()}"
}
fun isSamAdapterName(name: String): Boolean =
"\$sam$" in name && name.endsWith("$0")
@@ -0,0 +1,22 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.MethodReport
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import kotlin.reflect.KProperty1
class MethodAnnotationsChecker(annotationsProperty: KProperty1<MethodNode, List<Any?>?>) :
AnnotationsChecker<MethodNode>("method.${annotationsProperty.name}", annotationsProperty),
MethodChecker {
override fun check(method1: MethodNode, method2: MethodNode, report: MethodReport) {
val anns1 = getAnnotations(method1)
val anns2 = getAnnotations(method2)
val annDiff = compareAnnotations(anns1, anns2) ?: return
report.addAnnotationDiffs(this, annDiff)
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.MethodReport
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import kotlin.reflect.KProperty1
const val ignoreMissingNullabilityAnnotationsOnInvisibleMethods = true
interface MethodChecker : Checker {
fun check(method1: MethodNode, method2: MethodNode, report: MethodReport)
}
inline fun <T> methodPropertyChecker(name: String, crossinline get: (MethodNode) -> T) =
object : MethodPropertyChecker<T>(name) {
override fun getProperty(node: MethodNode): T =
get(node)
}
fun <T> methodPropertyChecker(methodProperty: KProperty1<MethodNode, T>) =
methodPropertyChecker(methodProperty.name) { methodProperty.get(it) }
inline fun <T> methodPropertyChecker(methodProperty: KProperty1<MethodNode, T>, crossinline html: (T) -> String) =
object : MethodPropertyChecker<T>(methodProperty.name) {
override fun getProperty(node: MethodNode): T =
methodProperty.get(node)
override fun valueToHtml(value: T, other: T): String =
html(value)
}
fun <T> methodPropertyChecker(name: String, methodProperty: KProperty1<MethodNode, T>) =
methodPropertyChecker(name) { methodProperty.get(it) }
@@ -0,0 +1,35 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.defects.*
import org.jetbrains.kotlin.abicmp.reports.MethodReport
import org.jetbrains.org.objectweb.asm.tree.AnnotationNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import kotlin.math.max
import kotlin.reflect.KProperty1
class MethodParameterAnnotationsChecker(
private val parameterAnnotationsProperty: KProperty1<MethodNode, Array<List<AnnotationNode?>?>?>,
) : MethodChecker {
override val name = "method.parameters.${parameterAnnotationsProperty.name}"
val mismatchDefect = DefectType("${name}.mismatch", "Value parameter annotation mismatch", METHOD_A, VP_INDEX_A, VALUE1_A, VALUE2_A)
val missing1Defect = DefectType("${name}.missing1", "Missing value parameter annotation in #1", METHOD_A, VP_INDEX_A, VALUE2_A)
val missing2Defect = DefectType("${name}.missing2", "Missing value parameter annotation in #2", METHOD_A, VP_INDEX_A, VALUE1_A)
override fun check(method1: MethodNode, method2: MethodNode, report: MethodReport) {
val paramAnnsList1 = parameterAnnotationsProperty.get(method1)?.toList().orEmpty()
val paramAnnsList2 = parameterAnnotationsProperty.get(method2)?.toList().orEmpty()
for (i in 0 until max(paramAnnsList1.size, paramAnnsList2.size)) {
val anns1 = paramAnnsList1.getOrElse(i) { emptyList() }.toAnnotations()
val anns2 = paramAnnsList2.getOrElse(i) { emptyList() }.toAnnotations()
val annDiff = compareAnnotations(anns1, anns2) ?: continue
report.addValueParameterAnnotationDiffs(this, i, annDiff)
}
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.MethodReport
import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry
import org.jetbrains.org.objectweb.asm.tree.MethodNode
abstract class MethodPropertyChecker<T>(name: String) :
PropertyChecker<T, MethodNode>("method.$name"),
MethodChecker {
override fun check(method1: MethodNode, method2: MethodNode, report: MethodReport) {
val value1 = getProperty(method1)
val value2 = getProperty(method2)
if (!areEqual(value1, value2)) {
report.addPropertyDiff(
defectType,
NamedDiffEntry(
name,
valueToHtml(value1, value2),
valueToHtml(value2, value1)
)
)
}
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.defects.DefectType
import org.jetbrains.kotlin.abicmp.defects.METHOD_A
import org.jetbrains.kotlin.abicmp.isSynthetic
import org.jetbrains.kotlin.abicmp.listOfNotNull
import org.jetbrains.kotlin.abicmp.methodFlags
import org.jetbrains.kotlin.abicmp.reports.ClassReport
import org.jetbrains.kotlin.abicmp.reports.ListEntryDiff
import org.jetbrains.kotlin.abicmp.tasks.methodId
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
class MethodsListChecker : ClassChecker {
override val name = "class.methods"
val missing1Defect = DefectType("${name}.missing1", "Missing method in #1", METHOD_A)
val missing2Defect = DefectType("${name}.missing2", "Missing method in #2", METHOD_A)
override fun check(class1: ClassNode, class2: ClassNode, report: ClassReport) {
val methods1 = class1.loadMethods()
val methods2 = class2.loadMethods()
val relevantMethodIds = methods1.keys.union(methods2.keys)
.filter {
val method1 = methods1[it]
val method2 = methods2[it]
acceptNonSyntheticMethods(method1, method2)
}.toSet()
val methodIds1 = methods1.keys.intersect(relevantMethodIds).sorted()
val methodIds2 = methods2.keys.intersect(relevantMethodIds).sorted()
val listDiff = compareLists(methodIds1, methodIds2) ?: return
report.addMethodListDiffs(
this,
listDiff.map {
ListEntryDiff(
it.value1?.toMethodWithFlags(methods1),
it.value2?.toMethodWithFlags(methods2)
)
}
)
}
private fun acceptNonSyntheticMethods(method1: MethodNode?, method2: MethodNode?) =
method1 != null && !method1.access.isSynthetic() ||
method2 != null && !method2.access.isSynthetic()
private fun String.toMethodWithFlags(methods: Map<String, MethodNode>): String {
val method = methods[this] ?: return this
return "$this ${method.access.methodFlags()}"
}
}
fun ClassNode.loadMethods(): Map<String, MethodNode> =
methods.listOfNotNull<MethodNode>().filter {
(it.access and Opcodes.ACC_PUBLIC) != 0 ||
(it.access and Opcodes.ACC_PROTECTED) != 0
}.associateBy { it.methodId() }
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.MultiFileClassFacadeMetadataReport
interface MultiFileClassFacadeMetadataChecker : Checker {
fun check(
metadata1: KotlinClassMetadata.MultiFileClassFacade,
metadata2: KotlinClassMetadata.MultiFileClassFacade,
report: MultiFileClassFacadeMetadataReport,
)
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.MultiFileClassFacadeMetadataReport
abstract class MultiFileClassFacadeMetadataListChecker(name: String) : MultiFileClassFacadeMetadataChecker {
override fun check(
metadata1: KotlinClassMetadata.MultiFileClassFacade,
metadata2: KotlinClassMetadata.MultiFileClassFacade,
report: MultiFileClassFacadeMetadataReport,
) {
val list1 = getList(metadata1)
val list2 = getList(metadata2)
val diff = compareLists(list1.sorted(), list2.sorted()) ?: return
report.addMembersListDiffs(diff)
}
abstract fun getList(metadata: KotlinClassMetadata.MultiFileClassFacade): List<String>
override val name: String = "class.metadata.$name"
}
fun multiFileClassFacadeMetadataListChecker(name: String, listGetter: (KotlinClassMetadata.MultiFileClassFacade) -> List<String>) =
object : MultiFileClassFacadeMetadataListChecker(name) {
override fun getList(metadata: KotlinClassMetadata.MultiFileClassFacade) = listGetter(metadata)
}
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.MultiFileClassPartMetadataReport
interface MultiFileClassPartMetadataChecker : Checker {
fun check(
metadata1: KotlinClassMetadata.MultiFileClassPart,
metadata2: KotlinClassMetadata.MultiFileClassPart,
report: MultiFileClassPartMetadataReport,
)
}
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.MultiFileClassPartMetadataReport
import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry
abstract class MultiFileClassPartMetadataPropertyChecker(name: String) :
PropertyChecker<String, KotlinClassMetadata.MultiFileClassPart>("class.metadata.$name"), MultiFileClassPartMetadataChecker {
override fun check(
metadata1: KotlinClassMetadata.MultiFileClassPart,
metadata2: KotlinClassMetadata.MultiFileClassPart,
report: MultiFileClassPartMetadataReport,
) {
val value1 = getProperty(metadata1)
val value2 = getProperty(metadata2)
if (!areEqual(value1, value2)) {
report.addPropertyDiff(NamedDiffEntry(name, value1, value2))
}
}
}
fun multiFileClassPartMetadataPropertyChecker(name: String, propertyGetter: (KotlinClassMetadata.MultiFileClassPart) -> String) =
object : MultiFileClassPartMetadataPropertyChecker(name) {
override fun getProperty(node: KotlinClassMetadata.MultiFileClassPart) = propertyGetter(node)
}
@@ -0,0 +1,13 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.KmPackage
import org.jetbrains.kotlin.abicmp.reports.PackageMetadataReport
interface PackageMetadataChecker : Checker {
fun check(metadata1: KmPackage, metadata2: KmPackage, report: PackageMetadataReport)
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.KmPackage
import org.jetbrains.kotlin.abicmp.reports.PackageMetadataReport
abstract class PackageMetadataListChecker(name: String) : PackageMetadataChecker {
override fun check(
metadata1: KmPackage,
metadata2: KmPackage,
report: PackageMetadataReport,
) {
val list1 = getList(metadata1)
val list2 = getList(metadata2)
val diff = compareLists(list1.sorted(), list2.sorted()) ?: return
report.addMembersListDiffs(diff)
}
abstract fun getList(metadata: KmPackage): List<String>
override val name: String = "class.metadata.$name"
}
fun fileFacadeMetadataListChecker(name: String, listGetter: (KmPackage) -> List<String>) =
object : PackageMetadataListChecker(name) {
override fun getList(metadata: KmPackage) = listGetter(metadata)
}
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.SyntheticClassMetadataReport
interface SyntheticClassMetadataChecker : Checker {
fun check(
metadata1: KotlinClassMetadata.SyntheticClass,
metadata2: KotlinClassMetadata.SyntheticClass,
report: SyntheticClassMetadataReport,
)
}
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.NamedDiffEntry
import org.jetbrains.kotlin.abicmp.reports.SyntheticClassMetadataReport
abstract class SyntheticClassMetadataPropertyChecker(name: String) :
PropertyChecker<String, KotlinClassMetadata.SyntheticClass>("class.metadata.$name"), SyntheticClassMetadataChecker {
override fun check(
metadata1: KotlinClassMetadata.SyntheticClass,
metadata2: KotlinClassMetadata.SyntheticClass,
report: SyntheticClassMetadataReport,
) {
val value1 = getProperty(metadata1)
val value2 = getProperty(metadata2)
if (!areEqual(value1, value2)) {
report.addPropertyDiff(NamedDiffEntry(name, value1, value2))
}
}
}
fun syntheticClassMetadataPropertyChecker(name: String, propertyGetter: (KotlinClassMetadata.SyntheticClass) -> String) =
object : SyntheticClassMetadataPropertyChecker(name) {
override fun getProperty(node: KotlinClassMetadata.SyntheticClass) = propertyGetter(node)
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2023 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.checkers
import org.jetbrains.kotlin.abicmp.reports.ListEntryDiff
fun compareLists(list1: List<String>, list2: List<String>): List<ListEntryDiff>? {
val result = ArrayList<ListEntryDiff>()
var i1 = 0
var i2 = 0
while (i1 < list1.size || i2 < list2.size) {
val s1 = list1.getOrNull(i1)
val s2 = list2.getOrNull(i2)
if (s1 == s2) {
++i1
++i2
continue
}
when {
s1 == null && s2 == null ->
break // really should not happen
s1 == null -> {
result.add(ListEntryDiff(null, s2))
++i2
}
s2 == null -> {
result.add(ListEntryDiff(s1, null))
++i1
}
s1 < s2 -> {
result.add(ListEntryDiff(s1, null))
++i1
}
s1 > s2 -> {
result.add(ListEntryDiff(null, s2))
++i2
}
}
}
return if (result.isEmpty()) null else result
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2023 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.checkers
import kotlinx.metadata.*
import kotlinx.metadata.jvm.KotlinClassMetadata
import kotlinx.metadata.jvm.getterSignature
import kotlinx.metadata.jvm.localDelegatedProperties
import kotlinx.metadata.jvm.signature
import org.jetbrains.kotlin.abicmp.reports.MetadataPropertyReport
import org.jetbrains.kotlin.abicmp.tasks.GenericMetadataTask
import org.jetbrains.kotlin.kotlinp.*
fun loadProperties(container: KmDeclarationContainer) = container.properties.associateBy { it.getterSignature?.toString() ?: it.name }
fun loadConstructors(clazz: KotlinClassMetadata.Class) = clazz.kmClass.constructors.associateBy { it.signature.toString() }
fun loadFunctions(container: KmDeclarationContainer) = container.functions.associateBy { (it.signature ?: it.name).toString() }
fun loadTypeAliases(container: KmDeclarationContainer) = container.typeAliases.associateBy { it.name }
fun loadLocalDelegatedProperties(kmPackage: KmPackage) =
kmPackage.localDelegatedProperties.associateBy { it.getterSignature?.toString() ?: it.name }
fun loadLocalDelegatedProperties(clazz: KotlinClassMetadata.Class) =
clazz.kmClass.localDelegatedProperties.associateBy { it.getterSignature?.toString() ?: it.name }
fun List<KmVersionRequirement>.stringifyRelevantRequirements() =
// older versions of requirements are redundant for compiler with version 1.9 or newer
filter { it.version.major >= 2 || (it.version.major == 1 && it.version.minor >= 8) }
.map(::printVersionRequirement).sorted().joinToString(prefix = "[", postfix = "]")
fun List<KmType>.stringifyTypeListSorted() = map(::printType).sorted().joinToString(prefix = "[", postfix = "]")
fun List<KmTypeParameter>.stringifyTypeParameters() = joinToString(prefix = "<", postfix = ">") { typeParam ->
printTypeParameter(
typeParam,
KotlinpSettings(
isVerbose = true,
sortDeclarations = true
)
)
}
fun List<KmValueParameter>.stringifyValueParameters() = joinToString(prefix = "(", postfix = ")", transform = ::printValueParameter)
fun List<KmAnnotation>.stringifyAnnotations() = joinToString(prefix = "[", postfix = "]", transform = ::renderAnnotation)
inline fun <R, T> checkMetadataMembers(
metadata1: R,
metadata2: R,
checkers: List<GenericMetadataChecker<T>>,
reportBuilder: (String) -> MetadataPropertyReport,
membersGetter: (R) -> Map<String, T>,
) {
val membersMetadata1 = membersGetter(metadata1)
val membersMetadata2 = membersGetter(metadata2)
val commonIds = membersMetadata1.keys.intersect(membersMetadata2.keys).sorted()
for (id in commonIds) {
val member1 = membersMetadata1[id]!!
val member2 = membersMetadata2[id]!!
GenericMetadataTask(member1, member2, reportBuilder(id), checkers).run()
}
}
@@ -0,0 +1,8 @@
/*
* Copyright 2010-2023 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.defects
class Defect(val location: Location, val info: DefectInfo)
@@ -0,0 +1,59 @@
/*
* Copyright 2010-2023 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.defects
class DefectAttribute(val id: String, val htmlId: String)
class DefectType(
val id: String,
val messageText: String,
vararg val requiredAttributes: DefectAttribute,
) : Comparable<DefectType> {
override fun compareTo(other: DefectType): Int =
id.compareTo(other.id)
override fun hashCode(): Int = id.hashCode()
}
class DefectInfo(
val type: DefectType,
val attributes: Map<DefectAttribute, String>,
) : Comparable<DefectInfo> {
init {
for (requiredAttribute in type.requiredAttributes) {
if (requiredAttribute !in attributes) {
throw IllegalArgumentException(
"Missing required attribute ${requiredAttribute.id} for defect type ${type.id}"
)
}
}
}
operator fun get(key: DefectAttribute) = attributes[key]
override fun compareTo(other: DefectInfo): Int =
when (val typeCmp = type.compareTo(other.type)) {
0 -> compareAttributes(other)
else -> typeCmp
}
private fun compareAttributes(other: DefectInfo): Int {
for ((key, value) in attributes) {
val otherValue = other[key] ?: return 1
when (val valueCmp = value.compareTo(otherValue)) {
0 -> continue
else -> return valueCmp
}
}
return 0
}
override fun equals(other: Any?): Boolean =
other is DefectInfo && compareTo(other) == 0
override fun hashCode(): Int =
type.hashCode()
}
@@ -0,0 +1,60 @@
/*
* Copyright 2010-2023 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.defects
sealed class Location : Comparable<Location> {
abstract val jarFileName: String
open val className: String? get() = null
open val methodName: String? get() = null
open val fieldName: String? get() = null
enum class Kind {
JAR_FILE, CLASS, METHOD, FIELD
}
abstract val kind: Kind
override fun compareTo(other: Location): Int =
compareValuesBy(
this, other,
{ it.kind },
{ it.jarFileName },
{ it.className },
{ it.methodName },
{ it.fieldName }
)
data class JarFile(
override val jarFileName: String,
) : Location() {
override val kind get() = Kind.JAR_FILE
}
data class Class(
override val jarFileName: String,
override val className: String,
) : Location() {
override val kind get() = Kind.CLASS
fun method(methodName: String) = Method(jarFileName, className, methodName)
fun field(fieldName: String) = Field(jarFileName, className, fieldName)
}
data class Method(
override val jarFileName: String,
override val className: String,
override val methodName: String,
) : Location() {
override val kind get() = Kind.METHOD
}
data class Field(
override val jarFileName: String,
override val className: String,
override val fieldName: String,
) : Location() {
override val kind get() = Kind.FIELD
}
}
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2023 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.defects
val JAR_FILE1_A = DefectAttribute("JAR_FILE1", "jarFile1")
val JAR_FILE2_A = DefectAttribute("JAR_FILE2", "jarFile2")
val CLASS_A = DefectAttribute("CLASS", "class")
val METHOD_A = DefectAttribute("METHOD", "method")
val FIELD_A = DefectAttribute("FIELD", "field")
val VALUE1_A = DefectAttribute("VALUE1", "value1")
val VALUE2_A = DefectAttribute("VALUE2", "value2")
val VP_INDEX_A = DefectAttribute("VP_INDEX", "vpIndex")
val INNER_CLASS_A = DefectAttribute("INNER_CLASS", "innerClass")
val METADATA_A = DefectAttribute("METADATA", "metadata")
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2023 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
import org.apache.commons.text.StringEscapeUtils
import java.io.PrintWriter
const val NON_BREAKING_HYPHEN = "&#8209;"
fun String?.escapeHtml(): String {
if (this == null) return "NULL"
return StringEscapeUtils.escapeHtml4(this)
}
fun Any?.toHtmlString(): String {
if (this == null) return "NULL"
return StringEscapeUtils.escapeHtml4(toString()).replace("\n", "<br>")
}
fun PrintWriter.tag(tagName: String) {
print("<$tagName/>")
}
inline fun PrintWriter.tag(tagName: String, body: () -> Unit) {
print("<$tagName>")
body()
println("</$tagName>")
}
fun PrintWriter.tag(tagName: String, content: String) {
println("<$tagName>$content</$tagName>")
}
inline fun PrintWriter.table(body: () -> Unit) {
tag("table", body)
}
fun PrintWriter.tableHeader(vararg headers: String) {
tag("tr") {
for (header in headers) {
tag("th", header)
}
}
}
fun PrintWriter.tableData(vararg data: String) {
tag("tr") {
for (d in data) {
tag("td", d)
}
}
}
fun PrintWriter.tableDataWithClass(tdClass: String, vararg data: String) {
println("<tr>")
for (d in data) {
println("<td class=\"$tdClass\">$d</td>")
}
println("</tr>")
}
fun Any.tag(tagName: String) =
"<$tagName>$this</$tagName>"
@@ -0,0 +1,96 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.tag
import java.io.PrintWriter
class ClassMetadataReport(
private val classInternalName: String,
val header1: String,
val header2: String,
) : ComparisonReport {
private val membersDiffList = ArrayList<DiffEntry>()
private val propertyDiffs = ArrayList<NamedDiffEntry>()
private val constructorReports = ArrayList<MetadataPropertyReport>()
private val functionReports = ArrayList<MetadataPropertyReport>()
private val propertyReports = ArrayList<MetadataPropertyReport>()
private val typeAliasReports = ArrayList<MetadataPropertyReport>()
private val localDelegatedPropertyReport = ArrayList<MetadataPropertyReport>()
override fun isEmpty() =
membersDiffList.isEmpty()
&& propertyDiffs.isEmpty()
&& propertyReports.areAllEmpty()
&& constructorReports.areAllEmpty()
&& functionReports.areAllEmpty()
&& typeAliasReports.areAllEmpty()
&& localDelegatedPropertyReport.areAllEmpty()
override fun writeAsHtml(output: PrintWriter) {
if (isEmpty()) return
output.tag("h2", "CLASS METADATA $classInternalName")
output.listDiff(header1, header2, membersDiffList)
output.propertyDiffTable(header1, header2, propertyDiffs)
for (report in listOf(
constructorReports,
functionReports,
propertyReports,
typeAliasReports,
localDelegatedPropertyReport
).flatten()) {
report.writeAsHtml(output)
}
}
fun constructorReport(id: String) = MetadataPropertyReport("CONSTRUCTOR $id", header1, header2).also { constructorReports.add(it) }
fun functionReport(id: String) = MetadataPropertyReport("FUNCTION $id", header1, header2).also { functionReports.add(it) }
fun propertyReport(id: String) = MetadataPropertyReport("PROPERTY $id", header1, header2).also { propertyReports.add(it) }
fun typeAliasReport(id: String) = MetadataPropertyReport("TYPE ALIAS $id", header1, header2).also { typeAliasReports.add(it) }
fun localDelegatedPropertyReport(id: String) =
MetadataPropertyReport("LOCAL DELEGATED PROPERTY $id", header1, header2).also { localDelegatedPropertyReport.add(it) }
fun addMembersListDiffs(diffs: List<ListEntryDiff>) {
for (diff in diffs) {
membersDiffList.add(diff.toDiffEntry())
}
}
fun addPropertyDiff(diff: NamedDiffEntry) {
propertyDiffs.add(diff)
}
fun TextTreeBuilderContext.appendClassMetadataReport() {
if (isNotEmpty()) {
node("CLASS METADATA") {
appendDiffEntries(header1, header2, membersDiffList)
appendNamedDiffEntries(header1, header2, propertyDiffs, "Property")
for (report in listOf(
constructorReports,
functionReports,
propertyReports,
typeAliasReports,
localDelegatedPropertyReport
).flatten()) {
with(report) { appendReport() }
}
}
}
}
}
@@ -0,0 +1,222 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.checkers.ClassAnnotationsChecker
import org.jetbrains.kotlin.abicmp.checkers.FieldsListChecker
import org.jetbrains.kotlin.abicmp.checkers.InnerClassesListChecker
import org.jetbrains.kotlin.abicmp.checkers.MethodsListChecker
import org.jetbrains.kotlin.abicmp.defects.*
import org.jetbrains.kotlin.abicmp.escapeHtml
import org.jetbrains.kotlin.abicmp.tag
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
class ClassReport(
private val location: Location.Class,
val classInternalName: String,
val header1: String,
val header2: String,
private val defectReport: DefectReport,
) : ComparisonReport {
private val infoParagraphs = ArrayList<String>()
private val propertyDiffs = ArrayList<NamedDiffEntry>()
private val annotationDiffs = ArrayList<NamedDiffEntry>()
private val innerClassesDiffs = ArrayList<DiffEntry>()
private val methodListDiffs = ArrayList<DiffEntry>()
private val fieldListDiffs = ArrayList<DiffEntry>()
private val metadataDiffs = ArrayList<DiffEntry>()
private val methodReports = ArrayList<MethodReport>()
private val fieldReports = ArrayList<FieldReport>()
private var classMetadataReport: ClassMetadataReport? = null
private var fileFacadeMetadataReport: PackageMetadataReport? = null
private var multiFileClassFacadeMetadataReport: MultiFileClassFacadeMetadataReport? = null
private var multiFileClassPartMetadataReport: MultiFileClassPartMetadataReport? = null
private var syntheticClassMetadataReport: SyntheticClassMetadataReport? = null
private val ComparisonReport?.isNullOrEmpty get() = this == null || this.isEmpty()
override fun isEmpty(): Boolean =
metadataDiffs.isEmpty() &&
propertyDiffs.isEmpty() &&
annotationDiffs.isEmpty() &&
innerClassesDiffs.isEmpty() &&
methodListDiffs.isEmpty() &&
getFilteredMethodReports().isEmpty() &&
fieldListDiffs.isEmpty() &&
getFilteredFieldReports().isEmpty()
&& classMetadataReport.isNullOrEmpty
&& fileFacadeMetadataReport.isNullOrEmpty
&& multiFileClassFacadeMetadataReport.isNullOrEmpty
&& multiFileClassPartMetadataReport.isNullOrEmpty
&& syntheticClassMetadataReport.isNullOrEmpty
fun addInfo(info: String) {
infoParagraphs.add(info)
}
inline fun info(fm: PrintWriter.() -> Unit) {
val bytes = ByteArrayOutputStream()
val ps = PrintWriter(bytes)
ps.fm()
ps.close()
addInfo(String(bytes.toByteArray()))
}
private fun DefectType.report(vararg attributes: Pair<DefectAttribute, String>) {
defectReport.report(this, location, *attributes)
}
fun addPropertyDiff(defectType: DefectType, diff: NamedDiffEntry) {
propertyDiffs.add(diff)
defectType.report(VALUE1_A to diff.value1, VALUE2_A to diff.value2)
}
fun addAnnotationDiffs(checker: ClassAnnotationsChecker, diffs: List<ListEntryDiff>) {
for (diff in diffs) {
annotationDiffs.add(NamedDiffEntry(checker.name, diff.value1 ?: "---", diff.value2 ?: "---"))
when {
diff.value1 != null && diff.value2 != null ->
checker.mismatchDefect.report(VALUE1_A to diff.value1, VALUE2_A to diff.value2)
diff.value1 == null && diff.value2 != null ->
checker.missing1Defect.report(VALUE2_A to diff.value2)
diff.value1 != null && diff.value2 == null ->
checker.missing2Defect.report(VALUE1_A to diff.value1)
}
}
}
fun addInnerClassesDiffs(checker: InnerClassesListChecker, diffs: List<ListEntryDiff>) {
for (diff in diffs) {
innerClassesDiffs.add(diff.toDiffEntry())
reportMissing(diff, checker.missing1Defect, checker.missing2Defect, INNER_CLASS_A)
}
}
fun addMethodListDiffs(checker: MethodsListChecker, diffs: List<ListEntryDiff>) {
for (diff in diffs) {
methodListDiffs.add(diff.toDiffEntry())
reportMissing(diff, checker.missing1Defect, checker.missing2Defect, METHOD_A)
}
}
fun addFieldListDiffs(checker: FieldsListChecker, diffs: List<ListEntryDiff>) {
for (diff in diffs) {
fieldListDiffs.add(diff.toDiffEntry())
reportMissing(diff, checker.missing1Defect, checker.missing2Defect, FIELD_A)
}
}
fun addMetadataDiff(diff: ListEntryDiff) {
metadataDiffs.add(diff.toDiffEntry())
val missing1Defect = DefectType("class.metadata.missing1", "Missing metadata in #1", METADATA_A)
val missing2Defect = DefectType("class.metadata.missing2", "Missing metadata in #2", METADATA_A)
reportMissing(diff, missing1Defect, missing2Defect, METADATA_A)
}
private fun reportMissing(diff: ListEntryDiff, missing1: DefectType, missing2: DefectType, attr: DefectAttribute) {
when {
diff.value1 == null && diff.value2 != null ->
missing1.report(attr to diff.value2)
diff.value1 != null && diff.value2 == null ->
missing2.report(attr to diff.value1)
}
}
fun classMetadataReport() = ClassMetadataReport(classInternalName, header1, header2).also { this.classMetadataReport = it }
fun fileFacadeMetadataReport() = PackageMetadataReport(classInternalName, header1, header2).also { this.fileFacadeMetadataReport = it }
fun multiFileClassFacadeMetadataReport() =
MultiFileClassFacadeMetadataReport(classInternalName, header1, header2).also { this.multiFileClassFacadeMetadataReport = it }
fun multiFileClassPartMetadataReport() =
MultiFileClassPartMetadataReport(classInternalName, header1, header2).also { this.multiFileClassPartMetadataReport = it }
fun syntheticMetadataReport() =
SyntheticClassMetadataReport(classInternalName, header1, header2).also { this.syntheticClassMetadataReport = it }
fun methodReport(methodId: String): MethodReport =
MethodReport(location.method(methodId), methodId, header1, header2, defectReport)
.also { methodReports.add(it) }
fun fieldReport(fieldId: String): FieldReport =
FieldReport(location.field(fieldId), fieldId, header1, header2, defectReport)
.also { fieldReports.add(it) }
private fun getFilteredMethodReports() =
methodReports.filter { !it.isEmpty() }.sortedBy { it.methodId }
private fun getFilteredFieldReports() =
fieldReports.filter { !it.isEmpty() }.sortedBy { it.fieldId }
fun TextTreeBuilderContext.appendClassReport() {
node("CLASS $classInternalName") {
classMetadataReport?.run { appendClassMetadataReport() }
fileFacadeMetadataReport?.run { appendPackageMetadataReport() }
multiFileClassFacadeMetadataReport?.run { appendMultiFileClassFacadeReport() }
multiFileClassPartMetadataReport?.run { appendMultiFileClassPartReport() }
syntheticClassMetadataReport?.run { appendSyntheticClassMetadataReport() }
appendNamedDiffEntries(header1, header2, propertyDiffs, "Property")
appendNamedDiffEntries(header1, header2, annotationDiffs, "Annotation")
appendDiffEntries(header1, header2, innerClassesDiffs)
appendDiffEntries(header1, header2, methodListDiffs)
appendDiffEntries(header1, header2, metadataDiffs)
for (mr in getFilteredMethodReports()) {
with(mr) { appendMethodReport() }
}
appendDiffEntries(header1, header2, fieldListDiffs)
for (fr in getFilteredFieldReports()) {
with(fr) { appendFieldReport() }
}
}
}
override fun writeAsHtml(output: PrintWriter) {
output.tag("h1", "CLASS " + classInternalName.escapeHtml())
for (info in infoParagraphs) {
output.tag("p", info)
}
output.propertyDiffTable(header1, header2, propertyDiffs)
output.annotationDiffTable(header1, header2, annotationDiffs)
output.listDiff(header1, header2, innerClassesDiffs)
output.listDiff(header1, header2, methodListDiffs)
output.listDiff(header1, header2, metadataDiffs)
classMetadataReport?.run { writeAsHtml(output) }
fileFacadeMetadataReport?.run { writeAsHtml(output) }
multiFileClassFacadeMetadataReport?.run { writeAsHtml(output) }
multiFileClassPartMetadataReport?.run { writeAsHtml(output) }
syntheticClassMetadataReport?.run { writeAsHtml(output) }
for (mr in getFilteredMethodReports()) {
mr.writeAsHtml(output)
}
output.listDiff(header1, header2, fieldListDiffs)
for (fr in getFilteredFieldReports()) {
fr.writeAsHtml(output)
}
}
}
@@ -0,0 +1,15 @@
/*
* Copyright 2010-2023 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
interface ComparisonReport {
fun isEmpty(): Boolean
fun writeAsHtml(output: PrintWriter)
}
fun ComparisonReport.isNotEmpty() = !isEmpty()
@@ -0,0 +1,21 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.defects.*
class DefectReport {
val defects: MutableList<Defect> = ArrayList()
fun report(type: DefectType, location: Location, vararg attributes: Pair<DefectAttribute, String>) {
defects.add(Defect(location, DefectInfo(type, attributes.toMap())))
}
fun isEmpty() = defects.isEmpty()
}
@@ -0,0 +1,22 @@
/*
* Copyright 2010-2023 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
class DiffEntry(val value1: String, val value2: String)
class NamedDiffEntry(val name: String, val value1: String, val value2: String)
class ListEntryDiff(val value1: String?, val value2: String?)
class ListDiff(val diff1: List<String>, val diff2: List<String>)
class TextDiffEntry(val lines1: List<String>, val lines2: List<String>)
fun ListEntryDiff.toDiffEntry() =
DiffEntry(value1 ?: "---", value2 ?: "---")
fun ListEntryDiff.toNamedDiffEntry(name: String) =
NamedDiffEntry(name, value1 ?: "---", value2 ?: "---")
@@ -0,0 +1,82 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.checkers.FieldAnnotationsChecker
import org.jetbrains.kotlin.abicmp.defects.*
import org.jetbrains.kotlin.abicmp.escapeHtml
import org.jetbrains.kotlin.abicmp.tag
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
class FieldReport(
private val location: Location.Field,
val fieldId: String,
val header1: String,
val header2: String,
private val defectReport: DefectReport,
) : ComparisonReport {
private val infoParagraphs = ArrayList<String>()
private val propertyDiffs = ArrayList<NamedDiffEntry>()
private val annotationDiffs = ArrayList<NamedDiffEntry>()
fun addInfo(info: String) {
infoParagraphs.add(info)
}
inline fun info(fm: PrintWriter.() -> Unit) {
val bytes = ByteArrayOutputStream()
val ps = PrintWriter(bytes)
ps.fm()
ps.close()
addInfo(String(bytes.toByteArray()))
}
private fun DefectType.report(vararg attributes: Pair<DefectAttribute, String>) {
defectReport.report(this, location, *attributes)
defectReport.report(this, location, *attributes)
}
fun addPropertyDiff(defectType: DefectType, diff: NamedDiffEntry) {
propertyDiffs.add(diff)
defectType.report(VALUE1_A to diff.value1, VALUE2_A to diff.value2)
}
fun addAnnotationDiffs(checker: FieldAnnotationsChecker, diffs: List<ListEntryDiff>) {
for (diff in diffs) {
annotationDiffs.add(diff.toNamedDiffEntry(checker.name))
when {
diff.value1 != null && diff.value2 != null ->
checker.mismatchDefect.report(VALUE1_A to diff.value1, VALUE2_A to diff.value2)
diff.value1 == null && diff.value2 != null ->
checker.missing1Defect.report(VALUE2_A to diff.value2)
diff.value1 != null && diff.value2 == null ->
checker.missing2Defect.report(VALUE1_A to diff.value1)
}
}
}
override fun isEmpty(): Boolean = propertyDiffs.isEmpty() && annotationDiffs.isEmpty()
override fun writeAsHtml(output: PrintWriter) {
output.tag("h2", "&gt; FIELD " + fieldId.escapeHtml())
for (info in infoParagraphs) {
output.tag("p", info)
}
output.propertyDiffTable(header1, header2, propertyDiffs)
output.annotationDiffTable(header1, header2, annotationDiffs)
}
fun TextTreeBuilderContext.appendFieldReport() {
node("FIELD $fieldId") {
appendNamedDiffEntries(header1, header2, propertyDiffs, "Property")
appendNamedDiffEntries(header1, header2, annotationDiffs, "Annotation")
}
}
}
@@ -0,0 +1,106 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.defects.*
import org.jetbrains.kotlin.abicmp.tag
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
val MISSING_CLASS1_D = DefectType("jar.missingClass1", "Missing class in #1", CLASS_A, JAR_FILE1_A)
val MISSING_CLASS2_D = DefectType("jar.missingClass2", "Missing class in #2", CLASS_A, JAR_FILE2_A)
class JarReport(
private val header: String,
private val header1: String,
private val header2: String,
private val jarFileName1: String,
private val jarFileName2: String,
) : ComparisonReport {
private val infoParagraphs = ArrayList<String>()
private val classReports = ArrayList<ClassReport>()
private val missingClassNames1 = HashSet<String>()
private val missingClassNames2 = HashSet<String>()
val defectReport = DefectReport()
fun classLocation(classInternalName: String) =
Location.Class(jarFileName1, classInternalName)
fun addInfo(info: String) {
infoParagraphs.add(info)
}
inline fun info(fm: PrintWriter.() -> Unit) {
val bytes = ByteArrayOutputStream()
val ps = PrintWriter(bytes)
ps.fm()
ps.close()
addInfo(String(bytes.toByteArray()))
}
fun classReport(classInternalName: String) =
ClassReport(classLocation(classInternalName), classInternalName, header1, header2, defectReport)
.also { classReports.add(it) }
private fun getFilteredClassReports(): List<ClassReport> =
classReports.filter { !it.isEmpty() }.sortedBy { it.classInternalName }
private val jar1Location = Location.JarFile(jarFileName1)
private val jar2Location = Location.JarFile(jarFileName2)
fun addMissingClassName1(classInternalName: String) {
missingClassNames1.add(classInternalName)
defectReport.report(
MISSING_CLASS1_D,
jar1Location,
CLASS_A to classInternalName,
JAR_FILE1_A to jarFileName1
)
}
fun addMissingClassName2(classInternalName: String) {
missingClassNames2.add(classInternalName)
defectReport.report(
MISSING_CLASS2_D,
jar2Location,
CLASS_A to classInternalName,
JAR_FILE2_A to jarFileName2
)
}
override fun isEmpty(): Boolean =
defectReport.isEmpty()
override fun writeAsHtml(output: PrintWriter) {
output.tag("h1", header)
for (info in infoParagraphs) {
output.tag("p", info)
}
for (classReport in getFilteredClassReports()) {
classReport.writeAsHtml(output)
}
writeMissingClasses(output, jarFileName1, missingClassNames1)
writeMissingClasses(output, jarFileName2, missingClassNames2)
}
private fun writeMissingClasses(output: PrintWriter, name: String, missing: Collection<String>) {
if (missing.isNotEmpty()) {
output.tag("p", "Classes missing in $name: <b>${missing.size}</b>")
output.tag("ul") {
for (className in missing) {
output.tag("li", className)
}
}
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.tag
import java.io.PrintWriter
class MetadataPropertyReport(val id: String, val header1: String, val header2: String) : ComparisonReport {
private val propertyDiffs = ArrayList<NamedDiffEntry>()
override fun isEmpty() = propertyDiffs.isEmpty()
override fun writeAsHtml(output: PrintWriter) {
if (isEmpty()) return
output.tag("h3", id)
output.propertyDiffTable(header1, header2, propertyDiffs)
}
fun addPropertyDiff(diff: NamedDiffEntry) {
propertyDiffs.add(diff)
}
fun TextTreeBuilderContext.appendReport() {
if (!isEmpty()) {
node(id) {
appendNamedDiffEntries(header1, header2, propertyDiffs, "Property")
}
}
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.checkers.MethodAnnotationsChecker
import org.jetbrains.kotlin.abicmp.checkers.MethodParameterAnnotationsChecker
import org.jetbrains.kotlin.abicmp.defects.*
import org.jetbrains.kotlin.abicmp.escapeHtml
import org.jetbrains.kotlin.abicmp.tag
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
class MethodReport(
private val location: Location.Method,
val methodId: String,
val header1: String,
val header2: String,
private val defectReport: DefectReport,
) : ComparisonReport {
private val infoParagraphs = ArrayList<String>()
private val propertyDiffs = ArrayList<NamedDiffEntry>()
private val annotationDiffs = ArrayList<NamedDiffEntry>()
fun addInfo(info: String) {
infoParagraphs.add(info)
}
inline fun info(fm: PrintWriter.() -> Unit) {
val bytes = ByteArrayOutputStream()
val ps = PrintWriter(bytes)
ps.fm()
ps.close()
addInfo(String(bytes.toByteArray()))
}
private fun DefectType.report(vararg attributes: Pair<DefectAttribute, String>) {
defectReport.report(this, location, *attributes)
}
fun addPropertyDiff(defectType: DefectType, diff: NamedDiffEntry) {
propertyDiffs.add(diff)
defectType.report(VALUE1_A to diff.value1, VALUE2_A to diff.value2)
}
fun addAnnotationDiffs(checker: MethodAnnotationsChecker, diffs: List<ListEntryDiff>) {
for (diff in diffs) {
annotationDiffs.add(diff.toNamedDiffEntry(checker.name))
when {
diff.value1 != null && diff.value2 != null ->
checker.mismatchDefect.report(VALUE1_A to diff.value1, VALUE2_A to diff.value2)
diff.value1 == null && diff.value2 != null ->
checker.missing1Defect.report(VALUE2_A to diff.value2)
diff.value1 != null && diff.value2 == null ->
checker.missing2Defect.report(VALUE1_A to diff.value1)
}
}
}
fun addValueParameterAnnotationDiffs(checker: MethodParameterAnnotationsChecker, index: Int, diffs: List<ListEntryDiff>) {
val diffEntryName = "${checker.name}.p$index"
for (diff in diffs) {
annotationDiffs.add(diff.toNamedDiffEntry(diffEntryName))
when {
diff.value1 != null && diff.value2 != null ->
checker.mismatchDefect.report(
VP_INDEX_A to index.toString(),
VALUE1_A to diff.value1,
VALUE2_A to diff.value2
)
diff.value1 == null && diff.value2 != null ->
checker.missing1Defect.report(
METHOD_A to methodId,
VP_INDEX_A to index.toString(),
VALUE2_A to diff.value2
)
diff.value1 != null && diff.value2 == null ->
checker.missing2Defect.report(
METHOD_A to methodId,
VP_INDEX_A to index.toString(),
VALUE1_A to diff.value1
)
}
}
}
override fun isEmpty(): Boolean = propertyDiffs.isEmpty() && annotationDiffs.isEmpty()
override fun writeAsHtml(output: PrintWriter) {
output.tag("h2", "&gt; METHOD " + methodId.escapeHtml())
for (info in infoParagraphs) {
output.tag("p", info)
}
output.propertyDiffTable(header1, header2, propertyDiffs)
output.annotationDiffTable(header1, header2, annotationDiffs)
}
fun TextTreeBuilderContext.appendMethodReport() {
node("METHOD $methodId") {
appendNamedDiffEntries(header1, header2, propertyDiffs, "Property")
appendNamedDiffEntries(header1, header2, annotationDiffs, "Annotation")
}
}
}
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.tag
import java.io.PrintWriter
class MultiFileClassFacadeMetadataReport(private val classInternalName: String, val header1: String, val header2: String) :
ComparisonReport {
private val membersDiffList = ArrayList<DiffEntry>()
override fun isEmpty() = membersDiffList.isEmpty()
override fun writeAsHtml(output: PrintWriter) {
if (isEmpty()) return
output.tag("h2", "MULTIFILE CLASS FACADE METADATA $classInternalName")
output.listDiff(header1, header2, membersDiffList)
}
fun addMembersListDiffs(diffs: List<ListEntryDiff>) {
for (diff in diffs) {
membersDiffList.add(diff.toDiffEntry())
}
}
fun TextTreeBuilderContext.appendMultiFileClassFacadeReport() {
if (isNotEmpty()) {
node("MULTIFILE FILE FACADE METADATA") {
appendDiffEntries(header1, header2, membersDiffList)
}
}
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.tag
import java.io.PrintWriter
class MultiFileClassPartMetadataReport(private val classInternalName: String, val header1: String, val header2: String) : ComparisonReport {
private val propertyDiffs = ArrayList<NamedDiffEntry>()
private var packageReport: PackageMetadataReport? = null
override fun isEmpty() = propertyDiffs.isEmpty() && packageReport.isNullOrEmpty()
override fun writeAsHtml(output: PrintWriter) {
if (isEmpty()) return
output.tag("h2", "MULTIFILE CLASS PART METADATA $classInternalName")
output.propertyDiffTable(header1, header2, propertyDiffs)
packageReport?.run { writeAsHtml(output) }
}
fun packageReport() = PackageMetadataReport(classInternalName, header1, header2).also { packageReport = it }
fun addPropertyDiff(diff: NamedDiffEntry) {
propertyDiffs.add(diff)
}
fun TextTreeBuilderContext.appendMultiFileClassPartReport() {
if (isNotEmpty()) {
node("MULTIFILE FILE PART METADATA") {
appendNamedDiffEntries(header1, header2, propertyDiffs, "Property")
packageReport?.run { appendPackageMetadataReport() }
}
}
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.tag
import java.io.PrintWriter
class PackageMetadataReport(private val classInternalName: String, val header1: String, val header2: String) : ComparisonReport {
private val membersDiffList = ArrayList<DiffEntry>()
private val functionReports = ArrayList<MetadataPropertyReport>()
private val propertyReports = ArrayList<MetadataPropertyReport>()
private val typeAliasReports = ArrayList<MetadataPropertyReport>()
private val localDelegatedPropertyReport = ArrayList<MetadataPropertyReport>()
override fun isEmpty() =
membersDiffList.isEmpty()
&& functionReports.areAllEmpty()
&& propertyReports.areAllEmpty()
&& typeAliasReports.areAllEmpty()
&& localDelegatedPropertyReport.areAllEmpty()
override fun writeAsHtml(output: PrintWriter) {
if (isEmpty()) return
output.tag("h2", "PACKAGE METADATA $classInternalName")
output.listDiff(header1, header2, membersDiffList)
for (report in listOf(
functionReports,
propertyReports,
typeAliasReports,
localDelegatedPropertyReport
).flatten()) {
report.writeAsHtml(output)
}
}
fun addMembersListDiffs(diffs: List<ListEntryDiff>) {
for (diff in diffs) {
membersDiffList.add(diff.toDiffEntry())
}
}
fun functionReport(id: String) = MetadataPropertyReport("FUNCTION $id", header1, header2).also { functionReports.add(it) }
fun propertyReport(id: String) = MetadataPropertyReport("PROPERTY $id", header1, header2).also { propertyReports.add(it) }
fun typeAliasReport(id: String) = MetadataPropertyReport("TYPE ALIAS $id", header1, header2).also { typeAliasReports.add(it) }
fun localDelegatedPropertyReport(id: String) =
MetadataPropertyReport("LOCAL DELEGATED PROPERTY $id", header1, header2).also { localDelegatedPropertyReport.add(it) }
fun TextTreeBuilderContext.appendPackageMetadataReport() {
if (isNotEmpty()) {
node("PACKAGE METADATA") {
appendDiffEntries(header1, header2, membersDiffList)
for (report in listOf(
functionReports,
propertyReports,
typeAliasReports,
localDelegatedPropertyReport
).flatten()) {
with(report) { appendReport() }
}
}
}
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.*
import org.jetbrains.kotlin.abicmp.defects.DefectInfo
import org.jetbrains.kotlin.abicmp.defects.Location
import java.io.File
import java.io.PrintWriter
class SummaryReport {
private val defectsByInfo: MutableMap<DefectInfo, MutableSet<Location>> = HashMap()
fun add(defectReport: DefectReport) {
for (defect in defectReport.defects) {
defectsByInfo.getOrPut(defect.info) { HashSet() }.add(defect.location)
}
}
fun totalUnique() = defectsByInfo.keys.size
fun totalDefects() = defectsByInfo.values.sumOf { it.size }
fun writeReport(outputFile: File) {
PrintWriter(outputFile).use { out ->
out.tag("html") {
out.tag("head") {
out.tag("style", REPORT_CSS)
}
out.tag("body") {
out.writeReportBody()
}
}
}
}
private fun PrintWriter.writeReportBody() {
tag("p") {
println("Total defects: ${totalDefects().tag("b")}, unique: ${totalUnique().tag("b")}")
}
for (info in defectsByInfo.keys.sorted()) {
val locations = defectsByInfo[info]!!
writeDefectInfo(info)
writeLocations(locations.toList().sorted())
}
}
private fun PrintWriter.writeDefectInfo(info: DefectInfo) {
tag("p") {
tag("h2") {
println("[${info.type.id}] ${info.type.messageText}")
}
}
table {
for ((attr, value) in info.attributes) {
tableData(attr.htmlId, value.toHtmlString().withTag("code"))
}
}
println("&nbsp;<br/>")
}
private fun PrintWriter.writeLocations(locations: List<Location>) {
table {
when (locations.first()) {
is Location.JarFile ->
tableHeader("jar")
is Location.Class ->
tableHeader("jar", "class")
is Location.Method ->
tableHeader("jar", "class", "method")
is Location.Field ->
tableHeader("jar", "class", "field")
}
for (location in locations) {
when (location) {
is Location.JarFile ->
tableDataWithClass(
"location",
location.jarFileName.replace("-", NON_BREAKING_HYPHEN)
)
is Location.Class ->
tableDataWithClass(
"location",
location.jarFileName.replace("-", NON_BREAKING_HYPHEN),
location.className.withTag("code")
)
is Location.Method ->
tableDataWithClass(
"location",
location.jarFileName.replace("-", NON_BREAKING_HYPHEN),
location.className.withTag("code"),
location.methodName.withTag("code")
)
is Location.Field ->
tableDataWithClass(
"location",
location.jarFileName.replace("-", NON_BREAKING_HYPHEN),
location.className.withTag("code"),
location.fieldName.withTag("code")
)
}
}
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.tag
import java.io.PrintWriter
class SyntheticClassMetadataReport(private val classInternalName: String, val header1: String, val header2: String) : ComparisonReport {
private val propertyDiffs = ArrayList<NamedDiffEntry>()
private var functionReport: MetadataPropertyReport? = null
override fun isEmpty() = propertyDiffs.isEmpty() && functionReport.isNullOrEmpty()
override fun writeAsHtml(output: PrintWriter) {
if (isEmpty()) return
output.tag("h2", "SYNTHETIC CLASS METADATA $classInternalName")
functionReport?.run { writeAsHtml(output) }
}
fun functionReport() = MetadataPropertyReport("function", header1, header2).also { functionReport = it }
fun addPropertyDiff(diff: NamedDiffEntry) {
propertyDiffs.add(diff)
}
fun TextTreeBuilderContext.appendSyntheticClassMetadataReport() {
if (isNotEmpty()) {
node("SYNTHETIC CLASS METADATA") {
appendNamedDiffEntries(header1, header2, propertyDiffs, "Property")
}
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2023 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
const val REPORT_CSS = """
table, th, td {
border: 1px solid black;
border-collapse: collapse;
padding: 2px;
}
th {
background: #B0B0B0;
}
td {
background: #EFEFB0;
vertical-align: top;
}
td.location {
background: #E0E0E0;
vertical-align: top;
}
hr {
border: 1px solid black;
border-collapse: collapse;
}
"""
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2023 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 TextTreeBuilderContext(val ident: String, val out: PrintWriter)
private fun String.addIdent(ident: String) = "${ident}${replace("\n", "\n$ident")}"
fun dumpTree(out: PrintWriter, body: TextTreeBuilderContext.() -> Unit) {
TextTreeBuilderContext("", out).also(body)
out.flush()
}
fun TextTreeBuilderContext.node(header: String, body: TextTreeBuilderContext.() -> Unit = {}) {
out.println(header.addIdent(ident))
TextTreeBuilderContext(ident + "\t", out).also(body)
}
fun TextTreeBuilderContext.appendDiffEntries(header1: String, header2: String, diffs: ArrayList<DiffEntry>) {
if (diffs.isNotEmpty()) {
for (diff in diffs) {
node(header1) {
node(diff.value1)
}
node(header2) {
node(diff.value2)
}
}
}
}
fun TextTreeBuilderContext.appendNamedDiffEntries(header1: String, header2: String, diffEntries: ArrayList<NamedDiffEntry>, title: String) {
if (diffEntries.isNotEmpty()) {
for (entry in diffEntries) {
node("$title: ${entry.name}") {
node(header1) {
node(entry.value1)
}
node(header2) {
node(entry.value2)
}
}
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2010-2023 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 org.jetbrains.kotlin.abicmp.*
import java.io.PrintWriter
fun String.withTag(tagName: String) = "<$tagName>$this</$tagName>"
fun PrintWriter.propertyDiffTable(header1: String, header2: String, propertyDiffs: List<NamedDiffEntry>) {
if (propertyDiffs.isNotEmpty()) {
table {
tableHeader("Property", header1, header2)
for (pd in propertyDiffs) {
tableData(pd.name, pd.value1.toHtmlString().withTag("code"), pd.value2.toHtmlString().withTag("code"))
}
}
println("&nbsp;")
}
}
fun PrintWriter.annotationDiffTable(header1: String, header2: String, annotationDiffs: List<NamedDiffEntry>) {
if (annotationDiffs.isNotEmpty()) {
table {
tableHeader("Annotation", header1, header2)
for (ad in annotationDiffs) {
tableData(ad.name, ad.value1.escapeHtml().withTag("code"), ad.value2.escapeHtml().withTag("code"))
}
}
println("&nbsp;")
}
}
fun PrintWriter.listDiff(header1: String, header2: String, listDiffs: List<DiffEntry>) {
if (listDiffs.isNotEmpty()) {
table {
tableHeader(header1, header2)
for (me in listDiffs) {
tableData(me.value1.escapeHtml().withTag("code"), me.value2.escapeHtml().withTag("code"))
}
}
println("&nbsp;")
}
}
fun ComparisonReport?.isNullOrEmpty() = this == null || isEmpty()
fun List<ComparisonReport>.areAllEmpty() = !any { it.isNotEmpty() }
@@ -0,0 +1,224 @@
/*
* Copyright 2010-2023 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 kotlinx.metadata.ExperimentalContextReceivers
import kotlinx.metadata.hasConstant
import kotlinx.metadata.isVar
import kotlinx.metadata.jvm.*
import kotlinx.metadata.visibility
import org.jetbrains.kotlin.abicmp.*
import org.jetbrains.kotlin.abicmp.checkers.*
import org.jetbrains.kotlin.kotlinp.*
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.FieldNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import kotlin.contracts.ExperimentalContracts
private val allClassCheckers = listOf(
classPropertyChecker(ClassNode::version),
classPropertyChecker(ClassNode::access) { v -> "${v.toString(2)} ${v.classFlags()}" },
classPropertyChecker("internalName", ClassNode::name),
classPropertyChecker(ClassNode::signature),
classPropertyChecker("superClassInternalName", ClassNode::superName),
classPropertyChecker("superInterfaces") { (it.interfaces as List<String>).sorted() },
classPropertyChecker(ClassNode::sourceFile),
classPropertyChecker(ClassNode::outerClass),
classPropertyChecker(ClassNode::outerMethod),
classPropertyChecker(ClassNode::outerMethodDesc),
ClassAnnotationsChecker(ClassNode::visibleAnnotations),
ClassAnnotationsChecker(ClassNode::invisibleAnnotations),
InnerClassesListChecker(),
MethodsListChecker(),
FieldsListChecker()
)
private val allMethodCheckers = listOf(
methodPropertyChecker(MethodNode::access) { v -> "${v.toString(2)} ${v.methodFlags()}" },
methodPropertyChecker("methodName", MethodNode::name),
methodPropertyChecker(MethodNode::desc),
methodPropertyChecker(MethodNode::signature),
methodPropertyChecker("exceptions") { it.exceptions.listOfNotNull<String>().sorted() },
methodPropertyChecker("annotationDefault") { it.annotationDefault?.toAnnotationArgumentValue() },
MethodAnnotationsChecker(MethodNode::visibleAnnotations),
MethodAnnotationsChecker(MethodNode::invisibleAnnotations),
MethodParameterAnnotationsChecker(MethodNode::visibleParameterAnnotations),
MethodParameterAnnotationsChecker(MethodNode::invisibleParameterAnnotations)
)
private val allConstructorMetadataCheckers = listOf(
constructorMetadataPropertyChecker("versionRequirements") { it.versionRequirements.stringifyRelevantRequirements() },
constructorMetadataPropertyChecker("modifiers") { printConstructorModifiers(it) },
constructorMetadataPropertyChecker("valueParameters") { it.valueParameters.stringifyValueParameters() }
)
private val allFunctionMetadataCheckers = listOf(
functionMetadataPropertyChecker("lambdaClassOriginName") { it.lambdaClassOriginName.toString() },
functionMetadataPropertyChecker("versionRequirements") { it.versionRequirements.stringifyRelevantRequirements() },
functionMetadataPropertyChecker("contextReceiverTypes") {
@OptIn(ExperimentalContextReceivers::class)
it.contextReceiverTypes.stringifyTypeListSorted()
},
functionMetadataPropertyChecker("modifiers") { printFunctionModifiers(it) },
functionMetadataPropertyChecker("typeParameters") { it.typeParameters.stringifyTypeParameters() },
functionMetadataPropertyChecker("receiverParameterType") {
it.receiverParameterType?.let { type -> printType(type) } ?: PROPERTY_VAL_STUB
},
functionMetadataPropertyChecker("valueParameters") { it.valueParameters.stringifyValueParameters() },
functionMetadataPropertyChecker("contract") {
@OptIn(ExperimentalContracts::class)
it.contract?.let { contract -> printContract(contract) } ?: PROPERTY_VAL_STUB
}
)
private val allPropertyMetadataCheckers = listOf(
propertyMetadataPropertyChecker("versionRequirements") { it.versionRequirements.stringifyRelevantRequirements() },
propertyMetadataPropertyChecker("fieldSignature") { it.fieldSignature?.toString() ?: PROPERTY_VAL_STUB },
propertyMetadataPropertyChecker("getterSignature") { it.getterSignature?.toString() ?: PROPERTY_VAL_STUB },
propertyMetadataPropertyChecker("setterSignature") { it.setterSignature?.toString() ?: PROPERTY_VAL_STUB },
propertyMetadataPropertyChecker("syntheticMethodForAnnotations") { it.syntheticMethodForAnnotations?.toString() ?: PROPERTY_VAL_STUB },
propertyMetadataPropertyChecker("syntheticMethodForDelegate") { it.syntheticMethodForDelegate?.toString() ?: PROPERTY_VAL_STUB },
propertyMetadataPropertyChecker("isMovedFromInterfaceCompanion") { it.isMovedFromInterfaceCompanion.toString() },
propertyMetadataPropertyChecker("contextReceiverTypes") {
@OptIn(ExperimentalContextReceivers::class)
it.contextReceiverTypes.stringifyTypeListSorted()
},
propertyMetadataPropertyChecker("modifiers") { printPropertyModifiers(it) },
propertyMetadataPropertyChecker("isVar") { it.isVar.toString() },
propertyMetadataPropertyChecker("typeParameters") { it.typeParameters.stringifyTypeParameters() },
propertyMetadataPropertyChecker("receiverParameterType") {
it.receiverParameterType?.let { type -> printType(type) } ?: PROPERTY_VAL_STUB
},
propertyMetadataPropertyChecker("returnType") { printType(it.returnType) },
propertyMetadataPropertyChecker("hasConstant") { it.hasConstant.toString() },
propertyMetadataPropertyChecker("getterModifiers") { printPropertyAccessorModifiers(it.getter) },
propertyMetadataPropertyChecker("setterModifiers") {
it.setter?.let { setter -> printPropertyAccessorModifiers(setter) } ?: PROPERTY_VAL_STUB
},
propertyMetadataPropertyChecker("setterValueParameter") {
it.setterParameter?.let { param -> printValueParameter(param) } ?: PROPERTY_VAL_STUB
}
)
private val allTypeAliasMetadataCheckers = listOf(
typeAliasMetadataPropertyChecker("versionRequirements") { it.versionRequirements.stringifyRelevantRequirements() },
typeAliasMetadataPropertyChecker("annotations") { it.annotations.stringifyAnnotations() },
typeAliasMetadataPropertyChecker("visibility") { it.visibility.toString() },
typeAliasMetadataPropertyChecker("typeParameters") { it.typeParameters.stringifyTypeParameters() },
typeAliasMetadataPropertyChecker("underlyingType") { printType(it.underlyingType) },
typeAliasMetadataPropertyChecker("expandedType") { printType(it.expandedType) }
)
private val allFieldCheckers = listOf(
fieldPropertyChecker(FieldNode::access) { v -> "${v.toString(2)} ${v.fieldFlags()}" },
fieldPropertyChecker("fieldName", FieldNode::name),
fieldPropertyChecker(FieldNode::desc),
fieldPropertyChecker(FieldNode::signature),
fieldPropertyChecker("initialValue", FieldNode::value),
FieldAnnotationsChecker(FieldNode::visibleAnnotations),
FieldAnnotationsChecker(FieldNode::invisibleAnnotations)
)
private val allClassMetadataCheckers = listOf(
classMetadataListChecker("constructors") { loadConstructors(it).keys.toList() },
classMetadataListChecker("functions") { loadFunctions(it.kmClass).keys.toList() },
classMetadataListChecker("properties") { loadProperties(it.kmClass).keys.toList() },
classMetadataListChecker("typeAliases") { it.kmClass.typeAliases.map { typeAlias -> typeAlias.name } },
classMetadataListChecker("nestedClasses") { it.kmClass.nestedClasses },
classMetadataListChecker("enumEntries") { it.kmClass.enumEntries },
classMetadataListChecker("sealedSubclasses") { it.kmClass.sealedSubclasses },
classMetadataListChecker("localDelegatedProperties") { loadLocalDelegatedProperties(it).keys.toList() },
classMetadataPropertyChecker("typeParameters") { it.kmClass.typeParameters.stringifyTypeParameters() },
classMetadataPropertyChecker("superTypes") {
it.kmClass.supertypes.map { type -> printType(type) }.sorted().joinToString(prefix = "[", postfix = "]")
},
classMetadataPropertyChecker("companionObject") { it.kmClass.companionObject.toString() },
classMetadataPropertyChecker("inlineClassUnderlyingPropertyName") { it.kmClass.inlineClassUnderlyingPropertyName.toString() },
classMetadataPropertyChecker("inlineClassUnderlyingType") {
it.kmClass.inlineClassUnderlyingType?.let { type -> printType(type) } ?: "---"
},
classMetadataPropertyChecker("contextReceiverTypes") {
@OptIn(ExperimentalContextReceivers::class)
it.kmClass.contextReceiverTypes.stringifyTypeListSorted()
},
classMetadataPropertyChecker("versionRequirements") { it.kmClass.versionRequirements.stringifyRelevantRequirements() }
)
private val allPackageMetadataCheckers = listOf(
fileFacadeMetadataListChecker("functions") { loadFunctions(it).keys.toList() },
fileFacadeMetadataListChecker("properties") { loadProperties(it).keys.toList() },
fileFacadeMetadataListChecker("typeAliases") { loadTypeAliases(it).keys.toList() },
fileFacadeMetadataListChecker("localDelegatedProperties") { loadLocalDelegatedProperties(it).keys.toList() }
)
private val allMultifileClassFacadeMetadataCheckers = listOf(
multiFileClassFacadeMetadataListChecker("partClassNames") { it.partClassNames }
)
private val allMultifileClassPartMetadataCheckers = listOf(
multiFileClassPartMetadataPropertyChecker("facadeClassName") { it.facadeClassName }
)
private val allSyntheticClassMetadataCheckers = listOf(
syntheticClassMetadataPropertyChecker("isLambda") { it.isLambda.toString() },
syntheticClassMetadataPropertyChecker("function") {
it.kmLambda?.function?.let { function ->
printFunction(
function,
KotlinpSettings(
isVerbose = true,
sortDeclarations = true
)
)
} ?: PROPERTY_VAL_STUB
}
)
class CheckerConfigurationBuilder {
private val enabledExclusively = HashSet<String>()
private val disabled = HashSet<String>()
fun enableExclusively(name: String) {
enabledExclusively.add(name)
}
fun disable(name: String) {
disabled.add(name)
}
fun build() = CheckerConfiguration(enabledExclusively, disabled)
}
inline fun checkerConfiguration(b: CheckerConfigurationBuilder.() -> Unit): CheckerConfiguration {
val builder = CheckerConfigurationBuilder()
builder.b()
return builder.build()
}
class CheckerConfiguration(private val enabledExclusively: Set<String>, private val disabled: Set<String>) {
private fun <T : Checker> List<T>.filterOutDisabled() = filter { it.isEnabled() }
val enabledClassCheckers: List<ClassChecker> = allClassCheckers.filterOutDisabled()
val enabledMethodCheckers: List<MethodChecker> = allMethodCheckers.filterOutDisabled()
val enabledFieldCheckers: List<FieldChecker> = allFieldCheckers.filterOutDisabled()
val enabledPropertyMetadataCheckers = allPropertyMetadataCheckers.filterOutDisabled()
val enabledConstructorMetadataCheckers = allConstructorMetadataCheckers.filterOutDisabled()
val enabledFunctionMetadataCheckers = allFunctionMetadataCheckers.filterOutDisabled()
val enabledTypeAliasMetadataCheckers = allTypeAliasMetadataCheckers.filterOutDisabled()
val enabledClassMetadataCheckers = allClassMetadataCheckers.filterOutDisabled()
val enabledPackageMetadataCheckers = allPackageMetadataCheckers.filterOutDisabled()
val enabledMultifileClassFacadeMetadataCheckers = allMultifileClassFacadeMetadataCheckers.filterOutDisabled()
val enabledMultifileClassPartMetadataCheckers = allMultifileClassPartMetadataCheckers.filterOutDisabled()
val enabledAllSyntheticClassMetadataCheckers = allSyntheticClassMetadataCheckers.filterOutDisabled()
private fun Checker.isEnabled(): Boolean {
if (enabledExclusively.isNotEmpty() && name !in enabledExclusively) return false
return name !in disabled
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2010-2023 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 kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.checkers.*
import org.jetbrains.kotlin.abicmp.reports.ClassMetadataReport
class ClassMetadataTask(
private val configuration: CheckerConfiguration,
private val metadata1: KotlinClassMetadata.Class,
private val metadata2: KotlinClassMetadata.Class,
private val report: ClassMetadataReport,
) : Runnable {
override fun run() {
for (checker in configuration.enabledClassMetadataCheckers) {
checker.check(metadata1, metadata2, report)
}
checkMetadataMembers(
metadata1,
metadata2,
configuration.enabledConstructorMetadataCheckers,
report::constructorReport,
::loadConstructors
)
checkMetadataMembers(
metadata1,
metadata2,
configuration.enabledFunctionMetadataCheckers,
report::functionReport
) { loadFunctions(it.kmClass) }
checkMetadataMembers(
metadata1,
metadata2,
configuration.enabledPropertyMetadataCheckers,
report::propertyReport
) { loadProperties(it.kmClass) }
checkMetadataMembers(
metadata1,
metadata2,
configuration.enabledTypeAliasMetadataCheckers,
report::typeAliasReport
) { loadTypeAliases(it.kmClass) }
checkMetadataMembers(
metadata1,
metadata2,
configuration.enabledPropertyMetadataCheckers,
report::localDelegatedPropertyReport
) { loadLocalDelegatedProperties(it) }
}
}
@@ -0,0 +1,152 @@
/*
* Copyright 2010-2023 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 kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.checkers.loadFields
import org.jetbrains.kotlin.abicmp.checkers.loadMethods
import org.jetbrains.kotlin.abicmp.classFlags
import org.jetbrains.kotlin.abicmp.isSynthetic
import org.jetbrains.kotlin.abicmp.reports.ClassReport
import org.jetbrains.kotlin.abicmp.reports.ListEntryDiff
import org.jetbrains.kotlin.abicmp.tag
import org.jetbrains.kotlin.kotlinp.Kotlinp
import org.jetbrains.kotlin.kotlinp.KotlinpSettings
import org.jetbrains.kotlin.kotlinp.readKotlinClassHeader
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassWriter
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.FieldNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
class ClassTask(
private val checkerConfiguration: CheckerConfiguration,
private val class1: ClassNode,
private val class2: ClassNode,
private val report: ClassReport,
) : Runnable {
override fun run() {
addClassInfo()
checkMetadata()
for (checker in checkerConfiguration.enabledClassCheckers) {
checker.check(class1, class2, report)
}
checkMethods()
checkFields()
}
private fun checkMetadata() {
fun ClassNode.getMetadata(): KotlinClassMetadata? {
val classWriter = ClassWriter(0)
accept(classWriter)
val classReader = ClassReader(classWriter.toByteArray())
return classReader.readKotlinClassHeader()?.run { KotlinClassMetadata.readStrict(this) }
}
val kotlinp = Kotlinp(KotlinpSettings(isVerbose = false, sortDeclarations = true))
val metadata1 = class1.getMetadata()
val metadata2 = class2.getMetadata()
if (metadata1 == null && metadata2 == null) return
if (metadata1 == null || metadata2 == null) {
report.addMetadataDiff(
ListEntryDiff(
metadata1?.run { kotlinp.renderClassFile(metadata1) },
metadata2?.run { kotlinp.renderClassFile(metadata2) })
)
return
}
if (metadata1::class != metadata2::class) {
report.addMetadataDiff(ListEntryDiff(metadata1::class.simpleName, metadata2::class.simpleName))
return
}
when (metadata1) {
is KotlinClassMetadata.Class -> ClassMetadataTask(
checkerConfiguration,
metadata1,
metadata2 as KotlinClassMetadata.Class,
report.classMetadataReport()
).run()
is KotlinClassMetadata.FileFacade -> PackageMetadataTask(
checkerConfiguration,
metadata1.kmPackage,
(metadata2 as KotlinClassMetadata.FileFacade).kmPackage,
report.fileFacadeMetadataReport()
).run()
is KotlinClassMetadata.MultiFileClassFacade -> MultiFileClassFacadeMetadataTask(
checkerConfiguration,
metadata1,
metadata2 as KotlinClassMetadata.MultiFileClassFacade,
report.multiFileClassFacadeMetadataReport()
)
is KotlinClassMetadata.MultiFileClassPart -> MultiFileClassPartMetadataTask(
checkerConfiguration,
metadata1,
metadata2 as KotlinClassMetadata.MultiFileClassPart,
report.multiFileClassPartMetadataReport()
)
is KotlinClassMetadata.SyntheticClass -> SyntheticMetadataTask(
checkerConfiguration,
metadata1,
metadata2 as KotlinClassMetadata.SyntheticClass,
report.syntheticMetadataReport()
)
is KotlinClassMetadata.Unknown -> {}
}
}
private fun addClassInfo() {
report.info {
tag("p") {
tag("b", report.header1)
println(": ${class1.access.classFlags()}")
}
tag("p") {
tag("b", report.header2)
println(": ${class2.access.classFlags()}")
}
}
}
private fun checkMethods() {
val methods1 = class1.loadMethods()
val methods2 = class2.loadMethods()
val commonIds = methods1.keys.intersect(methods2.keys).sorted()
for (id in commonIds) {
val method1 = methods1[id]!!
val method2 = methods2[id]!!
if (method1.access.isSynthetic() && method2.access.isSynthetic()) continue
val methodReport = report.methodReport(id)
MethodTask(checkerConfiguration, method1, method2, methodReport).run()
}
}
private fun checkFields() {
val fields1 = class1.loadFields()
val fields2 = class2.loadFields()
val commonIds = fields1.keys.intersect(fields2.keys).sorted()
for (id in commonIds) {
val field1 = fields1[id]!!
val field2 = fields2[id]!!
if (field1.access.isSynthetic() && field2.access.isSynthetic()) continue
val fieldReport = report.fieldReport(id)
FieldTask(checkerConfiguration, field1, field2, fieldReport).run()
}
}
}
fun MethodNode.methodId() = "$name$desc"
fun FieldNode.fieldId() = "$name:$desc"
@@ -0,0 +1,79 @@
/*
* Copyright 2010-2023 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.SummaryReport
import java.io.File
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.jar.JarFile
class DirTask(
private val dir1: File,
private val dir2: File,
private val id1: String?,
private val id2: String?,
private val header1: String,
private val header2: String,
private val reportDir: File,
private val checkerConfiguration: CheckerConfiguration = checkerConfiguration {},
) : Runnable {
private val executor = Executors.newWorkStealingPool()
private val lastNameIndex = HashMap<String, Int>()
private val tasks = ArrayList<Pair<JarTask, Future<*>>>()
override fun run() {
println("Comparing directories: $dir1, $dir2")
walkRecursively(dir1, dir2)
val summary = SummaryReport()
tasks.forEach { (jarTask, future) ->
future.get()
summary.add(jarTask.defectReport)
}
val summaryFile = File(reportDir, "SUMMARY.html")
println("Writing summary: $summaryFile")
summary.writeReport(summaryFile)
println("Done, ${summary.totalDefects()} defects, ${summary.totalUnique()} unique")
}
private fun walkRecursively(subdir1: File, subdir2: File) {
val files1 = subdir1.listFiles() ?: return
for (file1 in files1) {
val file2 = File(subdir2, file1.name.replaceIfNotNull(id1, id2))
if (file1.canRead() && file2.exists() && file2.canRead()) {
if (file1.isDirectory) {
if (file2.isDirectory) {
println("Comparing subdirectories: $file1, $file2")
walkRecursively(file1, file2)
}
} else if (file1.name.endsWith(".jar")) {
println("Comparing jars: $file1, $file2")
val index0 = lastNameIndex.getOrElse(file1.name) { 0 }
val index = index0 + 1
lastNameIndex[file1.name] = index
val jarTaskHeader = file1.name.replaceIfNotNull(id1, "").replace(".jar", "")
val reportFile = File(reportDir, "$jarTaskHeader-REPORT-$index.html")
val jarTask = JarTask(
jarTaskHeader,
JarFile(file1), JarFile(file2),
header1, header2,
reportFile, checkerConfiguration
)
tasks.add(jarTask to executor.submit(jarTask))
}
}
}
}
private fun String.replaceIfNotNull(pattern: String?, replaceWith: String?) =
if (pattern != null && replaceWith != null)
replace(pattern, replaceWith)
else
this
}
@@ -0,0 +1,40 @@
/*
* Copyright 2010-2023 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.fieldFlags
import org.jetbrains.kotlin.abicmp.reports.FieldReport
import org.jetbrains.kotlin.abicmp.tag
import org.jetbrains.org.objectweb.asm.tree.FieldNode
class FieldTask(
private val checkerConfiguration: CheckerConfiguration,
private val field1: FieldNode,
private val field2: FieldNode,
private val report: FieldReport,
) : Runnable {
override fun run() {
addFieldInfo()
for (checker in checkerConfiguration.enabledFieldCheckers) {
checker.check(field1, field2, report)
}
}
private fun addFieldInfo() {
report.info {
tag("p") {
tag("b", report.header1)
println(": ${field1.access.fieldFlags()}")
}
tag("p") {
tag("b", report.header2)
println(": ${field2.access.fieldFlags()}")
}
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2010-2023 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.checkers.GenericMetadataChecker
import org.jetbrains.kotlin.abicmp.reports.MetadataPropertyReport
class GenericMetadataTask<T>(
private val metadata1: T,
private val metadata2: T,
private val report: MetadataPropertyReport,
private val checkers: List<GenericMetadataChecker<T>>
) : Runnable {
override fun run() {
for (checker in checkers) {
checker.check(metadata1, metadata2, report)
}
}
}
@@ -0,0 +1,152 @@
/*
* Copyright 2010-2023 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.checkers.isSamAdapterName
import org.jetbrains.kotlin.abicmp.classFlags
import org.jetbrains.kotlin.abicmp.isSynthetic
import org.jetbrains.kotlin.abicmp.listOfNotNull
import org.jetbrains.kotlin.abicmp.reports.JarReport
import org.jetbrains.kotlin.abicmp.reports.REPORT_CSS
import org.jetbrains.kotlin.abicmp.reports.isNotEmpty
import org.jetbrains.kotlin.abicmp.tag
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import org.jetbrains.org.objectweb.asm.tree.InnerClassNode
import java.io.File
import java.io.InputStream
import java.io.PrintWriter
import java.util.jar.JarFile
class JarTask(
header: String,
private val jarFile1: JarFile,
private val jarFile2: JarFile,
private val header1: String,
private val header2: String,
private val outputFile: File,
private val checkerConfiguration: CheckerConfiguration,
) : Runnable {
private val ignoreAnonymousOrLocalClasses = true
private val ignoreJavaClasses = true
private val report = JarReport(header, header1, header2, jarFile1.name, jarFile2.name)
val defectReport get() = report.defectReport
private var totalDiffs = 0
private val names1 = HashSet<String>()
override fun run() {
println("Comparing jars: ${jarFile1.name}, ${jarFile2.name}")
addJarsInfo()
checkJarFile1()
checkJarFile2()
writeReportIfRequired()
}
private fun checkJarFile1() {
jarFile1.stream().forEach { entry1 ->
val name1 = entry1.name
if (name1.endsWith(".class")) {
names1.add(name1)
val entry2 = jarFile2.getEntry(name1)
if (entry2 == null) {
val classNode = parseClassNode(jarFile1.getInputStream(entry1))
if (!classNode.shouldBeIgnored()) {
report.addMissingClassName2("$name1 ${classNode.access.classFlags()}")
++totalDiffs
}
} else {
val class1 = parseClassNode(jarFile1.getInputStream(entry1))
val class2 = parseClassNode(jarFile2.getInputStream(entry2))
if (!class1.shouldBeIgnored() || !class2.shouldBeIgnored()) {
println("Comparing classes: ${class1.name}")
val classReport = report.classReport(class1.name)
val classTask = ClassTask(checkerConfiguration, class1, class2, classReport)
classTask.run()
if (classReport.isNotEmpty()) {
++totalDiffs
}
} else {
println("Skipping $name1")
}
}
}
}
}
private fun parseClassNode(input: InputStream): ClassNode =
ClassNode().also { ClassReader(input).accept(it, ClassReader.SKIP_CODE) }
private fun checkJarFile2() {
jarFile2.stream().forEach { entry2 ->
val name2 = entry2.name
if (name2.endsWith(".class") && name2 !in names1) {
val classNode = parseClassNode(jarFile2.getInputStream(entry2))
if (!classNode.shouldBeIgnored()) {
report.addMissingClassName1("$name2 ${classNode.access.classFlags()}")
++totalDiffs
}
}
}
}
private fun writeReportIfRequired() {
if (report.isNotEmpty()) {
PrintWriter(outputFile).use { out ->
out.tag("html") {
out.tag("head") {
out.printCss()
}
out.tag("body") {
report.writeAsHtml(out)
}
}
}
}
}
private fun PrintWriter.printCss() {
tag("style", REPORT_CSS)
}
private fun ClassNode.shouldBeIgnored(): Boolean {
if (ignoreJavaClasses && isJavaClass()) return true
if (access.isSynthetic()) return true
if (ignoreAnonymousOrLocalClasses && isAnonymousOrLocalClass()) return true
return false
}
private fun ClassNode.isAnonymousOrLocalClass() =
isSamAdapterName(name) ||
innerClasses.listOfNotNull<InnerClassNode>().any {
it.name == this.name && it.outerName == null
}
private fun ClassNode.isJavaClass() =
sourceFile != null && sourceFile.endsWith(".java")
private fun addJarsInfo() {
report.info {
tag("p") {
tag("b", header1)
println(": ${jarFile1.name}")
}
tag("p") {
tag("b", header2)
println(": ${jarFile2.name}")
}
}
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2010-2023 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.methodFlags
import org.jetbrains.kotlin.abicmp.reports.MethodReport
import org.jetbrains.kotlin.abicmp.tag
import org.jetbrains.org.objectweb.asm.tree.MethodNode
class MethodTask(
private val checkerConfiguration: CheckerConfiguration,
private val method1: MethodNode,
private val method2: MethodNode,
private val report: MethodReport,
) : Runnable {
override fun run() {
addMethodInfo()
for (checker in checkerConfiguration.enabledMethodCheckers) {
checker.check(method1, method2, report)
}
}
private fun addMethodInfo() {
report.info {
tag("p") {
tag("b", report.header1)
println(": ${method1.access.methodFlags()}")
}
tag("p") {
tag("b", report.header2)
println(": ${method2.access.methodFlags()}")
}
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright 2010-2023 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 kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.MultiFileClassFacadeMetadataReport
class MultiFileClassFacadeMetadataTask(
private val configuration: CheckerConfiguration,
private val metadata1: KotlinClassMetadata.MultiFileClassFacade,
private val metadata2: KotlinClassMetadata.MultiFileClassFacade,
private val report: MultiFileClassFacadeMetadataReport,
) : Runnable {
override fun run() {
for (checker in configuration.enabledMultifileClassFacadeMetadataCheckers) {
checker.check(metadata1, metadata2, report)
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2010-2023 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 kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.MultiFileClassPartMetadataReport
class MultiFileClassPartMetadataTask(
private val configuration: CheckerConfiguration,
private val metadata1: KotlinClassMetadata.MultiFileClassPart,
private val metadata2: KotlinClassMetadata.MultiFileClassPart,
private val report: MultiFileClassPartMetadataReport,
) : Runnable {
override fun run() {
for (checker in configuration.enabledMultifileClassPartMetadataCheckers) {
checker.check(metadata1, metadata2, report)
}
PackageMetadataTask(configuration, metadata1.kmPackage, metadata2.kmPackage, report.packageReport()).run()
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2023 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 kotlinx.metadata.KmPackage
import org.jetbrains.kotlin.abicmp.checkers.*
import org.jetbrains.kotlin.abicmp.reports.PackageMetadataReport
class PackageMetadataTask(
private val configuration: CheckerConfiguration,
private val metadata1: KmPackage,
private val metadata2: KmPackage,
private val report: PackageMetadataReport,
) : Runnable {
override fun run() {
for (checker in configuration.enabledPackageMetadataCheckers) {
checker.check(metadata1, metadata2, report)
}
checkMetadataMembers(metadata1, metadata2, configuration.enabledFunctionMetadataCheckers, report::functionReport, ::loadFunctions)
checkMetadataMembers(metadata1, metadata2, configuration.enabledPropertyMetadataCheckers, report::propertyReport, ::loadProperties)
checkMetadataMembers(
metadata1,
metadata2,
configuration.enabledTypeAliasMetadataCheckers,
report::typeAliasReport,
::loadTypeAliases
)
checkMetadataMembers(
metadata1,
metadata2,
configuration.enabledPropertyMetadataCheckers,
report::localDelegatedPropertyReport,
::loadLocalDelegatedProperties
)
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2010-2023 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 kotlinx.metadata.jvm.KotlinClassMetadata
import org.jetbrains.kotlin.abicmp.reports.SyntheticClassMetadataReport
class SyntheticMetadataTask(
private val configuration: CheckerConfiguration,
private val metadata1: KotlinClassMetadata.SyntheticClass,
private val metadata2: KotlinClassMetadata.SyntheticClass,
private val report: SyntheticClassMetadataReport,
) : Runnable {
override fun run() {
for (checker in configuration.enabledAllSyntheticClassMetadataCheckers) {
checker.check(metadata1, metadata2, report)
}
val lambda1 = metadata1.kmLambda
val lambda2 = metadata2.kmLambda
if (lambda1 != null && lambda2 != null) {
GenericMetadataTask(
lambda1.function,
lambda2.function,
report.functionReport(),
configuration.enabledFunctionMetadataCheckers
).run()
}
}
}
@@ -0,0 +1,10 @@
/*
* Copyright 2010-2023 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
inline fun <reified T : Any> List<Any?>?.listOfNotNull() = orEmpty().filterIsInstance<T>()
const val PROPERTY_VAL_STUB = "---"
@@ -13,6 +13,10 @@ private object SpecialCharacters {
const val TYPE_ALIAS_MARKER = '^'
}
fun printFunction(function: KmFunction, settings: KotlinpSettings): String {
return StringBuilder().also { visitFunction(function, settings, it) }.toString()
}
@OptIn(ExperimentalContextReceivers::class, ExperimentalContracts::class)
private fun visitFunction(
function: KmFunction,
@@ -153,7 +157,7 @@ private fun visitTypeAlias(
sb.appendLine()
}
private fun printType(type: KmType): String {
fun printType(type: KmType): String {
val classifier = when (val cls = type.classifier) {
is KmClassifier.Class -> cls.name
is KmClassifier.TypeParameter -> "T#${cls.id}"
@@ -221,7 +225,7 @@ private fun printType(type: KmType): String {
}
}
private fun printTypeParameter(
fun printTypeParameter(
typeParameter: KmTypeParameter,
settings: KotlinpSettings
): String = buildString {
@@ -241,7 +245,7 @@ private fun printTypeParameter(
}
}
private fun printValueParameter(
fun printValueParameter(
valueParameter: KmValueParameter
): String {
val type = printType(valueParameter.type)
@@ -259,7 +263,7 @@ private fun printValueParameter(
}
}
private fun renderAnnotation(annotation: KmAnnotation): String =
fun renderAnnotation(annotation: KmAnnotation): String =
annotation.className + if (annotation.arguments.isEmpty()) "" else
annotation.arguments.entries.joinToString(prefix = "(", postfix = ")") { (name, argument) ->
"$name = ${renderAnnotationArgument(argument)}"
@@ -313,7 +317,7 @@ private fun String.sanitize(quote: Char): String =
}
}
private fun printVersionRequirement(versionRequirement: KmVersionRequirement): String {
fun printVersionRequirement(versionRequirement: KmVersionRequirement): String {
val version = with(versionRequirement.version) { "$major.$minor.$patch" }
return buildString {
@@ -363,7 +367,7 @@ private fun <T, R : Comparable<R>> Iterable<T>.sortIfNeededBy(settings: KotlinpS
}
@ExperimentalContracts
private fun printContract(kmContract: KmContract): String = buildString {
fun printContract(kmContract: KmContract): String = buildString {
appendLine("contract {")
kmContract.effects.map(::printEffect).forEach { effect ->
appendLine(" $effect")
@@ -746,7 +750,7 @@ private fun StringBuilder.appendClassModifiers(kmClass: KmClass) {
append(CLASS_KIND_MAP[kmClass.kind])
}
private fun StringBuilder.appendConstructorModifiers(kmConstructor: KmConstructor) {
fun printConstructorModifiers(kmConstructor: KmConstructor) = buildString {
append(VISIBILITY_MAP[kmConstructor.visibility])
appendFlags(
kmConstructor.isSecondary to "/* secondary */",
@@ -754,7 +758,11 @@ private fun StringBuilder.appendConstructorModifiers(kmConstructor: KmConstructo
)
}
private fun StringBuilder.appendFunctionModifiers(kmFunction: KmFunction) {
private fun StringBuilder.appendConstructorModifiers(kmConstructor: KmConstructor) {
append(printConstructorModifiers(kmConstructor))
}
fun printFunctionModifiers(kmFunction: KmFunction) = buildString {
append(VISIBILITY_MAP[kmFunction.visibility])
append(MODALITY_MAP[kmFunction.modality])
append(MEMBER_KIND_MAP[kmFunction.kind])
@@ -770,7 +778,11 @@ private fun StringBuilder.appendFunctionModifiers(kmFunction: KmFunction) {
)
}
private fun StringBuilder.appendPropertyModifiers(kmProperty: KmProperty) {
private fun StringBuilder.appendFunctionModifiers(kmFunction: KmFunction) {
append(printFunctionModifiers(kmFunction))
}
fun printPropertyModifiers(kmProperty: KmProperty) = buildString {
append(VISIBILITY_MAP[kmProperty.visibility])
append(MODALITY_MAP[kmProperty.modality])
append(MEMBER_KIND_MAP[kmProperty.kind])
@@ -783,7 +795,11 @@ private fun StringBuilder.appendPropertyModifiers(kmProperty: KmProperty) {
)
}
private fun StringBuilder.appendPropertyAccessorModifiers(accessorAttributes: KmPropertyAccessorAttributes) {
private fun StringBuilder.appendPropertyModifiers(kmProperty: KmProperty) {
append(printPropertyModifiers(kmProperty))
}
fun printPropertyAccessorModifiers(accessorAttributes: KmPropertyAccessorAttributes) = buildString {
append(VISIBILITY_MAP[accessorAttributes.visibility])
append(MODALITY_MAP[accessorAttributes.modality])
appendFlags(
@@ -793,6 +809,10 @@ private fun StringBuilder.appendPropertyAccessorModifiers(accessorAttributes: Km
)
}
private fun StringBuilder.appendPropertyAccessorModifiers(accessorAttributes: KmPropertyAccessorAttributes) {
append(printPropertyAccessorModifiers(accessorAttributes))
}
private fun StringBuilder.appendValueParameterModifiers(valueParameter: KmValueParameter) = appendFlags(
valueParameter.isCrossinline to "crossinline",
valueParameter.isNoinline to "noinline"
@@ -10,12 +10,14 @@ import org.jetbrains.org.objectweb.asm.*
import java.io.File
import java.io.FileInputStream
internal fun File.readKotlinClassHeader(): Metadata? {
internal fun File.readKotlinClassHeader(): Metadata? = ClassReader(FileInputStream(this)).readKotlinClassHeader()
fun ClassReader.readKotlinClassHeader(): Metadata? {
var header: Metadata? = null
try {
val metadataDesc = Type.getDescriptor(Metadata::class.java)
ClassReader(FileInputStream(this)).accept(object : ClassVisitor(Opcodes.API_VERSION) {
accept(object : ClassVisitor(Opcodes.API_VERSION) {
override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor? =
if (desc == metadataDesc) readMetadataVisitor { header = it }
else null