[Lombok] Properly support Guava collections with @Singular

^KT-53683 Fixed
This commit is contained in:
Dmitriy Novozhilov
2022-09-09 12:14:12 +03:00
committed by teamcity
parent 69a857649f
commit 2aadaee69f
12 changed files with 253 additions and 90 deletions
@@ -111,7 +111,7 @@ object EnvironmentBasedStandardLibrariesPathProvider : KotlinStandardLibrariesPa
const val KOTLIN_SCRIPT_RUNTIME_PROP = "org.jetbrains.kotlin.test.kotlin-script-runtime"
const val KOTLIN_ANNOTATIONS_JVM_PROP = "org.jetbrains.kotlin.test.kotlin-annotations-jvm"
private fun getFile(propertyName: String): File {
fun getFile(propertyName: String): File {
return System.getProperty(propertyName)
?.let(::File)
?.takeIf { it.exists() }
+11 -1
View File
@@ -38,7 +38,7 @@ dependencies {
testApi(commonDependency("junit:junit"))
testRuntimeOnly(commonDependency("com.google.guava:guava"))
testRuntimeOnly(toolsJar())
}
@@ -54,6 +54,16 @@ sourceSets {
projectTest(parallel = true) {
workingDir = rootDir
doFirst {
project.configurations
.testRuntimeClasspath.get()
.files
.find { "guava" in it.name }
?.absolutePath
?.let { systemProperty("org.jetbrains.kotlin.test.guava-location", it) }
}
}
runtimeJar()
@@ -22,6 +22,7 @@ object LombokNames {
val BUILDER = FqName("lombok.Builder")
val SINGULAR = FqName("lombok.Singular")
val TABLE = FqName("com.google.common.collect.Table")
val ACCESSORS_ID = ClassId.topLevel(ACCESSORS)
val GETTER_ID = ClassId.topLevel(GETTER)
@@ -35,6 +36,8 @@ object LombokNames {
val ALL_ARGS_CONSTRUCTOR_ID = ClassId.topLevel(ALL_ARGS_CONSTRUCTOR)
val REQUIRED_ARGS_CONSTRUCTOR_ID = ClassId.topLevel(REQUIRED_ARGS_CONSTRUCTOR)
val TABLE_CLASS_ID = ClassId.topLevel(TABLE)
//taken from idea lombok plugin
val NON_NULL_ANNOTATIONS = listOf(
"androidx.annotation.NonNull",
@@ -83,36 +86,23 @@ object LombokNames {
"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(
val SUPPORTED_GUAVA_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(
private val SUPPORTED_GUAVA_MAPS = setOf(
"com.google.common.collect.ImmutableMap",
"com.google.common.collect.ImmutableBiMap",
"com.google.common.collect.ImmutableSortedMap",
)
val SUPPORTED_COLLECTIONS = SUPPORTED_JAVA_COLLECTIONS + SUPPORTED_KOTLIN_COLLECTIONS + SUPPORTED_GUAVA_COLLECTIONS
val SUPPORTED_MAPS = SUPPORTED_JAVA_MAPS + SUPPORTED_KOTLIN_MAPS + SUPPORTED_GUAVA_MAPS
val SUPPORTED_TABLES = setOf(
"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
}
}
}
@@ -9,6 +9,7 @@ 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.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.load.java.JavaDescriptorVisibilities
import org.jetbrains.kotlin.load.java.lazy.LazyJavaResolverContext
import org.jetbrains.kotlin.load.java.lazy.descriptors.SyntheticJavaClassDescriptor
@@ -146,26 +147,28 @@ class BuilderProcessor(private val config: LombokConfig) : Processor {
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 -> {
in LombokNames.SUPPORTED_COLLECTIONS -> {
val parameterType = field.parameterType(0, singular.allowNull) ?: return
valueParameters = listOf(
LombokValueParameter(nameInSingularForm, parameterType)
)
addMultipleParameterType = field.module.builtIns.collection.defaultType.replace(
val builtIns = field.module.builtIns
val baseType = when (typeName) {
in LombokNames.SUPPORTED_GUAVA_COLLECTIONS -> builtIns.iterable.defaultType
else -> builtIns.collection.defaultType
}
addMultipleParameterType = baseType.replace(
newArguments = listOf(TypeProjectionImpl(parameterType))
)
}
in supportedMaps -> {
in LombokNames.SUPPORTED_MAPS -> {
val keyType = field.parameterType(0, singular.allowNull) ?: return
val valueType = field.parameterType(1, singular.allowNull) ?: return
valueParameters = listOf(
@@ -178,6 +181,28 @@ class BuilderProcessor(private val config: LombokConfig) : Processor {
)
}
in LombokNames.SUPPORTED_TABLES -> {
val rowKeyType = field.parameterType(0, singular.allowNull) ?: return
val columnKeyType = field.parameterType(1, singular.allowNull) ?: return
val valueType = field.parameterType(2, singular.allowNull) ?: return
val tableDescriptor = field.module.resolveClassByFqName(LombokNames.TABLE, NoLookupLocation.FROM_SYNTHETIC_SCOPE) ?: return
valueParameters = listOf(
LombokValueParameter(Name.identifier("rowKey"), rowKeyType),
LombokValueParameter(Name.identifier("columnKey"), columnKeyType),
LombokValueParameter(Name.identifier("value"), valueType),
)
addMultipleParameterType = tableDescriptor.defaultType.replace(
newArguments = listOf(
TypeProjectionImpl(rowKeyType),
TypeProjectionImpl(columnKeyType),
TypeProjectionImpl(valueType),
)
)
}
else -> return
}
@@ -217,9 +242,6 @@ class BuilderProcessor(private val config: LombokConfig) : Processor {
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()
@@ -184,24 +184,25 @@ class BuilderGenerator(session: FirSession) : FirDeclarationGenerationExtension(
val nameInSingularForm = (singular.singularName ?: field.name.identifier.singularForm)?.let(Name::identifier) ?: return
val useGuava = lombokService.config.useGuava
val supportedCollections = LombokNames.getSupportedCollectionsForSingular(useGuava)
val supportedMaps = LombokNames.getSupportedMapsForSingular(useGuava)
val addMultipleParameterType: FirTypeRef
val valueParameters: List<ConeLombokValueParameter>
when (typeName) {
in supportedCollections -> {
in LombokNames.SUPPORTED_COLLECTIONS -> {
val parameterType = javaClassifierType.parameterType(0, singular.allowNull) ?: return
valueParameters = listOf(
ConeLombokValueParameter(nameInSingularForm, parameterType.toRef())
)
addMultipleParameterType = DummyJavaClassType(JavaClasses.Collection, typeArguments = listOf(parameterType)).toRef()
val baseType = when (typeName) {
in LombokNames.SUPPORTED_GUAVA_COLLECTIONS -> JavaClasses.Iterable
else -> JavaClasses.Collection
}
addMultipleParameterType = DummyJavaClassType(baseType, typeArguments = listOf(parameterType)).toRef()
}
in supportedMaps -> {
in LombokNames.SUPPORTED_MAPS -> {
val keyType = javaClassifierType.parameterType(0, singular.allowNull) ?: return
val valueType = javaClassifierType.parameterType(1, singular.allowNull) ?: return
valueParameters = listOf(
@@ -212,6 +213,23 @@ class BuilderGenerator(session: FirSession) : FirDeclarationGenerationExtension(
addMultipleParameterType = DummyJavaClassType(JavaClasses.Map, typeArguments = listOf(keyType, valueType)).toRef()
}
in LombokNames.SUPPORTED_TABLES -> {
val rowKeyType = javaClassifierType.parameterType(0, singular.allowNull) ?: return
val columnKeyType = javaClassifierType.parameterType(1, singular.allowNull) ?: return
val valueType = javaClassifierType.parameterType(2, singular.allowNull) ?: return
valueParameters = listOf(
ConeLombokValueParameter(Name.identifier("rowKey"), rowKeyType.toRef()),
ConeLombokValueParameter(Name.identifier("columnKey"), columnKeyType.toRef()),
ConeLombokValueParameter(Name.identifier("value"), valueType.toRef()),
)
addMultipleParameterType = DummyJavaClassType(
JavaClasses.Table,
typeArguments = listOf(rowKeyType, columnKeyType, valueType)
).toRef()
}
else -> return
}
@@ -255,9 +273,6 @@ class BuilderGenerator(session: FirSession) : FirDeclarationGenerationExtension(
private val String.singularForm: String?
get() = StringUtil.unpluralize(this)
private val LombokConfig.useGuava: Boolean
get() = getBoolean("lombok.singular.useGuava") ?: false
private fun JavaClassifierType.parameterType(index: Int, allowNull: Boolean): JavaType? {
val type = typeArguments.getOrNull(index) ?: return null
return if (allowNull) type.makeNullable() else type.makeNotNullable()
@@ -7,17 +7,24 @@ package org.jetbrains.kotlin.lombok.k2.java
import org.jetbrains.kotlin.descriptors.Visibility
import org.jetbrains.kotlin.load.java.structure.*
import org.jetbrains.kotlin.lombok.utils.LombokNames
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
object JavaClasses {
val Iterable = DummyJavaClass("Iterable", javaLangName("Iterable"), numberOfTypeParameters = 1)
val Collection = DummyJavaClass("Collection", javaUtilName("Collection"), numberOfTypeParameters = 1)
val Map = DummyJavaClass("Map", javaUtilName("Map"), numberOfTypeParameters = 2)
val Table = DummyJavaClass("Table", LombokNames.TABLE, numberOfTypeParameters = 3)
private fun javaUtilName(name: String): FqName {
return FqName.fromSegments(listOf("java", "util", name))
}
private fun javaLangName(name: String): FqName {
return FqName.fromSegments(listOf("java", "lang", name))
}
}
class DummyJavaClass(name: String, override val fqName: FqName, numberOfTypeParameters: Int) : JavaClass {
+68
View File
@@ -0,0 +1,68 @@
// WITH_GUAVA
// WITH_STDLIB
// FULL_JDK
// FILE: User.java
import lombok.Builder;
import lombok.Data;
import lombok.Singular;
@Builder
@Data
public class User {
@Singular private com.google.common.collect.ImmutableMap<String, Integer> numbers;
@Singular private com.google.common.collect.ImmutableList<String> statuses;
@Singular private com.google.common.collect.ImmutableTable<String, String, String> values;
}
// 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
import com.google.common.collect.ImmutableTable
import com.google.common.collect.HashBasedTable
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))
.value("1", "a", "hello")
.values(ImmutableTable.of("2", "b", "world"))
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)
val expectedValues = HashBasedTable.create<String, String, String>().apply {
put("1", "a", "hello")
put("2", "b", "world")
}
return if (
user.numbers == expectedNumbers &&
user.statuses == expectedStatuses &&
user.values == expectedValues &&
outer.some == expectedSome
) {
"OK"
} else {
"Error: $user"
}
}
@@ -55,6 +55,12 @@ public class BlackBoxCodegenTestForLombokGenerated extends AbstractBlackBoxCodeg
runTest("plugins/lombok/testData/box/builder.kt");
}
@Test
@TestMetadata("builderGuava.kt")
public void testBuilderGuava() throws Exception {
runTest("plugins/lombok/testData/box/builderGuava.kt");
}
@Test
@TestMetadata("builderSingular.kt")
public void testBuilderSingular() throws Exception {
@@ -55,6 +55,12 @@ public class FirBlackBoxCodegenTestForLombokGenerated extends AbstractFirBlackBo
runTest("plugins/lombok/testData/box/builder.kt");
}
@Test
@TestMetadata("builderGuava.kt")
public void testBuilderGuava() throws Exception {
runTest("plugins/lombok/testData/box/builderGuava.kt");
}
@Test
@TestMetadata("builderSingular.kt")
public void testBuilderSingular() throws Exception {
@@ -55,6 +55,12 @@ public class IrBlackBoxCodegenTestForLombokGenerated extends AbstractIrBlackBoxC
runTest("plugins/lombok/testData/box/builder.kt");
}
@Test
@TestMetadata("builderGuava.kt")
public void testBuilderGuava() throws Exception {
runTest("plugins/lombok/testData/box/builderGuava.kt");
}
@Test
@TestMetadata("builderSingular.kt")
public void testBuilderSingular() throws Exception {
@@ -0,0 +1,79 @@
/*
* 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
import lombok.Getter
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoot
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar.ExtensionStorage
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.lombok.LombokDirectives.WITH_GUAVA
import org.jetbrains.kotlin.lombok.LombokEnvironmentConfigurator.Companion.GUAVA_JAR
import org.jetbrains.kotlin.test.directives.model.DirectivesContainer
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.*
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
class LombokAdditionalSourceFileProvider(testServices: TestServices) : AdditionalSourceProvider(testServices) {
companion object {
const val COMMON_SOURCE_PATH = "plugins/lombok/testData/common.kt"
}
override fun produceAdditionalFiles(globalDirectives: RegisteredDirectives, module: TestModule): List<TestFile> {
return listOf(File(COMMON_SOURCE_PATH).toTestFile())
}
}
class LombokEnvironmentConfigurator(testServices: TestServices) : EnvironmentConfigurator(testServices) {
companion object {
const val LOMBOK_CONFIG_NAME = "lombok.config"
private const val guavaPropertyName = "org.jetbrains.kotlin.test.guava-location"
val GUAVA_JAR: File
get() = EnvironmentBasedStandardLibrariesPathProvider.getFile(guavaPropertyName)
}
override val directiveContainers: List<DirectivesContainer>
get() = listOf(LombokDirectives)
override fun configureCompilerConfiguration(configuration: CompilerConfiguration, module: TestModule) {
configuration.addJvmClasspathRoot(PathUtil.getResourcePathForClass(Getter::class.java))
if (WITH_GUAVA in module.directives) {
configuration.addJvmClasspathRoot(GUAVA_JAR)
}
val lombokConfig = findLombokConfig(module) ?: return
lombokConfig.copyTo(testServices.sourceFileProvider.javaSourceDirectory.resolve(lombokConfig.name))
configuration.put(LombokConfigurationKeys.CONFIG_FILE, lombokConfig)
}
private fun findLombokConfig(module: TestModule): File? {
return module.files.singleOrNull { it.name == LOMBOK_CONFIG_NAME }?.let {
testServices.sourceFileProvider.getRealFileForSourceFile(it)
}
}
override fun ExtensionStorage.registerCompilerExtensions(module: TestModule, configuration: CompilerConfiguration) {
LombokComponentRegistrar.registerComponents(this, configuration)
}
}
class LombokRuntimeClassPathProvider(testServices: TestServices) : RuntimeClasspathProvider(testServices) {
override fun runtimeClassPaths(module: TestModule): List<File> {
return if (WITH_GUAVA in module.directives) {
listOf(GUAVA_JAR)
} else {
emptyList()
}
}
}
object LombokDirectives : SimpleDirectivesContainer() {
val WITH_GUAVA by directive("Add guava to classpath")
}
@@ -5,26 +5,13 @@
package org.jetbrains.kotlin.lombok
import lombok.Getter
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoot
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar.ExtensionStorage
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.runners.AbstractDiagnosticTest
import org.jetbrains.kotlin.test.runners.AbstractFirDiagnosticTest
import org.jetbrains.kotlin.test.runners.codegen.AbstractBlackBoxCodegenTest
import org.jetbrains.kotlin.test.runners.codegen.AbstractFirBlackBoxCodegenTest
import org.jetbrains.kotlin.test.runners.codegen.AbstractIrBlackBoxCodegenTest
import org.jetbrains.kotlin.test.runners.configurationForClassicAndFirTestsAlongside
import org.jetbrains.kotlin.test.services.AdditionalSourceProvider
import org.jetbrains.kotlin.test.services.EnvironmentConfigurator
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.sourceFileProvider
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
// ---------------------------- box ----------------------------
@@ -71,38 +58,5 @@ open class AbstractFirDiagnosticTestForLombok : AbstractFirDiagnosticTest() {
fun TestConfigurationBuilder.enableLombok() {
useConfigurators(::LombokEnvironmentConfigurator)
useAdditionalSourceProviders(::LombokAdditionalSourceFileProvider)
}
class LombokAdditionalSourceFileProvider(testServices: TestServices) : AdditionalSourceProvider(testServices) {
companion object {
const val COMMON_SOURCE_PATH = "plugins/lombok/testData/common.kt"
}
override fun produceAdditionalFiles(globalDirectives: RegisteredDirectives, module: TestModule): List<TestFile> {
return listOf(File(COMMON_SOURCE_PATH).toTestFile())
}
}
class LombokEnvironmentConfigurator(testServices: TestServices) : EnvironmentConfigurator(testServices) {
companion object {
const val LOMBOK_CONFIG_NAME = "lombok.config"
}
override fun configureCompilerConfiguration(configuration: CompilerConfiguration, module: TestModule) {
configuration.addJvmClasspathRoot(PathUtil.getResourcePathForClass(Getter::class.java))
val lombokConfig = findLombokConfig(module) ?: return
lombokConfig.copyTo(testServices.sourceFileProvider.javaSourceDirectory.resolve(lombokConfig.name))
configuration.put(LombokConfigurationKeys.CONFIG_FILE, lombokConfig)
}
private fun findLombokConfig(module: TestModule): File? {
return module.files.singleOrNull { it.name == LOMBOK_CONFIG_NAME }?.let {
testServices.sourceFileProvider.getRealFileForSourceFile(it)
}
}
override fun ExtensionStorage.registerCompilerExtensions(module: TestModule, configuration: CompilerConfiguration) {
LombokComponentRegistrar.registerComponents(this, configuration)
}
useCustomRuntimeClasspathProviders(::LombokRuntimeClassPathProvider)
}