diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/serialization/JvmSerializerExtension.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/serialization/JvmSerializerExtension.kt index 80805a03c81..ee3eb1ec306 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/serialization/JvmSerializerExtension.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/serialization/JvmSerializerExtension.kt @@ -26,6 +26,7 @@ import org.jetbrains.kotlin.protobuf.GeneratedMessageLite import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.DescriptorUtils.isInterface import org.jetbrains.kotlin.resolve.descriptorUtil.classId +import org.jetbrains.kotlin.resolve.descriptorUtil.isEffectivelyPrivateApi import org.jetbrains.kotlin.resolve.descriptorUtil.nonSourceAnnotations import org.jetbrains.kotlin.resolve.jvm.annotations.hasJvmDefaultAnnotation import org.jetbrains.kotlin.serialization.DescriptorSerializer @@ -59,6 +60,10 @@ class JvmSerializerExtension(private val bindings: JvmSerializationBindings, sta return classBuilderMode != ClassBuilderMode.ABI || descriptor.visibility != Visibilities.PRIVATE } + override fun shouldSerializeNestedClass(descriptor: ClassDescriptor): Boolean { + return classBuilderMode != ClassBuilderMode.ABI || !descriptor.isEffectivelyPrivateApi + } + override fun serializeClass( descriptor: ClassDescriptor, proto: ProtoBuf.Class.Builder, diff --git a/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt b/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt index 874bbbd8591..e3a3501a817 100644 --- a/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt +++ b/compiler/serialization/src/org/jetbrains/kotlin/serialization/DescriptorSerializer.kt @@ -119,6 +119,10 @@ class DescriptorSerializer private constructor( typeAliasProto(descriptor)?.let { builder.addTypeAlias(it) } } else { + if (descriptor is ClassDescriptor && !extension.shouldSerializeNestedClass(descriptor)) { + continue + } + val name = getSimpleNameIndex(descriptor.name) if (isEnumEntry(descriptor)) { builder.addEnumEntry(enumEntryProto(descriptor as ClassDescriptor)) diff --git a/compiler/serialization/src/org/jetbrains/kotlin/serialization/SerializerExtension.kt b/compiler/serialization/src/org/jetbrains/kotlin/serialization/SerializerExtension.kt index 35b7dc99111..31bec49f0dc 100644 --- a/compiler/serialization/src/org/jetbrains/kotlin/serialization/SerializerExtension.kt +++ b/compiler/serialization/src/org/jetbrains/kotlin/serialization/SerializerExtension.kt @@ -25,6 +25,7 @@ abstract class SerializerExtension { open fun shouldSerializeFunction(descriptor: FunctionDescriptor): Boolean = true open fun shouldSerializeProperty(descriptor: PropertyDescriptor): Boolean = true open fun shouldSerializeTypeAlias(descriptor: TypeAliasDescriptor): Boolean = true + open fun shouldSerializeNestedClass(descriptor: ClassDescriptor): Boolean = true interface ClassMembersProducer { fun getCallableMembers(classDescriptor: ClassDescriptor): Collection diff --git a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index fa4675c56db..b76c96bbc01 100755 --- a/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/tests/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -154,6 +154,7 @@ import org.jetbrains.kotlin.kapt.cli.test.AbstractArgumentParsingTest import org.jetbrains.kotlin.kapt.cli.test.AbstractKaptToolIntegrationTest import org.jetbrains.kotlin.jvm.abi.AbstractCompareJvmAbiTest import org.jetbrains.kotlin.jvm.abi.AbstractCompileAgainstJvmAbiTest +import org.jetbrains.kotlin.jvm.abi.AbstractJvmAbiContentTest import org.jetbrains.kotlin.kapt3.test.AbstractClassFileToSourceStubConverterTest import org.jetbrains.kotlin.kapt3.test.AbstractKotlinKaptContextTest import org.jetbrains.kotlin.noarg.AbstractBlackBoxCodegenTestForNoArg @@ -1076,6 +1077,10 @@ fun main(args: Array) { model("compare", recursive = false, extension = null) } + testClass { + model("content", recursive = false, extension = null) + } + testClass { model("compile", recursive = false, extension = null) } diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiAnalysisHandlerExtension.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiAnalysisHandlerExtension.kt index 6b31624f5a7..9c2d3781a95 100644 --- a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiAnalysisHandlerExtension.kt +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiAnalysisHandlerExtension.kt @@ -19,6 +19,7 @@ import org.jetbrains.kotlin.incremental.LocalFileKotlinClass import org.jetbrains.kotlin.incremental.isClassFile import org.jetbrains.kotlin.jvm.abi.asm.AbiClassBuilder import org.jetbrains.kotlin.jvm.abi.asm.FilterInnerClassesVisitor +import org.jetbrains.kotlin.jvm.abi.asm.InnerClassesCollectingVisitor import org.jetbrains.kotlin.load.kotlin.FileBasedKotlinClass import org.jetbrains.kotlin.load.kotlin.header.KotlinClassHeader import org.jetbrains.kotlin.metadata.ProtoBuf @@ -33,6 +34,7 @@ import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension import org.jetbrains.org.objectweb.asm.* import java.io.File +import java.util.* class JvmAbiAnalysisHandlerExtension( private val compilerConfiguration: CompilerConfiguration @@ -78,8 +80,22 @@ class JvmAbiAnalysisHandlerExtension( * Removes private or local classes from outputs */ private fun removeUnneededClasses(outputs: Iterable) { - val removedClasses = HashSet() + // maps internal names of classes: class -> inner classes + val innerClasses = HashMap>() + val internalNameToFile = HashMap() + for (output in outputs) { + if (!output.file.isClassFile()) continue + + val visitor = InnerClassesCollectingVisitor() + output.accept(visitor) + val outputInternalName = visitor.ownInternalName!! + internalNameToFile[outputInternalName] = output.file + innerClasses[outputInternalName] = visitor.innerClasses + } + + // internal names of removed files + val classesToRemoveQueue = ArrayDeque() for (output in outputs) { if (!output.file.isClassFile()) continue @@ -96,17 +112,33 @@ class JvmAbiAnalysisHandlerExtension( } if (!isNeededForAbi) { - output.delete() val jvmClassName = JvmClassName.byClassId(classData.classId) - removedClasses.add(jvmClassName.internalName) + classesToRemoveQueue.add(jvmClassName.internalName) } } + // we can remove inner classes of removed classes + val classesToRemove = HashSet() + classesToRemove.addAll(classesToRemoveQueue) + while (classesToRemoveQueue.isNotEmpty()) { + val classToRemove = classesToRemoveQueue.removeFirst() + innerClasses[classToRemove]?.forEach { + if (classesToRemove.add(it)) { + classesToRemoveQueue.add(it) + } + } + } + + val classFilesToRemove = classesToRemove.mapTo(HashSet()) { internalNameToFile[it] } for (output in outputs) { if (!output.file.isClassFile()) continue - output.transform { writer -> - FilterInnerClassesVisitor(removedClasses, Opcodes.ASM6, writer) + if (output.file in classFilesToRemove) { + output.delete() + } else { + output.transform { writer -> + FilterInnerClassesVisitor(classesToRemove, Opcodes.ASM6, writer) + } } } } @@ -164,6 +196,12 @@ class JvmAbiAnalysisHandlerExtension( this.bytes = cw.toByteArray() } + fun accept(visitor: ClassVisitor) { + val bytes = bytes ?: return + val cr = ClassReader(bytes) + cr.accept(visitor, 0) + } + fun flush() { bytes?.let { FileUtil.writeToFile(file, it) } } diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/asm/InnerClassesCollectingVisitor.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/asm/InnerClassesCollectingVisitor.kt new file mode 100644 index 00000000000..a790cca8dcf --- /dev/null +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/asm/InnerClassesCollectingVisitor.kt @@ -0,0 +1,30 @@ +package org.jetbrains.kotlin.jvm.abi.asm + +import org.jetbrains.org.objectweb.asm.ClassVisitor +import org.jetbrains.org.objectweb.asm.Opcodes + +internal class InnerClassesCollectingVisitor : ClassVisitor(Opcodes.ASM6) { + var ownInternalName: String? = null + private set + + private val myInnerClasses = arrayListOf() + val innerClasses: List + get() = myInnerClasses + + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array? + ) { + super.visit(version, access, name, signature, superName, interfaces) + ownInternalName = name + } + + override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) { + super.visitInnerClass(name, outerName, innerName, access) + myInnerClasses.add(name) + } +} diff --git a/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/AbstractJvmAbiContentTest.kt b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/AbstractJvmAbiContentTest.kt new file mode 100644 index 00000000000..d1bb30860f6 --- /dev/null +++ b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/AbstractJvmAbiContentTest.kt @@ -0,0 +1,94 @@ +package org.jetbrains.kotlin.jvm.abi + +import com.intellij.testFramework.UsefulTestCase +import org.jetbrains.kotlin.incremental.isClassFile +import org.jetbrains.kotlin.utils.Printer +import org.jetbrains.org.objectweb.asm.* +import java.io.File + +abstract class AbstractJvmAbiContentTest : BaseJvmAbiTest() { + fun doTest(path: String) { + val testDir = File(path) + val compilation = Compilation(testDir, name = null).also { make(it) } + + val classToBytecode = hashMapOf() + val baseDir = compilation.abiDir + val classFiles = baseDir.walk().filter { it.isFile && it.isClassFile() } + for (classFile in classFiles) { + val bytes = classFile.readBytes() + val reader = ClassReader(bytes) + val sb = StringBuilder() + val p = Printer(sb) + val visitor = PrintingClassVisitor(p) + reader.accept(visitor, 0) + classToBytecode[classFile] = sb.toString() + } + + val actual = classToBytecode.entries + .sortedBy { it.key.relativeTo(baseDir) } + .joinToString("\n") { it.value } + val signaturesFile = testDir.resolve("signatures.txt") + if (!signaturesFile.exists()) { + signaturesFile.writeText("") + } + UsefulTestCase.assertSameLinesWithFile(signaturesFile.canonicalPath, actual) + } +} + +private class PrintingClassVisitor(private val p: Printer) : ClassVisitor(Opcodes.ASM6) { + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array? + ) { + val superTypes = linkedSetOf() + superName?.let { superTypes.add(it) } + interfaces?.forEach { superTypes.add(it) } + val supertypeString = + if (superTypes.isEmpty()) "" + else " : ${superTypes.joinToString()}" + val classOrInterface = if (access and Opcodes.ACC_INTERFACE == Opcodes.ACC_INTERFACE) "interface" else "class" + + p.println("$classOrInterface $name$supertypeString {") + p.pushIndent() + + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitField(access: Int, name: String, desc: String, signature: String?, value: Any?): FieldVisitor? { + val flags = memberFlagsString(access) + p.println("$flags val $name $desc = $value") + + return super.visitField(access, name, desc, signature, value) + } + + override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array?): MethodVisitor? { + val flags = memberFlagsString(access) + p.println("$flags fun $name $desc") + + return super.visitMethod(access, name, desc, signature, exceptions) + } + + override fun visitEnd() { + p.popIndent() + p.println("}") + + super.visitEnd() + } + + private fun memberFlagsString(flags: Int): String = + listOfNotNull( + flagToString(flags, Opcodes.ACC_PUBLIC, "public"), + flagToString(flags, Opcodes.ACC_PROTECTED, "protected"), + flagToString(flags, Opcodes.ACC_PRIVATE, "private"), + flagToString(flags, Opcodes.ACC_STATIC, "static"), + flagToString(flags, Opcodes.ACC_FINAL, "final"), + flagToString(flags, Opcodes.ACC_ABSTRACT, "abstract") + ).joinToString(" ") + + private fun flagToString(flags: Int, flag: Int, flagName: String): String? = + flagName.takeIf { flags and flag == flag } +} \ No newline at end of file diff --git a/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/BaseJvmAbiTest.kt b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/BaseJvmAbiTest.kt index 20b01627d5b..8b1c240202f 100644 --- a/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/BaseJvmAbiTest.kt +++ b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/BaseJvmAbiTest.kt @@ -32,17 +32,17 @@ abstract class BaseJvmAbiTest : TestCase() { inner class Compilation( val projectDir: File, - val name: String, + val name: String?, val dependencies: Collection = emptyList() ) { val srcDir: File - get() = projectDir.resolve(name) + get() = if (name == null) projectDir else projectDir.resolve(name) val destinationDir: File - get() = workingDir.resolve("$name/out") + get() = if (name == null) workingDir.resolve("out") else workingDir.resolve("$name/out") val abiDir: File - get() = workingDir.resolve("$name/abi") + get() = if (name == null) workingDir.resolve("abi") else workingDir.resolve("$name/abi") override fun toString(): String = "compilation '$name'" diff --git a/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/CompareJvmAbiTestGenerated.java b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/CompareJvmAbiTestGenerated.java index aca8c3b796e..2b9a235040d 100644 --- a/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/CompareJvmAbiTestGenerated.java +++ b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/CompareJvmAbiTestGenerated.java @@ -64,6 +64,11 @@ public class CompareJvmAbiTestGenerated extends AbstractCompareJvmAbiTest { runTest("plugins/jvm-abi-gen/testData/compare/inlineFunctionBody/"); } + @TestMetadata("innerClasses") + public void testInnerClasses() throws Exception { + runTest("plugins/jvm-abi-gen/testData/compare/innerClasses/"); + } + @TestMetadata("lambda") public void testLambda() throws Exception { runTest("plugins/jvm-abi-gen/testData/compare/lambda/"); diff --git a/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/JvmAbiContentTestGenerated.java b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/JvmAbiContentTestGenerated.java new file mode 100644 index 00000000000..86c88f037fe --- /dev/null +++ b/plugins/jvm-abi-gen/test/org/jetbrains/kotlin/jvm/abi/JvmAbiContentTestGenerated.java @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. 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.jvm.abi; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("plugins/jvm-abi-gen/testData/content") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class JvmAbiContentTestGenerated extends AbstractJvmAbiContentTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath); + } + + public void testAllFilesPresentInContent() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("plugins/jvm-abi-gen/testData/content"), Pattern.compile("^([^\\.]+)$"), TargetBackend.ANY, false); + } + + @TestMetadata("class") + public void testClass() throws Exception { + runTest("plugins/jvm-abi-gen/testData/content/class/"); + } +} diff --git a/plugins/jvm-abi-gen/testData/compare/innerClasses/base/classes.kt b/plugins/jvm-abi-gen/testData/compare/innerClasses/base/classes.kt new file mode 100644 index 00000000000..68d258399a5 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/compare/innerClasses/base/classes.kt @@ -0,0 +1,31 @@ +package test + +class A { + private inner class AInner { + inner class AInnerInner {} + + fun aInnerMethod() { + class AInnerMethodLocal + + val aInnerMethodLambda: (Int) -> Int = { it * it } + } + } + + private class ANested { + inner class ANestedInner {} + class ANestedNested + object ANestedObject + + fun aNestedMethod() { + class ANestedMethodLocal + + val aNestedMethodLambda: (Int) -> Int = { it * it } + } + } +} + +private class B { + inner class BInnerClass + class BNestedClass + object BObject {} +} \ No newline at end of file diff --git a/plugins/jvm-abi-gen/testData/compare/innerClasses/sameAbi/classes.kt b/plugins/jvm-abi-gen/testData/compare/innerClasses/sameAbi/classes.kt new file mode 100644 index 00000000000..926842376c9 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/compare/innerClasses/sameAbi/classes.kt @@ -0,0 +1,3 @@ +package test + +class A \ No newline at end of file diff --git a/plugins/jvm-abi-gen/testData/content/class/classes.kt b/plugins/jvm-abi-gen/testData/content/class/classes.kt new file mode 100644 index 00000000000..cce6bbabf18 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/content/class/classes.kt @@ -0,0 +1,39 @@ +package test + +interface Interface { +} + +open class BaseClass { + open val baseClassPublicVal: Int = 0 + open fun baseClassPublicFun(): Int = 1 + + internal val baseClassInternalVal: Int = 2 + internal fun baseClassInternalFun(): Int = 3 + + protected val baseClassProtectedVal: Int = 4 + protected fun baseClassProtectedFun(): Int = 5 + + private val baseClassPrivateVal: Int = 6 + private fun baseClassPrivateFun(): Int = 7 + + companion object { + const val basePublicConst: Int = 8 + private const val basePrivateConst: Int = 9 + } +} + +class Class : BaseClass(), Interface { + fun classPublicMethod() { + class publicMethodLocalClass { + val x = 0 + } + + val publicMethodLambda: (Int) -> Int = { it * it } + } + + private class NestedInnerClass() { + class NestedNestedInnerClass() {} + } +} + +private class PrivateClass \ No newline at end of file diff --git a/plugins/jvm-abi-gen/testData/content/class/signatures.txt b/plugins/jvm-abi-gen/testData/content/class/signatures.txt new file mode 100644 index 00000000000..189973c215b --- /dev/null +++ b/plugins/jvm-abi-gen/testData/content/class/signatures.txt @@ -0,0 +1,23 @@ +class test/BaseClass$Companion : java/lang/Object { + public fun (Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +class test/BaseClass : java/lang/Object { + public static final val basePublicConst I = 8 + public static final val Companion Ltest/BaseClass$Companion; = null + public fun getBaseClassPublicVal ()I + public fun baseClassPublicFun ()I + public final fun getBaseClassInternalVal$main ()I + public final fun baseClassInternalFun$main ()I + protected final fun getBaseClassProtectedVal ()I + protected final fun baseClassProtectedFun ()I + public fun ()V +} + +class test/Class : test/BaseClass, test/Interface { + public final fun classPublicMethod ()V + public fun ()V +} + +interface test/Interface : java/lang/Object { +} \ No newline at end of file