[lombok] Experimental lombok plugin prototype

Add extension point for java descriptors
Add simple usage of this point to generate getter method
Add simple test infrastructure to test compilation with lombok plugin
This commit is contained in:
Andrey Zinovyev
2021-03-24 13:09:50 +03:00
committed by TeamCityServer
parent ac86ad252f
commit f71e08df4d
30 changed files with 606 additions and 3 deletions
@@ -0,0 +1,42 @@
description = "Lombok compiler plugin"
plugins {
kotlin("jvm")
id("jps-compatible")
}
dependencies {
implementation(project(":compiler:util"))
implementation(project(":compiler:plugin-api"))
implementation(project(":compiler:frontend"))
implementation(project(":compiler:frontend.java"))
compileOnly(intellijCoreDep()) { includeJars("intellij-core") }
testImplementation(intellijCoreDep()) { includeJars("intellij-core") }
testImplementation(commonDep("junit:junit"))
testImplementation(projectTests(":compiler:tests-common"))
testImplementation("org.projectlombok:lombok:1.18.16")
testRuntimeOnly(toolsJar())
}
sourceSets {
"main" { projectDefault() }
"test" { projectDefault() }
}
projectTest(parallel = true) {
workingDir = rootDir
dependsOn(":dist")
}
publish()
runtimeJar()
testsJar()
sourcesJar()
javadocJar()
@@ -0,0 +1 @@
org.jetbrains.kotlin.lombok.LombokComponentRegistrar
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2021 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 com.intellij.mock.MockProject
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.resolve.jvm.extensions.SyntheticJavaResolveExtension
class LombokComponentRegistrar : ComponentRegistrar {
companion object {
fun registerComponents(project: Project) {
SyntheticJavaResolveExtension.registerExtension(project, LombokResolveExtension())
}
}
override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
registerComponents(project)
}
}
@@ -0,0 +1,14 @@
/*
* Copyright 2010-2021 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 org.jetbrains.kotlin.resolve.jvm.SyntheticJavaPartsProvider
import org.jetbrains.kotlin.resolve.jvm.extensions.SyntheticJavaResolveExtension
class LombokResolveExtension : SyntheticJavaResolveExtension {
override fun getProvider(): SyntheticJavaPartsProvider = LombokSyntheticJavaPartsProvider()
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010-2021 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 org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor
import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaClassDescriptor
import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl
import org.jetbrains.kotlin.lombok.processor.GetterProcessor
import org.jetbrains.kotlin.lombok.processor.Parts
import org.jetbrains.kotlin.lombok.processor.Processor
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.jvm.SyntheticJavaPartsProvider
import java.util.*
class LombokSyntheticJavaPartsProvider : SyntheticJavaPartsProvider {
private val processors = initProcessors()
private fun initProcessors(): List<Processor> =
listOf(
GetterProcessor()
)
private val partsCache: MutableMap<ClassDescriptor, Parts> = WeakHashMap()
override fun getSyntheticFunctionNames(thisDescriptor: ClassDescriptor): List<Name> =
getSyntheticParts(thisDescriptor).methods.map { it.name }
override fun generateSyntheticMethods(
thisDescriptor: ClassDescriptor,
name: Name,
result: MutableCollection<SimpleFunctionDescriptor>
) {
getSyntheticParts(thisDescriptor).methods.find { it.name == name }?.let {
result.add(it)
}
}
private fun extractClass(descriptor: ClassDescriptor): JavaClassImpl? =
(descriptor as? LazyJavaClassDescriptor)?.jClass as? JavaClassImpl
private fun getSyntheticParts(descriptor: ClassDescriptor): Parts =
extractClass(descriptor)?.let { jClass ->
partsCache.getOrPut(descriptor) {
computeSyntheticParts(descriptor, jClass)
}
} ?: Parts.Empty
private fun computeSyntheticParts(descriptor: ClassDescriptor, jClass: JavaClassImpl): Parts =
processors.map { it.contribute(descriptor, jClass) }.reduce { a, b -> a + b }
}
@@ -0,0 +1,9 @@
/*
* Copyright 2010-2021 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.config
class LombokConfig {
}
@@ -0,0 +1,63 @@
/*
* Copyright 2010-2021 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.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl
import org.jetbrains.kotlin.lombok.utils.LombokAnnotationNames
import org.jetbrains.kotlin.lombok.utils.createFunction
import org.jetbrains.kotlin.lombok.utils.getVisibility
import org.jetbrains.kotlin.lombok.utils.toPreparedBase
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.model.SimpleTypeMarker
import org.jetbrains.kotlin.types.typeUtil.isBoolean
import org.jetbrains.kotlin.types.typeUtil.isPrimitiveNumberType
class GetterProcessor : Processor {
override fun contribute(classDescriptor: ClassDescriptor, jClass: JavaClassImpl): Parts {
val functions = classDescriptor.unsubstitutedMemberScope.getVariableNames()
.map {
classDescriptor.unsubstitutedMemberScope.getContributedVariables(it, NoLookupLocation.FROM_SYNTHETIC_SCOPE).single()
}.collectWithNotNull { it.annotations.findAnnotation(LombokAnnotationNames.GETTER) }
.mapNotNull { (field, annotation) -> createGetter(classDescriptor, field, annotation) }
// val functions = jClass.fields.collectWithNotNull { it.findAnnotation(LombokAnnotationNames.GETTER) }.mapNotNull { (field, annotation) ->
// createGetter(classDescriptor, field, annotation)
// }
return Parts(functions)
}
private fun createGetter(
classDescriptor: ClassDescriptor,
field: PropertyDescriptor,
annotation: AnnotationDescriptor
): SimpleFunctionDescriptor? {
val prefix = if (field.type.isPrimitiveBoolean()) "is" else "get"
val functionName = Name.identifier(prefix + toPreparedBase(field.name.identifier))
return createFunction(
classDescriptor,
functionName,
emptyList(),
field.returnType,
visibility = getVisibility(annotation)
)
}
@Suppress("UNCHECKED_CAST")
internal fun <E, R> Collection<E>.collectWithNotNull(f: (E) -> R?): List<Pair<E, R>> =
map { it to f(it) }.filter { it.second != null } as List<Pair<E, R>>
private fun KotlinType.isPrimitiveBoolean(): Boolean = this is SimpleTypeMarker && isBoolean() //todo
}
@@ -0,0 +1,17 @@
/*
* Copyright 2010-2021 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.SimpleFunctionDescriptor
data class Parts(val methods: List<SimpleFunctionDescriptor> = emptyList()) {
operator fun plus(other: Parts): Parts = Parts(methods + other.methods)
companion object {
val Empty = Parts()
}
}
@@ -0,0 +1,14 @@
/*
* Copyright 2010-2021 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.ClassDescriptor
import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl
interface Processor {
fun contribute(classDescriptor: ClassDescriptor, jClass: JavaClassImpl): Parts
}
@@ -0,0 +1,15 @@
/*
* Copyright 2010-2021 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.utils
import org.jetbrains.kotlin.name.FqName
object LombokAnnotationNames {
val GETTER = FqName("lombok.Getter")
val SETTER = FqName("lombok.Setter")
}
@@ -0,0 +1,63 @@
/*
* Copyright 2010-2021 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.utils
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.DescriptorVisibility
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.descriptors.java.JavaVisibilities
import org.jetbrains.kotlin.load.java.structure.JavaAnnotation
import org.jetbrains.kotlin.load.java.structure.JavaEnumValueAnnotationArgument
import org.jetbrains.kotlin.load.java.structure.JavaLiteralAnnotationArgument
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.constants.EnumValue
import java.lang.IllegalArgumentException
internal fun getVisibility(annotation: JavaAnnotation, field: String = "value"): DescriptorVisibility {
val value = getStringAnnotationValue(annotation, field, "PUBLIC")
val visibility = when (value) {
"PUBLIC" -> Visibilities.Public
"PROTECTED" -> Visibilities.Protected
"PRIVATE" -> Visibilities.Private
"PACKAGE" -> JavaVisibilities.PackageVisibility
else -> Visibilities.Public
}
return DescriptorVisibilities.toDescriptorVisibility(visibility)
}
internal fun getVisibility(annotation: AnnotationDescriptor, field: String = "value"): DescriptorVisibility {
val value = getStringAnnotationValue(annotation, field, "PUBLIC")
val visibility = when (value) {
"PUBLIC" -> Visibilities.Public
"PROTECTED" -> Visibilities.Protected
"PRIVATE" -> Visibilities.Private
"PACKAGE" -> JavaVisibilities.PackageVisibility
else -> Visibilities.Public
}
return DescriptorVisibilities.toDescriptorVisibility(visibility)
}
private fun getStringAnnotationValue(annotation: JavaAnnotation, argumentName: String, default: String): String {
val argument = annotation.arguments.find { it.name?.identifier == argumentName }
?: throw IllegalArgumentException("No argument '$argumentName' found in $annotation")
return when (argument) {
is JavaEnumValueAnnotationArgument -> argument.entryName?.asString() ?: default
is JavaLiteralAnnotationArgument -> argument.value?.toString() ?: default
else -> throw RuntimeException("Argument $argument is not supported")
}
}
private fun getStringAnnotationValue(annotation: AnnotationDescriptor, argumentName: String, default: String): String {
val argument = annotation.allValueArguments[Name.identifier(argumentName)]
?: return default
return when (argument) {
is EnumValue -> argument.enumEntryName.identifier
else -> throw RuntimeException("Argument $argument is not supported")
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2010-2021 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.utils
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.impl.SimpleFunctionDescriptorImpl
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.types.KotlinType
internal fun createFunction(
containingClass: ClassDescriptor,
name: Name,
valueParameters: List<ValueParameterDescriptor>,
returnType: KotlinType?,
modality: Modality? = Modality.OPEN,
visibility: DescriptorVisibility = DescriptorVisibilities.PUBLIC
): SimpleFunctionDescriptor {
val methodDescriptor = SimpleFunctionDescriptorImpl.create(
containingClass,
Annotations.EMPTY,
name,
CallableMemberDescriptor.Kind.SYNTHESIZED,
containingClass.source
)
methodDescriptor.initialize(
null,
containingClass.thisAsReceiverParameter,
mutableListOf(),
valueParameters,
returnType,
modality,
visibility
)
return methodDescriptor
}
@@ -0,0 +1,10 @@
/*
* Copyright 2010-2021 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.utils
import org.jetbrains.kotlin.util.capitalizeDecapitalize.capitalizeAsciiOnly
internal fun toPreparedBase(name: String): String = name.capitalizeAsciiOnly()
@@ -0,0 +1,38 @@
//FILE: GetterTest.java
import lombok.AccessLevel;
import lombok.Getter;
public class GetterTest {
@Getter private int age = 10;
@Getter(AccessLevel.PROTECTED) private String name;
@Getter private boolean primitiveBoolean;
@Getter private Boolean boxedBoolean;
void test() {
getAge();
isPrimitiveBoolean();
}
}
//FILE: test.kt
object Test {
fun usage() {
val obj = GetterTest()
val getter = obj.getAge()
val property = obj.age
//todo kotlin doesn't see isBoolean methods as properties
// obj.primitiveBoolean
obj.isPrimitiveBoolean()
obj.boxedBoolean
obj.getBoxedBoolean()
}
}
@@ -0,0 +1,22 @@
//FILE: GetterSetterExample.java
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
public class GetterSetterExample {
@Getter @Setter private int age = 10;
@Getter(AccessLevel.PROTECTED) private String name;
}
//FILE: test.kt
object Test {
fun usage() {
val obj = GetterSetterExample()
val getter = obj.getAge()
val property = obj.age
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2021 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.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot
import org.jetbrains.kotlin.codegen.CodegenTestCase
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
abstract class AbstractLombokCompileTest : CodegenTestCase() {
override fun doMultiFileTest(wholeFile: File, files: List<TestFile>) {
compile(files)
}
override fun setupEnvironment(environment: KotlinCoreEnvironment) {
LombokComponentRegistrar.registerComponents(environment.project)
environment.updateClasspath(listOf(JvmClasspathRoot(getLombokJar())))
}
override fun updateJavaClasspath(javaClasspath: MutableList<String>) {
javaClasspath += getLombokJar().absolutePath
}
private fun getLombokJar(): File = PathUtil.getResourcePathForClass(Getter::class.java)
}
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2021 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 com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.util.KtTestUtil;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("plugins/lombok/lombok-compiler-plugin/testData/compile")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class LombokCompileTestGenerated extends AbstractLombokCompileTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
public void testAllFilesPresentInCompile() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("plugins/lombok/lombok-compiler-plugin/testData/compile"), Pattern.compile("^(.+)\\.kt$"), null, true);
}
@TestMetadata("getters.kt")
public void testGetters() throws Exception {
runTest("plugins/lombok/lombok-compiler-plugin/testData/compile/getters.kt");
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
runTest("plugins/lombok/lombok-compiler-plugin/testData/compile/simple.kt");
}
}