diff --git a/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/typeEnhancement/typeEnhancement.kt b/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/typeEnhancement/typeEnhancement.kt index f79a4a4f92a..a68c7eca4b1 100644 --- a/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/typeEnhancement/typeEnhancement.kt +++ b/core/descriptors.jvm/src/org/jetbrains/kotlin/load/java/typeEnhancement/typeEnhancement.kt @@ -199,7 +199,7 @@ private fun getEnhancedNullability(qualifiers: JavaTypeQualifiers, position: Typ } } -private val ENHANCED_NULLABILITY_ANNOTATIONS = EnhancedTypeAnnotations(JvmAnnotationNames.ENHANCED_NULLABILITY_ANNOTATION) +val ENHANCED_NULLABILITY_ANNOTATIONS: Annotations = EnhancedTypeAnnotations(JvmAnnotationNames.ENHANCED_NULLABILITY_ANNOTATION) private val ENHANCED_MUTABILITY_ANNOTATIONS = EnhancedTypeAnnotations(JvmAnnotationNames.ENHANCED_MUTABILITY_ANNOTATION) private class EnhancedTypeAnnotations(private val fqNameToMatch: FqName) : Annotations { 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 c677019228b..77a770638cb 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 @@ -20,6 +20,7 @@ object LombokNames { val ALL_ARGS_CONSTRUCTOR = FqName("lombok.AllArgsConstructor") val REQUIRED_ARGS_CONSTRUCTOR = FqName("lombok.RequiredArgsConstructor") val BUILDER = FqName("lombok.Builder") + val SINGULAR = FqName("lombok.Singular") val ACCESSORS_ID = ClassId.topLevel(ACCESSORS) @@ -49,4 +50,67 @@ object LombokNames { "org.springframework.lang.NonNull" ).map { FqName(it) }.toSet() + private val SUPPORTED_JAVA_COLLECTIONS = setOf( + "java.util.Iterable", + "java.util.Collection", + "java.util.List", + "java.util.Set", + "java.util.SortedSet", + "java.util.NavigableSet", + ) + + private val SUPPORTED_JAVA_MAPS = setOf( + "java.util.Map", + "java.util.SortedMap", + "java.util.NavigableMap", + ) + + private val SUPPORTED_KOTLIN_COLLECTIONS = setOf( + "kotlin.collections.Iterable", + "kotlin.collections.MutableIterable", + "kotlin.collections.Collection", + "kotlin.collections.MutableCollection", + "kotlin.collections.List", + "kotlin.collections.MutableList", + "kotlin.collections.Set", + "kotlin.collections.MutableSet", + ) + + private val SUPPORTED_KOTLIN_MAPS = setOf( + "kotlin.collections.Map", + "kotlin.collections.MutableMap", + ) + + private val SUPPORTED_COLLECTIONS = SUPPORTED_JAVA_COLLECTIONS + SUPPORTED_KOTLIN_COLLECTIONS + private val SUPPORTED_MAPS = SUPPORTED_JAVA_MAPS + SUPPORTED_KOTLIN_MAPS + + private val SUPPORTED_COLLECTIONS_WITH_GUAVA = SUPPORTED_COLLECTIONS + setOf( + "com.google.common.collect.ImmutableCollection", + "com.google.common.collect.ImmutableList", + "com.google.common.collect.ImmutableSet", + "com.google.common.collect.ImmutableSortedSet", + ) + + private val SUPPORTED_MAPS_WITH_GUAVA = SUPPORTED_MAPS + setOf( + "com.google.common.collect.ImmutableMap", + "com.google.common.collect.ImmutableBiMap", + "com.google.common.collect.ImmutableSortedMap", + "com.google.common.collect.ImmutableTable", + ) + + fun getSupportedCollectionsForSingular(includeGuava: Boolean): Set { + return if (includeGuava) { + SUPPORTED_COLLECTIONS_WITH_GUAVA + } else { + SUPPORTED_COLLECTIONS + } + } + + fun getSupportedMapsForSingular(includeGuava: Boolean): Set { + return if (includeGuava) { + SUPPORTED_MAPS_WITH_GUAVA + } else { + SUPPORTED_MAPS + } + } } 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 61d55f0e4e2..151e0a06a83 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 @@ -219,6 +219,20 @@ object LombokAnnotations { } } } + + class Singular( + val singularName: String?, + val allowNull: Boolean, + ) { + companion object : AnnotationCompanion(LombokNames.SINGULAR) { + override fun extract(annotation: AnnotationDescriptor): Singular { + return Singular( + singularName = annotation.getStringArgument("value"), + allowNull = annotation.getBooleanArgument("ignoreNullCollections") ?: false + ) + } + } + } } 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 index 851dd484b5f..7af5ef6e5c5 100644 --- 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 @@ -5,16 +5,28 @@ package org.jetbrains.kotlin.lombok.processor +import com.intellij.openapi.util.text.StringUtil import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.annotations.Annotations +import org.jetbrains.kotlin.descriptors.annotations.CompositeAnnotations 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.load.java.typeEnhancement.ENHANCED_NULLABILITY_ANNOTATIONS import org.jetbrains.kotlin.lombok.config.LombokAnnotations.Builder +import org.jetbrains.kotlin.lombok.config.LombokAnnotations.Singular 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 +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe +import org.jetbrains.kotlin.resolve.descriptorUtil.module +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeProjectionImpl +import org.jetbrains.kotlin.types.replace +import org.jetbrains.kotlin.types.typeUtil.makeNotNullable +import org.jetbrains.kotlin.types.typeUtil.makeNullable +import org.jetbrains.kotlin.types.typeUtil.replaceAnnotations class BuilderProcessor(private val config: LombokConfig) : Processor { companion object { @@ -107,9 +119,13 @@ class BuilderProcessor(private val config: LombokConfig) : Processor { builderClass: ClassDescriptor, partsBuilder: SyntheticPartsBuilder ) { - val prefix = builder.setterPrefix + Singular.getOrNull(field)?.let { singular -> + createMethodsForSingularField(builder, singular, field, builderClass, partsBuilder) + return + } + val fieldName = field.name - val setterName = if (prefix.isNullOrBlank()) fieldName else Name.identifier("${prefix}${field.name.asString().capitalize()}") + val setterName = fieldName.toMethodName(builder) val setFunction = builderClass.createFunction( name = setterName, valueParameters = listOf(LombokValueParameter(fieldName, field.type)), @@ -120,5 +136,107 @@ class BuilderProcessor(private val config: LombokConfig) : Processor { partsBuilder.addMethod(setFunction) } + private fun createMethodsForSingularField( + builder: Builder, + singular: Singular, + field: PropertyDescriptor, + builderClass: ClassDescriptor, + partsBuilder: SyntheticPartsBuilder + ) { + val nameInSingularForm = (singular.singularName ?: field.name.identifier.singularForm)?.let(Name::identifier) ?: return + val typeName = field.type.constructor.declarationDescriptor?.fqNameSafe?.asString() ?: return + + val useGuava = config.useGuava + val supportedCollections = LombokNames.getSupportedCollectionsForSingular(useGuava) + val supportedMaps = LombokNames.getSupportedMapsForSingular(useGuava) + + val addMultipleParameterType: KotlinType + val valueParameters: List + + when (typeName) { + in supportedCollections -> { + val parameterType = field.parameterType(0, singular.allowNull) ?: return + valueParameters = listOf( + LombokValueParameter(nameInSingularForm, parameterType) + ) + + addMultipleParameterType = field.module.builtIns.collection.defaultType.replace( + newArguments = listOf(TypeProjectionImpl(parameterType)) + ) + } + + in supportedMaps -> { + val keyType = field.parameterType(0, singular.allowNull) ?: return + val valueType = field.parameterType(1, singular.allowNull) ?: return + valueParameters = listOf( + LombokValueParameter(Name.identifier("key"), keyType), + LombokValueParameter(Name.identifier("value"), valueType), + ) + + addMultipleParameterType = field.module.builtIns.map.defaultType.replace( + newArguments = listOf(TypeProjectionImpl(keyType), TypeProjectionImpl(valueType)) + ) + } + + else -> return + } + + val builderType = builderClass.defaultType + val visibility = builder.visibility.toDescriptorVisibility() + + val addSingleFunction = builderClass.createFunction( + name = nameInSingularForm.toMethodName(builder), + valueParameters, + returnType = builderType, + modality = Modality.FINAL, + visibility = visibility + ) + partsBuilder.addMethod(addSingleFunction) + + val addMultipleFunction = builderClass.createFunction( + name = field.name.toMethodName(builder), + valueParameters = listOf(LombokValueParameter(field.name, addMultipleParameterType)), + returnType = builderType, + modality = Modality.FINAL, + visibility = visibility + ) + partsBuilder.addMethod(addMultipleFunction) + + val clearFunction = builderClass.createFunction( + name = Name.identifier("clear${field.name.identifier.capitalize()}"), + valueParameters = listOf(), + returnType = builderType, + modality = Modality.FINAL, + visibility = visibility + ) + partsBuilder.addMethod(clearFunction) + } + + private val String.singularForm: String? + get() = StringUtil.unpluralize(this) + private class BuilderData(val builder: Builder, val constructingClass: ClassDescriptor) + + private val LombokConfig.useGuava: Boolean + get() = getBoolean("lombok.singular.useGuava") ?: false + + private fun PropertyDescriptor.parameterType(index: Int, allowNull: Boolean): KotlinType? { + val type = returnType?.arguments?.getOrNull(index)?.type ?: return null + val typeWithProperNullability = if (allowNull) type.makeNullable() else type.makeNotNullable() + return typeWithProperNullability.replaceAnnotations( + CompositeAnnotations( + typeWithProperNullability.annotations, + ENHANCED_NULLABILITY_ANNOTATIONS + ) + ) + } + + private fun Name.toMethodName(builder: Builder): Name { + val prefix = builder.setterPrefix + return if (prefix.isNullOrBlank()) { + this + } else { + Name.identifier("$prefix${identifier.capitalize()}") + } + } } diff --git a/plugins/lombok/testData/box/builderSingular.kt b/plugins/lombok/testData/box/builderSingular.kt new file mode 100644 index 00000000000..535c7dd8851 --- /dev/null +++ b/plugins/lombok/testData/box/builderSingular.kt @@ -0,0 +1,52 @@ +// WITH_STDLIB +// FULL_JDK +// FILE: User.java +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Builder +@Data +public class User { + @Singular private java.util.Map numbers; + @Singular private java.util.List statuses; +} + +// FILE: Other.java +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Builder(setterPrefix = "with") +@Data +public class Other { + @Singular("singleSome") private java.util.List some; +} + +// FILE: test.kt +fun box(): String { + val userBuilder = User.builder() + .status("wrong") + .clearStatuses() + .status("hello") + .statuses(listOf("world", "!")) + .number("1", 1) + .numbers(mapOf("2" to 2, "3" to 3)) + + val user = userBuilder.build() + + val outer = Other.builder() + .withSingleSome(1) + .withSome(listOf(2, 3)) + .build() + + val expectedNumbers = mapOf("1" to 1, "2" to 2, "3" to 3) + val expectedStatuses = listOf("hello", "world", "!") + val expectedSome = listOf(1, 2, 3) + + return if (user.numbers == expectedNumbers && user.statuses == expectedStatuses && outer.some == expectedSome) { + "OK" + } else { + "Error: $user" + } +} diff --git a/plugins/lombok/testData/diagnostics/builderSingularNullability.kt b/plugins/lombok/testData/diagnostics/builderSingularNullability.kt new file mode 100644 index 00000000000..c9dde278d32 --- /dev/null +++ b/plugins/lombok/testData/diagnostics/builderSingularNullability.kt @@ -0,0 +1,50 @@ +// FIR_IDENTICAL +// WITH_STDLIB +// FULL_JDK +// FILE: User.java +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Builder +@Data +public class User { + @Singular private java.util.List names; +} + +// FILE: UserWithoutNull.java +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Builder +@Data +public class UserWithoutNull { + @Singular(ignoreNullCollections = false) private java.util.List names; +} + +// FILE: UserWithNull.java +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Builder +@Data +public class UserWithNull { + @Singular(ignoreNullCollections = true) private java.util.List names; +} + +// FILE: test.kt +fun test() { + User.builder() + .name("User") + .name(null) // error + + UserWithoutNull.builder() + .name("User") + .name(null) // error + + UserWithNull.builder() + .name("User") + .name(null) // ok +} diff --git a/plugins/lombok/testData/diagnostics/builderSingularNullability.txt b/plugins/lombok/testData/diagnostics/builderSingularNullability.txt new file mode 100644 index 00000000000..e439d025d85 --- /dev/null +++ b/plugins/lombok/testData/diagnostics/builderSingularNullability.txt @@ -0,0 +1,43 @@ +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() + @lombok.Singular private final var names: kotlin.collections.(Mutable)List! + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open /*synthesized*/ fun getNames(): kotlin.collections.(Mutable)List! + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open /*synthesized*/ fun setNames(/*0*/ names: kotlin.collections.(Mutable)List!): kotlin.Unit + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun builder(): User.UserBuilder +} + +@lombok.Builder @lombok.Data public open class UserWithNull { + public constructor UserWithNull() + @lombok.Singular(ignoreNullCollections = true) private final var names: kotlin.collections.(Mutable)List! + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open /*synthesized*/ fun getNames(): kotlin.collections.(Mutable)List! + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open /*synthesized*/ fun setNames(/*0*/ names: kotlin.collections.(Mutable)List!): kotlin.Unit + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun builder(): UserWithNull.UserWithNullBuilder +} + +@lombok.Builder @lombok.Data public open class UserWithoutNull { + public constructor UserWithoutNull() + @lombok.Singular(ignoreNullCollections = false) private final var names: kotlin.collections.(Mutable)List! + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open /*synthesized*/ fun getNames(): kotlin.collections.(Mutable)List! + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open /*synthesized*/ fun setNames(/*0*/ names: kotlin.collections.(Mutable)List!): kotlin.Unit + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun builder(): UserWithoutNull.UserWithoutNullBuilder +} 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 9ff2411feec..0f2098fe172 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/BlackBoxCodegenTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/BlackBoxCodegenTestForLombokGenerated.java @@ -55,6 +55,12 @@ public class BlackBoxCodegenTestForLombokGenerated extends AbstractBlackBoxCodeg runTest("plugins/lombok/testData/box/builder.kt"); } + @Test + @TestMetadata("builderSingular.kt") + public void testBuilderSingular() throws Exception { + runTest("plugins/lombok/testData/box/builderSingular.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 a41348ee566..521f125c2e6 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/DiagnosticTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/DiagnosticTestForLombokGenerated.java @@ -48,6 +48,12 @@ public class DiagnosticTestForLombokGenerated extends AbstractDiagnosticTestForL runTest("plugins/lombok/testData/diagnostics/builderConfig.kt"); } + @Test + @TestMetadata("builderSingularNullability.kt") + public void testBuilderSingularNullability() throws Exception { + runTest("plugins/lombok/testData/diagnostics/builderSingularNullability.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 633732841ed..8508578117c 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirBlackBoxCodegenTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirBlackBoxCodegenTestForLombokGenerated.java @@ -55,6 +55,12 @@ public class FirBlackBoxCodegenTestForLombokGenerated extends AbstractFirBlackBo runTest("plugins/lombok/testData/box/builder.kt"); } + @Test + @TestMetadata("builderSingular.kt") + public void testBuilderSingular() throws Exception { + runTest("plugins/lombok/testData/box/builderSingular.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 086f992ed6a..ab35c8f5cb7 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirDiagnosticTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/FirDiagnosticTestForLombokGenerated.java @@ -48,6 +48,12 @@ public class FirDiagnosticTestForLombokGenerated extends AbstractFirDiagnosticTe runTest("plugins/lombok/testData/diagnostics/builderConfig.kt"); } + @Test + @TestMetadata("builderSingularNullability.kt") + public void testBuilderSingularNullability() throws Exception { + runTest("plugins/lombok/testData/diagnostics/builderSingularNullability.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 6e57d9bd9de..98f3cb7d9de 100644 --- a/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/IrBlackBoxCodegenTestForLombokGenerated.java +++ b/plugins/lombok/tests-gen/org/jetbrains/kotlin/lombok/IrBlackBoxCodegenTestForLombokGenerated.java @@ -55,6 +55,12 @@ public class IrBlackBoxCodegenTestForLombokGenerated extends AbstractIrBlackBoxC runTest("plugins/lombok/testData/box/builder.kt"); } + @Test + @TestMetadata("builderSingular.kt") + public void testBuilderSingular() throws Exception { + runTest("plugins/lombok/testData/box/builderSingular.kt"); + } + @Test @TestMetadata("configAccessors.kt") public void testConfigAccessors() throws Exception {