[Lombok K1] Support @Singular annotation
^KT-46959
This commit is contained in:
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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<String> {
|
||||
return if (includeGuava) {
|
||||
SUPPORTED_COLLECTIONS_WITH_GUAVA
|
||||
} else {
|
||||
SUPPORTED_COLLECTIONS
|
||||
}
|
||||
}
|
||||
|
||||
fun getSupportedMapsForSingular(includeGuava: Boolean): Set<String> {
|
||||
return if (includeGuava) {
|
||||
SUPPORTED_MAPS_WITH_GUAVA
|
||||
} else {
|
||||
SUPPORTED_MAPS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,20 @@ object LombokAnnotations {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Singular(
|
||||
val singularName: String?,
|
||||
val allowNull: Boolean,
|
||||
) {
|
||||
companion object : AnnotationCompanion<Singular>(LombokNames.SINGULAR) {
|
||||
override fun extract(annotation: AnnotationDescriptor): Singular {
|
||||
return Singular(
|
||||
singularName = annotation.getStringArgument("value"),
|
||||
allowNull = annotation.getBooleanArgument("ignoreNullCollections") ?: false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+120
-2
@@ -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<LombokValueParameter>
|
||||
|
||||
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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -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<String, Integer> numbers;
|
||||
@Singular private java.util.List<String> 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<Integer> 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"
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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<String> 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<String> names;
|
||||
}
|
||||
|
||||
// FILE: test.kt
|
||||
fun test() {
|
||||
User.builder()
|
||||
.name("User")
|
||||
.name(<!NULL_FOR_NONNULL_TYPE!>null<!>) // error
|
||||
|
||||
UserWithoutNull.builder()
|
||||
.name("User")
|
||||
.name(<!NULL_FOR_NONNULL_TYPE!>null<!>) // error
|
||||
|
||||
UserWithNull.builder()
|
||||
.name("User")
|
||||
.name(null) // ok
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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()
|
||||
@lombok.Singular private final var names: kotlin.collections.(Mutable)List<kotlin.String!>!
|
||||
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
|
||||
public open /*synthesized*/ fun getNames(): kotlin.collections.(Mutable)List<kotlin.String!>!
|
||||
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
|
||||
public open /*synthesized*/ fun setNames(/*0*/ names: kotlin.collections.(Mutable)List<kotlin.String!>!): 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<kotlin.String!>!
|
||||
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
|
||||
public open /*synthesized*/ fun getNames(): kotlin.collections.(Mutable)List<kotlin.String!>!
|
||||
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
|
||||
public open /*synthesized*/ fun setNames(/*0*/ names: kotlin.collections.(Mutable)List<kotlin.String!>!): 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<kotlin.String!>!
|
||||
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
|
||||
public open /*synthesized*/ fun getNames(): kotlin.collections.(Mutable)List<kotlin.String!>!
|
||||
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
|
||||
public open /*synthesized*/ fun setNames(/*0*/ names: kotlin.collections.(Mutable)List<kotlin.String!>!): kotlin.Unit
|
||||
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
|
||||
|
||||
// Static members
|
||||
public final /*synthesized*/ fun builder(): UserWithoutNull.UserWithoutNullBuilder
|
||||
}
|
||||
Generated
+6
@@ -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 {
|
||||
|
||||
Generated
+6
@@ -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 {
|
||||
|
||||
Generated
+6
@@ -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 {
|
||||
|
||||
Generated
+6
@@ -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 {
|
||||
|
||||
Generated
+6
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user