[Lombok K1] Support @Builder annotation

^KT-46959
This commit is contained in:
Dmitriy Novozhilov
2022-08-05 14:17:20 +03:00
parent 6fde784b55
commit b311f0b862
15 changed files with 338 additions and 7 deletions
@@ -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)
@@ -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 {
@@ -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<Builder>(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")
)
}
}
}
}
@@ -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)
}
@@ -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
+29
View File
@@ -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"
}
}
@@ -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()
}
@@ -0,0 +1,25 @@
package
public fun </*0*/ T> 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
}
+19
View File
@@ -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
+17
View File
@@ -0,0 +1,17 @@
package
public fun </*0*/ T> 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
}
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {