[Lombok K1] Support @Singular annotation

^KT-46959
This commit is contained in:
Dmitriy Novozhilov
2022-08-05 16:51:33 +03:00
parent b311f0b862
commit b32f9dcac2
12 changed files with 374 additions and 3 deletions
@@ -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
)
}
}
}
}
@@ -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
View File
@@ -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
}
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {