diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiClassBuilderInterceptor.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiClassBuilderInterceptor.kt index 38494cd636f..011abc12164 100644 --- a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiClassBuilderInterceptor.kt +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiClassBuilderInterceptor.kt @@ -5,25 +5,17 @@ package org.jetbrains.kotlin.jvm.abi -import com.intellij.psi.PsiElement import org.jetbrains.kotlin.backend.jvm.extensions.ClassGenerator import org.jetbrains.kotlin.backend.jvm.extensions.ClassGeneratorExtension -import org.jetbrains.kotlin.codegen.ClassBuilder -import org.jetbrains.kotlin.codegen.ClassBuilderFactory -import org.jetbrains.kotlin.codegen.DelegatingClassBuilder -import org.jetbrains.kotlin.codegen.DelegatingClassBuilderFactory import org.jetbrains.kotlin.codegen.inline.coroutines.FOR_INLINE_SUFFIX import org.jetbrains.kotlin.codegen.`when`.WhenByEnumsMapping -import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.DescriptorVisibilities -import org.jetbrains.kotlin.descriptors.FunctionDescriptor -import org.jetbrains.kotlin.descriptors.MemberDescriptor -import org.jetbrains.kotlin.diagnostics.DiagnosticSink import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI +import org.jetbrains.kotlin.ir.util.primaryConstructor +import org.jetbrains.kotlin.load.java.JvmAbi import org.jetbrains.kotlin.load.java.JvmAnnotationNames -import org.jetbrains.kotlin.resolve.BindingContext -import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin import org.jetbrains.org.objectweb.asm.AnnotationVisitor import org.jetbrains.org.objectweb.asm.MethodVisitor import org.jetbrains.org.objectweb.asm.Opcodes @@ -64,7 +56,9 @@ sealed class AbiClassInfo { * be stripped. However, if `f` is not callable directly, we only generate a * single inline method `f` which should be kept. */ -class JvmAbiClassBuilderInterceptor : ClassGeneratorExtension { +class JvmAbiClassBuilderInterceptor( + private val removeDataClassCopyIfConstructorIsPrivate: Boolean, +) : ClassGeneratorExtension { val abiClassInfo: MutableMap = mutableMapOf() override fun generateClass(generator: ClassGenerator, declaration: IrClass?): ClassGenerator = @@ -75,6 +69,12 @@ class JvmAbiClassBuilderInterceptor : ClassGeneratorExtension { irClass: IrClass?, ) : ClassGenerator by delegate { private val isPrivateClass = irClass != null && DescriptorVisibilities.isPrivate(irClass.visibility) + private val isDataClass = irClass != null && irClass.isData + + @OptIn(UnsafeDuringIrConstructionAPI::class) + private val primaryConstructorIsNotInAbi = + irClass?.primaryConstructor?.visibility?.let(DescriptorVisibilities::isPrivate) == true + lateinit var internalName: String var localOrAnonymousClass = false var publicAbi = false @@ -126,6 +126,14 @@ class JvmAbiClassBuilderInterceptor : ClassGeneratorExtension { return delegate.newMethod(declaration, access, name, desc, signature, exceptions) } + if (isDataClass && removeDataClassCopyIfConstructorIsPrivate && + (name == "copy" || name == "copy${JvmAbi.DEFAULT_PARAMS_IMPL_SUFFIX}") + ) { + if (primaryConstructorIsNotInAbi) { + return delegate.newMethod(declaration, access, name, desc, signature, exceptions) + } + } + // Copy inline functions verbatim if (declaration?.isInline == true && !isPrivateClass) { methodInfos[Method(name, desc)] = AbiMethodInfo.KEEP diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiCommandLineProcessor.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiCommandLineProcessor.kt index 431f2f1bffd..feec26fac5a 100644 --- a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiCommandLineProcessor.kt +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiCommandLineProcessor.kt @@ -32,18 +32,32 @@ class JvmAbiCommandLineProcessor : CommandLineProcessor { "sites of inline functions.", false, ) + + val REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE_OPTION: CliOption = + CliOption( + "removeDataClassCopyIfConstructorIsPrivate", + "true/false", + "Remove the 'copy' function from ABI if the primary constructor of the data class is private. False by default. " + + "Note that if ABI jars are used for incremental compilation, removal of the 'copy' function might break usages " + + "outside of the compilation unit where it's declared.", + false, + ) } override val pluginId: String get() = COMPILER_PLUGIN_ID override val pluginOptions: Collection - get() = listOf(OUTPUT_PATH_OPTION, REMOVE_DEBUG_INFO_OPTION) + get() = listOf(OUTPUT_PATH_OPTION, REMOVE_DEBUG_INFO_OPTION, REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE_OPTION) override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) { when (option) { OUTPUT_PATH_OPTION -> configuration.put(JvmAbiConfigurationKeys.OUTPUT_PATH, value) REMOVE_DEBUG_INFO_OPTION -> configuration.put(JvmAbiConfigurationKeys.REMOVE_DEBUG_INFO, value == "true") + REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE_OPTION -> configuration.put( + JvmAbiConfigurationKeys.REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE, + value == "true" + ) else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}") } } diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiComponentRegistrar.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiComponentRegistrar.kt index 706a572c8c3..c5f36159a45 100644 --- a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiComponentRegistrar.kt +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiComponentRegistrar.kt @@ -19,11 +19,15 @@ class JvmAbiComponentRegistrar : CompilerPluginRegistrar() { val outputPath = configuration.getNotNull(JvmAbiConfigurationKeys.OUTPUT_PATH) val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) configuration.put(JVMConfigurationKeys.RETAIN_OUTPUT_IN_MEMORY, true) - val builderExtension = JvmAbiClassBuilderInterceptor() + val removeDataClassCopy = configuration.getBoolean(JvmAbiConfigurationKeys.REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE) + + val builderExtension = JvmAbiClassBuilderInterceptor(removeDataClassCopy) val outputExtension = JvmAbiOutputExtension( File(outputPath), builderExtension.abiClassInfo, messageCollector, configuration.getBoolean(JvmAbiConfigurationKeys.REMOVE_DEBUG_INFO), + removeDataClassCopy, ) + ClassGeneratorExtension.registerExtension(builderExtension) ClassFileFactoryFinalizerExtension.registerExtension(outputExtension) } diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiConfigurationKeys.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiConfigurationKeys.kt index 20e4c9b108b..21b3c437288 100644 --- a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiConfigurationKeys.kt +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiConfigurationKeys.kt @@ -12,4 +12,6 @@ object JvmAbiConfigurationKeys { CompilerConfigurationKey.create(JvmAbiCommandLineProcessor.OUTPUT_PATH_OPTION.description) val REMOVE_DEBUG_INFO: CompilerConfigurationKey = CompilerConfigurationKey.create(JvmAbiCommandLineProcessor.REMOVE_DEBUG_INFO_OPTION.description) + val REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE: CompilerConfigurationKey = + CompilerConfigurationKey.create(JvmAbiCommandLineProcessor.REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE_OPTION.description) } diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiMetadataProcessor.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiMetadataProcessor.kt index 1e31406f51c..909b15c3175 100644 --- a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiMetadataProcessor.kt +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiMetadataProcessor.kt @@ -15,7 +15,10 @@ import org.jetbrains.org.objectweb.asm.Opcodes * Wrap the visitor for a Kotlin Metadata annotation to strip out private and local * functions, properties, and type aliases as well as local delegated properties. */ -fun abiMetadataProcessor(annotationVisitor: AnnotationVisitor): AnnotationVisitor = +fun abiMetadataProcessor( + annotationVisitor: AnnotationVisitor, + removeDataClassCopyIfConstructorIsPrivate: Boolean, +): AnnotationVisitor = kotlinClassHeaderVisitor { header -> // kotlinx-metadata only supports writing Kotlin metadata of version >= 1.4, so we need to // update the metadata version if we encounter older metadata annotations. @@ -29,7 +32,7 @@ fun abiMetadataProcessor(annotationVisitor: AnnotationVisitor): AnnotationVisito KotlinClassMetadata.transform(header) { metadata -> when (metadata) { is KotlinClassMetadata.Class -> { - metadata.kmClass.removePrivateDeclarations() + metadata.kmClass.removePrivateDeclarations(removeDataClassCopyIfConstructorIsPrivate) } is KotlinClassMetadata.FileFacade -> { metadata.kmPackage.removePrivateDeclarations() @@ -146,21 +149,23 @@ private fun AnnotationVisitor.visitKotlinMetadata(header: Metadata) { visitEnd() } -private fun KmClass.removePrivateDeclarations() { +private fun KmClass.removePrivateDeclarations(removeCopyAlongWithConstructor: Boolean) { constructors.removeIf { it.visibility.isPrivate } - (this as KmDeclarationContainer).removePrivateDeclarations() + (this as KmDeclarationContainer).removePrivateDeclarations( + copyFunShouldBeDeleted(removeCopyAlongWithConstructor) + ) localDelegatedProperties.clear() // TODO: do not serialize private type aliases once KT-17229 is fixed. } private fun KmPackage.removePrivateDeclarations() { - (this as KmDeclarationContainer).removePrivateDeclarations() + (this as KmDeclarationContainer).removePrivateDeclarations(false) localDelegatedProperties.clear() // TODO: do not serialize private type aliases once KT-17229 is fixed. } -private fun KmDeclarationContainer.removePrivateDeclarations() { - functions.removeIf { it.visibility.isPrivate } +private fun KmDeclarationContainer.removePrivateDeclarations(copyFunShouldBeDeleted: Boolean) { + functions.removeIf { it.visibility.isPrivate || (copyFunShouldBeDeleted && it.name == "copy") } properties.removeIf { it.visibility.isPrivate } functions.sortWith(compareBy(KmFunction::name, { it.signature.toString() })) @@ -174,5 +179,8 @@ private fun KmDeclarationContainer.removePrivateDeclarations() { } } +private fun KmClass.copyFunShouldBeDeleted(removeDataClassCopy: Boolean): Boolean = + removeDataClassCopy && isData && constructors.none { !it.isSecondary } + private val Visibility.isPrivate: Boolean get() = this == Visibility.PRIVATE || this == Visibility.PRIVATE_TO_THIS || this == Visibility.LOCAL diff --git a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiOutputExtension.kt b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiOutputExtension.kt index 86d59567ee2..b3f0bd424c0 100644 --- a/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiOutputExtension.kt +++ b/plugins/jvm-abi-gen/src/org/jetbrains/kotlin/jvm/abi/JvmAbiOutputExtension.kt @@ -27,11 +27,12 @@ class JvmAbiOutputExtension( private val abiClassInfos: Map, private val messageCollector: MessageCollector, private val removeDebugInfo: Boolean, + private val removeDataClassCopyIfConstructorIsPrivate: Boolean, ) : ClassFileFactoryFinalizerExtension { override fun finalizeClassFactory(factory: ClassFileFactory) { // We need to wait until the end to produce any output in order to strip classes // from the InnerClasses attributes. - val outputFiles = AbiOutputFiles(abiClassInfos, factory, removeDebugInfo) + val outputFiles = AbiOutputFiles(abiClassInfos, factory, removeDebugInfo, removeDataClassCopyIfConstructorIsPrivate) if (outputPath.extension == "jar") { // We don't include the runtime or main class in interface jars and always reset time stamps. CompileEnvironmentUtil.writeToJar( @@ -54,6 +55,7 @@ class JvmAbiOutputExtension( val abiClassInfos: Map, val outputFiles: OutputFileCollection, val removeDebugInfo: Boolean, + val removeCopyAlongWithConstructor: Boolean, ) : OutputFileCollection { override fun get(relativePath: String): OutputFile? { error("AbiOutputFiles does not implement `get`.") @@ -153,7 +155,7 @@ class JvmAbiOutputExtension( if (descriptor != JvmAnnotationNames.METADATA_DESC) return delegate // Strip private declarations from the Kotlin Metadata annotation. - return abiMetadataProcessor(delegate) + return abiMetadataProcessor(delegate, removeCopyAlongWithConstructor) } override fun visitEnd() { 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 f8907a8e31a..682b68bcd16 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 @@ -79,6 +79,11 @@ abstract class BaseJvmAbiTest : TestCase() { abiOption(JvmAbiCommandLineProcessor.OUTPUT_PATH_OPTION.optionName, compilation.abiDir.canonicalPath), abiOption(JvmAbiCommandLineProcessor.REMOVE_DEBUG_INFO_OPTION.optionName, true.toString()).takeIf { InTextDirectivesUtils.findStringWithPrefixes(directives, "// REMOVE_DEBUG_INFO") != null + }, + abiOption( + JvmAbiCommandLineProcessor.REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE_OPTION.optionName, true.toString() + ).takeIf { + InTextDirectivesUtils.findStringWithPrefixes(directives, "// REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE") != null } ).toTypedArray() destination = compilation.destinationDir.canonicalPath 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 b386ceb1a90..b0b74f5c5b8 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 @@ -55,6 +55,16 @@ public class CompareJvmAbiTestGenerated extends AbstractCompareJvmAbiTest { runTest("plugins/jvm-abi-gen/testData/compare/constant/"); } + @TestMetadata("dataClassWithPrivateConstructor") + public void testDataClassWithPrivateConstructor() throws Exception { + runTest("plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/"); + } + + @TestMetadata("dataClassWithPrivateConstructorWithoutOption") + public void testDataClassWithPrivateConstructorWithoutOption() throws Exception { + runTest("plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructorWithoutOption/"); + } + @TestMetadata("debugInfoLineNumberTable") public void testDebugInfoLineNumberTable() throws Exception { runTest("plugins/jvm-abi-gen/testData/compare/debugInfoLineNumberTable/"); diff --git a/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/base/test.kt b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/base/test.kt new file mode 100644 index 00000000000..5c98ee6dc30 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/base/test.kt @@ -0,0 +1,6 @@ +package test + +data class Class private constructor( + val publicProperty: Any, + private val privateProperty: Any, +) diff --git a/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/directives.txt b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/directives.txt new file mode 100644 index 00000000000..473deb34d86 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/directives.txt @@ -0,0 +1 @@ +// REMOVE_DATA_CLASS_COPY_IF_CONSTRUCTOR_IS_PRIVATE diff --git a/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/sameAbi/test.kt b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/sameAbi/test.kt new file mode 100644 index 00000000000..4216f123767 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructor/sameAbi/test.kt @@ -0,0 +1,5 @@ +package test + +data class Class private constructor( + val publicProperty: Any, +) diff --git a/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructorWithoutOption/base/test.kt b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructorWithoutOption/base/test.kt new file mode 100644 index 00000000000..5c98ee6dc30 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructorWithoutOption/base/test.kt @@ -0,0 +1,6 @@ +package test + +data class Class private constructor( + val publicProperty: Any, + private val privateProperty: Any, +) diff --git a/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructorWithoutOption/differentAbi/test.kt b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructorWithoutOption/differentAbi/test.kt new file mode 100644 index 00000000000..4216f123767 --- /dev/null +++ b/plugins/jvm-abi-gen/testData/compare/dataClassWithPrivateConstructorWithoutOption/differentAbi/test.kt @@ -0,0 +1,5 @@ +package test + +data class Class private constructor( + val publicProperty: Any, +)