diff --git a/plugins/lombok/lombok.common/src/org/jetbrains/kotlin/lombok/utils/LombokNames.kt b/plugins/lombok/lombok.common/src/org/jetbrains/kotlin/lombok/utils/LombokNames.kt index 4d2a4c1a3af..c677019228b 100644 --- a/plugins/lombok/lombok.common/src/org/jetbrains/kotlin/lombok/utils/LombokNames.kt +++ b/plugins/lombok/lombok.common/src/org/jetbrains/kotlin/lombok/utils/LombokNames.kt @@ -19,6 +19,7 @@ object LombokNames { val NO_ARGS_CONSTRUCTOR = FqName("lombok.NoArgsConstructor") val ALL_ARGS_CONSTRUCTOR = FqName("lombok.AllArgsConstructor") val REQUIRED_ARGS_CONSTRUCTOR = FqName("lombok.RequiredArgsConstructor") + val BUILDER = FqName("lombok.Builder") val ACCESSORS_ID = ClassId.topLevel(ACCESSORS) diff --git a/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/LombokSyntheticJavaPartsProvider.kt b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/LombokSyntheticJavaPartsProvider.kt index a3cc6d097fd..72f690f783e 100644 --- a/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/LombokSyntheticJavaPartsProvider.kt +++ b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/LombokSyntheticJavaPartsProvider.kt @@ -10,6 +10,8 @@ import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor import org.jetbrains.kotlin.load.java.lazy.LazyJavaResolverContext +import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaClassDescriptor +import org.jetbrains.kotlin.load.java.lazy.descriptors.SyntheticJavaClassDescriptor import org.jetbrains.kotlin.lombok.config.LombokConfig import org.jetbrains.kotlin.lombok.processor.* import org.jetbrains.kotlin.lombok.utils.getJavaClass @@ -30,7 +32,8 @@ class LombokSyntheticJavaPartsProvider(config: LombokConfig) : SyntheticJavaPart WithProcessor(), NoArgsConstructorProcessor(), AllArgsConstructorProcessor(), - RequiredArgsConstructorProcessor() + RequiredArgsConstructorProcessor(), + BuilderProcessor(config) ) /** @@ -85,12 +88,12 @@ class LombokSyntheticJavaPartsProvider(config: LombokConfig) : SyntheticJavaPart } context(LazyJavaResolverContext) - private fun getSyntheticParts(descriptor: ClassDescriptor): SyntheticParts = - descriptor.getJavaClass()?.let { - partsCache.getOrPut(descriptor) { - computeSyntheticParts(descriptor) - } - } ?: SyntheticParts.Empty + private fun getSyntheticParts(descriptor: ClassDescriptor): SyntheticParts { + if (descriptor !is LazyJavaClassDescriptor && descriptor !is SyntheticJavaClassDescriptor) return SyntheticParts.Empty + return partsCache.getOrPut(descriptor) { + computeSyntheticParts(descriptor) + } + } context(LazyJavaResolverContext) private fun computeSyntheticParts(descriptor: ClassDescriptor): SyntheticParts { diff --git a/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/config/annotationConfig.kt b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/config/annotationConfig.kt index 810ed7efe35..61d55f0e4e2 100644 --- a/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/config/annotationConfig.kt +++ b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/config/annotationConfig.kt @@ -189,6 +189,36 @@ object LombokAnnotations { ) } } + + class Builder( + val builderClassName: String, + val buildMethodName: String, + val builderMethodName: String, + val requiresToBuilder: Boolean, + val visibility: AccessLevel, + val setterPrefix: String? + ) { + companion object : AnnotationAndConfigCompanion(LombokNames.BUILDER) { + private const val DEFAULT_BUILDER_CLASS_NAME = "*Builder" + private const val DEFAULT_BUILD_METHOD_NAME = "build" + private const val DEFAULT_BUILDER_METHOD_NAME = "builder" + private const val DEFAULT_REQUIRES_TO_BUILDER = false + + + override fun extract(annotation: AnnotationDescriptor?, config: LombokConfig): Builder { + return Builder( + builderClassName = annotation?.getStringArgument("builderClassName") + ?: config.getString("lombok.builder.className") + ?: DEFAULT_BUILDER_CLASS_NAME, + buildMethodName = annotation?.getStringArgument("buildMethodName") ?: DEFAULT_BUILD_METHOD_NAME, + builderMethodName = annotation?.getStringArgument("builderMethodName") ?: DEFAULT_BUILDER_METHOD_NAME, + requiresToBuilder = annotation?.getBooleanArgument("toBuilder") ?: DEFAULT_REQUIRES_TO_BUILDER, + visibility = annotation?.getAccessLevel("access") ?: AccessLevel.PUBLIC, + setterPrefix = annotation?.getStringArgument("setterPrefix") + ) + } + } + } } diff --git a/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/processor/BuilderProcessor.kt b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/processor/BuilderProcessor.kt new file mode 100644 index 00000000000..851dd484b5f --- /dev/null +++ b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/processor/BuilderProcessor.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2022 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.lombok.processor + +import org.jetbrains.kotlin.descriptors.* +import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.load.java.JavaDescriptorVisibilities +import org.jetbrains.kotlin.load.java.lazy.LazyJavaResolverContext +import org.jetbrains.kotlin.load.java.lazy.descriptors.SyntheticJavaClassDescriptor +import org.jetbrains.kotlin.lombok.config.LombokAnnotations.Builder +import org.jetbrains.kotlin.lombok.config.LombokConfig +import org.jetbrains.kotlin.lombok.config.toDescriptorVisibility +import org.jetbrains.kotlin.lombok.utils.* +import org.jetbrains.kotlin.name.Name + +class BuilderProcessor(private val config: LombokConfig) : Processor { + companion object { + private const val BUILDER_DATA = "Lombok.BuilderData" + private const val TO_BUILDER = "toBuilder" + } + + context(LazyJavaResolverContext) + @Suppress("IncorrectFormatting") // KTIJ-22227 + override fun contribute(classDescriptor: ClassDescriptor, partsBuilder: SyntheticPartsBuilder) { + if (classDescriptor is SyntheticJavaClassDescriptor) { + val builderData = classDescriptor.attributes[BUILDER_DATA] as? BuilderData ?: return + contributeToBuilderClass(classDescriptor, builderData.constructingClass, builderData.builder, partsBuilder) + } else { + val builder = Builder.getIfAnnotated(classDescriptor, config) ?: return + contributeToAnnotatedClass(classDescriptor, builder, partsBuilder) + } + } + + context(LazyJavaResolverContext) + @Suppress("IncorrectFormatting") // KTIJ-22227 + private fun contributeToAnnotatedClass(classDescriptor: ClassDescriptor, builder: Builder, partsBuilder: SyntheticPartsBuilder) { + val builderName = Name.identifier(builder.builderClassName.replace("*", classDescriptor.name.asString())) + val visibility = builder.visibility.toDescriptorVisibility() + val builderDescriptor = SyntheticJavaClassDescriptor( + outerContext = this@LazyJavaResolverContext, + name = builderName, + outerClass = classDescriptor, + classKind = ClassKind.CLASS, + modality = Modality.FINAL, + visibility = visibility, + isInner = false, + isRecord = false, + annotations = Annotations.EMPTY, + declaredTypeParameters = emptyList(), + sealedSubclasses = emptyList(), + supertypes = listOf(components.module.builtIns.anyType), + attributes = mapOf(BUILDER_DATA to BuilderData(builder, classDescriptor)) + ) + partsBuilder.addClass(builderDescriptor) + val builderFunction = classDescriptor.createFunction( + Name.identifier(builder.builderMethodName), + valueParameters = emptyList(), + returnType = builderDescriptor.defaultType, + modality = Modality.FINAL, + visibility = visibility, + receiver = null + ) + partsBuilder.addStaticFunction(builderFunction) + + if (builder.requiresToBuilder) { + val toBuilderFunction = classDescriptor.createFunction( + Name.identifier(TO_BUILDER), + valueParameters = emptyList(), + returnType = builderDescriptor.defaultType, + modality = Modality.FINAL, + visibility = visibility + ) + partsBuilder.addMethod(toBuilderFunction) + } + } + + private fun contributeToBuilderClass( + builderClass: ClassDescriptor, + constructingClass: ClassDescriptor, + builder: Builder, + partsBuilder: SyntheticPartsBuilder + ) { + val constructor = builderClass.createJavaConstructor(valueParameters = emptyList(), JavaDescriptorVisibilities.PACKAGE_VISIBILITY) + partsBuilder.addConstructor(constructor) + + val visibility = builder.visibility.toDescriptorVisibility() + + val buildFunction = builderClass.createFunction( + Name.identifier(builder.buildMethodName), + valueParameters = emptyList(), + returnType = constructingClass.defaultType, + visibility = visibility + ) + partsBuilder.addMethod(buildFunction) + + for (field in constructingClass.getJavaFields()) { + createSetterMethod(builder, field, builderClass, partsBuilder) + } + } + + private fun createSetterMethod( + builder: Builder, + field: PropertyDescriptor, + builderClass: ClassDescriptor, + partsBuilder: SyntheticPartsBuilder + ) { + val prefix = builder.setterPrefix + val fieldName = field.name + val setterName = if (prefix.isNullOrBlank()) fieldName else Name.identifier("${prefix}${field.name.asString().capitalize()}") + val setFunction = builderClass.createFunction( + name = setterName, + valueParameters = listOf(LombokValueParameter(fieldName, field.type)), + returnType = builderClass.defaultType, + modality = Modality.FINAL, + visibility = builder.visibility.toDescriptorVisibility() + ) + partsBuilder.addMethod(setFunction) + } + + private class BuilderData(val builder: Builder, val constructingClass: ClassDescriptor) +} diff --git a/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/utils/annotationUtils.kt b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/utils/annotationUtils.kt index 473b919db8e..2ea377b1697 100644 --- a/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/utils/annotationUtils.kt +++ b/plugins/lombok/lombok.k1/src/org/jetbrains/kotlin/lombok/utils/annotationUtils.kt @@ -20,6 +20,11 @@ fun getAccessLevel(annotation: AnnotationDescriptor, field: String = "value"): A return AccessLevel.valueOf(value) } +@JvmName("getAccessLevelWithReceiver") +fun AnnotationDescriptor.getAccessLevel(field: String = "value"): AccessLevel { + return getAccessLevel(this, field) +} + fun AnnotationDescriptor.getStringArgument(argumentName: String): String? { val argument = allValueArguments[Name.identifier(argumentName)] ?: return null diff --git a/plugins/lombok/testData/box/builder.kt b/plugins/lombok/testData/box/builder.kt new file mode 100644 index 00000000000..11143a0481f --- /dev/null +++ b/plugins/lombok/testData/box/builder.kt @@ -0,0 +1,29 @@ +// FILE: User.java + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class User { + @Builder.Default private int created = 0; + private String name; + private int age; +} + + +// FILE: test.kt + +fun box(): String { + val userBuilder = User.builder() + .created(10) + .name("John") + .age(42) + + val user = userBuilder.build() + return if (user.created == 10 && user.name == "John" && user.age == 42) { + "OK" + } else { + "Error: $user" + } +} diff --git a/plugins/lombok/testData/diagnostics/builderAnnotationArguments.kt b/plugins/lombok/testData/diagnostics/builderAnnotationArguments.kt new file mode 100644 index 00000000000..557a6f79e30 --- /dev/null +++ b/plugins/lombok/testData/diagnostics/builderAnnotationArguments.kt @@ -0,0 +1,36 @@ +// FIR_IDENTICAL +// FILE: User.java + +import lombok.Builder; +import lombok.Data; + +@Builder( + builderClassName = "SpecialUserBuilder", + buildMethodName = "execute", + builderMethodName = "createBuilder", + toBuilder = true, + access = AccessLevel.PACKAGE, + setterPrefix = "set" +) +@Data +public class User { + @Builder.Default private int created = 0; + private String name; + private int age; +} + + +// FILE: test.kt + +fun test() { + val userBuilder: User.SpecialUserBuilder = User.createBuilder() + val user = userBuilder + .setCreated(10) + .setName("John") + .setAge(42) + .execute() +} + +fun testToBuilder(user: User) { + val userBuilder: User.SpecialUserBuilder = user.toBuilder() +} diff --git a/plugins/lombok/testData/diagnostics/builderAnnotationArguments.txt b/plugins/lombok/testData/diagnostics/builderAnnotationArguments.txt new file mode 100644 index 00000000000..4c143da0f48 --- /dev/null +++ b/plugins/lombok/testData/diagnostics/builderAnnotationArguments.txt @@ -0,0 +1,25 @@ +package + +public fun assertEquals(/*0*/ a: T, /*1*/ b: T): kotlin.Unit +public fun test(): kotlin.Unit +public fun testToBuilder(/*0*/ user: User): kotlin.Unit + +@lombok.Builder(access = AccessLevel.PACKAGE, buildMethodName = "execute", builderClassName = "SpecialUserBuilder", builderMethodName = "createBuilder", setterPrefix = "set", toBuilder = true) @lombok.Data public open class User { + public constructor User() + private final var age: kotlin.Int + @lombok.Builder.Default private final var created: kotlin.Int + private final var name: kotlin.String! + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open /*synthesized*/ fun getAge(): kotlin.Int + public open /*synthesized*/ fun getCreated(): kotlin.Int + public open /*synthesized*/ fun getName(): kotlin.String! + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open /*synthesized*/ fun setAge(/*0*/ age: kotlin.Int): kotlin.Unit + public open /*synthesized*/ fun setCreated(/*0*/ created: kotlin.Int): kotlin.Unit + public open /*synthesized*/ fun setName(/*0*/ name: kotlin.String!): kotlin.Unit + public/*package*/ final /*synthesized*/ fun toBuilder(): User.SpecialUserBuilder + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public/*package*/ final /*synthesized*/ fun createBuilder(): User.SpecialUserBuilder +} diff --git a/plugins/lombok/testData/diagnostics/builderConfig.kt b/plugins/lombok/testData/diagnostics/builderConfig.kt new file mode 100644 index 00000000000..ef7b094652d --- /dev/null +++ b/plugins/lombok/testData/diagnostics/builderConfig.kt @@ -0,0 +1,19 @@ +// FILE: User.java + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class User { + private String name; +} + + +// FILE: test.kt +fun test() { + val userBuilder: User.SpecialUserBuilder = User.builder() +} + +// FILE: lombok.config +lombok.builder.className=Special*Builder diff --git a/plugins/lombok/testData/diagnostics/builderConfig.txt b/plugins/lombok/testData/diagnostics/builderConfig.txt new file mode 100644 index 00000000000..03cd9a20ee2 --- /dev/null +++ b/plugins/lombok/testData/diagnostics/builderConfig.txt @@ -0,0 +1,17 @@ +package + +public fun assertEquals(/*0*/ a: T, /*1*/ b: T): kotlin.Unit +public fun test(): kotlin.Unit + +@lombok.Builder @lombok.Data public open class User { + public constructor User() + private final var name: kotlin.String! + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open /*synthesized*/ fun getName(): kotlin.String! + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open /*synthesized*/ fun setName(/*0*/ name: kotlin.String!): kotlin.Unit + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun builder(): User.SpecialUserBuilder +} diff --git a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/BlackBoxCodegenTestForLombokGenerated.java b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/BlackBoxCodegenTestForLombokGenerated.java index ab08d1889f7..9ff2411feec 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/BlackBoxCodegenTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/BlackBoxCodegenTestForLombokGenerated.java @@ -49,6 +49,12 @@ public class BlackBoxCodegenTestForLombokGenerated extends AbstractBlackBoxCodeg KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/lombok/testData/box"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM, true); } + @Test + @TestMetadata("builder.kt") + public void testBuilder() throws Exception { + runTest("plugins/lombok/testData/box/builder.kt"); + } + @Test @TestMetadata("configAccessors.kt") public void testConfigAccessors() throws Exception { diff --git a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/DiagnosticTestForLombokGenerated.java b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/DiagnosticTestForLombokGenerated.java index 3d26b4e3620..a41348ee566 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/DiagnosticTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/DiagnosticTestForLombokGenerated.java @@ -36,6 +36,18 @@ public class DiagnosticTestForLombokGenerated extends AbstractDiagnosticTestForL runTest("plugins/lombok/testData/diagnostics/annotationTypes.kt"); } + @Test + @TestMetadata("builderAnnotationArguments.kt") + public void testBuilderAnnotationArguments() throws Exception { + runTest("plugins/lombok/testData/diagnostics/builderAnnotationArguments.kt"); + } + + @Test + @TestMetadata("builderConfig.kt") + public void testBuilderConfig() throws Exception { + runTest("plugins/lombok/testData/diagnostics/builderConfig.kt"); + } + @Test @TestMetadata("clashAccessors.kt") public void testClashAccessors() throws Exception { diff --git a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirBlackBoxCodegenTestForLombokGenerated.java b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirBlackBoxCodegenTestForLombokGenerated.java index 4c2ace8af0d..633732841ed 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirBlackBoxCodegenTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirBlackBoxCodegenTestForLombokGenerated.java @@ -49,6 +49,12 @@ public class FirBlackBoxCodegenTestForLombokGenerated extends AbstractFirBlackBo KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/lombok/testData/box"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true); } + @Test + @TestMetadata("builder.kt") + public void testBuilder() throws Exception { + runTest("plugins/lombok/testData/box/builder.kt"); + } + @Test @TestMetadata("configAccessors.kt") public void testConfigAccessors() throws Exception { diff --git a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirDiagnosticTestForLombokGenerated.java b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirDiagnosticTestForLombokGenerated.java index 5d1b34e9203..086f992ed6a 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirDiagnosticTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirDiagnosticTestForLombokGenerated.java @@ -36,6 +36,18 @@ public class FirDiagnosticTestForLombokGenerated extends AbstractFirDiagnosticTe runTest("plugins/lombok/testData/diagnostics/annotationTypes.kt"); } + @Test + @TestMetadata("builderAnnotationArguments.kt") + public void testBuilderAnnotationArguments() throws Exception { + runTest("plugins/lombok/testData/diagnostics/builderAnnotationArguments.kt"); + } + + @Test + @TestMetadata("builderConfig.kt") + public void testBuilderConfig() throws Exception { + runTest("plugins/lombok/testData/diagnostics/builderConfig.kt"); + } + @Test @TestMetadata("clashAccessors.kt") public void testClashAccessors() throws Exception { diff --git a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/IrBlackBoxCodegenTestForLombokGenerated.java b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/IrBlackBoxCodegenTestForLombokGenerated.java index 3266de618ed..6e57d9bd9de 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/IrBlackBoxCodegenTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/IrBlackBoxCodegenTestForLombokGenerated.java @@ -49,6 +49,12 @@ public class IrBlackBoxCodegenTestForLombokGenerated extends AbstractIrBlackBoxC KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/lombok/testData/box"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true); } + @Test + @TestMetadata("builder.kt") + public void testBuilder() throws Exception { + runTest("plugins/lombok/testData/box/builder.kt"); + } + @Test @TestMetadata("configAccessors.kt") public void testConfigAccessors() throws Exception {