kotlin-dsl-samples icon indicating copy to clipboard operation
kotlin-dsl-samples copied to clipboard

KT-6653: Implementing Gradle's Java Bean structured interfaces in Kotlin is incredibly verbose

Open JLLeitschuh opened this issue 7 years ago • 9 comments

As an plugin developer implementing interfaces in Kotlin, some of the bean interfaces can be incredibly verbose to implement.

For example, if, in my plugin, I want to implement Named I need to create a separate property and also override the getName method.

data class SomethingNamed(
    val aName: String,
    var someOtherData: Data
): Named {
    override fun getName(): String {
        return aName
    }
}

If the Named interface were instead something like the below:

interface Named {
    val name: String
}

The implementation of such a class would be far simpler:

data class SomethingNamed(
    override val name: String,
    var someOtherData: Data
): Named

There are various other locations throughout the Gradle API where there are Bean style interfaces that would be simpler for plugin developers to work with if they were kotlinified.

An alternative to this, is to dynamically weave in the Kotlin annotations into the bytecode so that the Kotlin compiler sees these bean interfaces as having properties. I think spring does something similar with their API's to allow for better interop with Kotlin.

This change wouldn't be a binary breaking change for any existing Java or Groovy plugins, however, making such a change after the 1.0 release of the Kotlin DSL would constitute an API breaking change for Kotlin plugins.

Longer Example

This problem gets more complicated when you start getting even bigger bean interfaces

data class CustomPasswordCredentials(
    @get:Input
    var user: String?,
    @get:Input
    var pass: String?
) : PasswordCredentials {
    override fun setUsername(userName: String?) {
        user = userName
    }

    override fun getUsername(): String? {
        return user
    }

    override fun getPassword(): String? {
        return pass
    }

    override fun setPassword(password: String?) {
        pass = password
    }
}

JLLeitschuh avatar Aug 10 '18 13:08 JLLeitschuh

Yep, the required extra ceremony is annoying.

Given the following Java interface:

public interface InterfaceWithJavaBeanProperty {
    String getFoo();
    void setFoo(String value);
}

when implementing in Kotlin one has to write:

class JavaBeanPropertyFromJavaInterface(
    private var foo: String
) : InterfaceWithJavaBeanProperty {
    override fun getFoo(): String = foo
    override fun setFoo(value: String) { foo = value }
}

It feels like this should be enough:

class JavaBeanPropertyFromJavaInterface(override var foo: String) : InterfaceWithJavaBeanProperty

Could be seen as a Kotlin Java interop issue/feature. @JLLeitschuh could you please try find related KT issues?

@sdeleuze, any chance you could enlight us if something was done for Spring about that?

eskatos avatar Aug 10 '18 14:08 eskatos

The TL;DR this is a really complicated issue that I don't think will ever be fixed in the Kotlin Compiler.

https://youtrack.jetbrains.com/issue/KT-6653

This is a rather deep issue, unfortunately. It's unlikely that we'll ever make it work the way you'd like - @abreslav

JLLeitschuh avatar Aug 10 '18 14:08 JLLeitschuh

@JLLeitschuh thanks, that's unfortunate Could you also please edit the issue title to better reflect the issue at hand?

eskatos avatar Aug 10 '18 15:08 eskatos

@JLLeitschuh actually, nevermind, I edited the issue title

eskatos avatar Aug 10 '18 15:08 eskatos

@JLLeitschuh you beat me to it, good, I like your new title

eskatos avatar Aug 10 '18 15:08 eskatos

A simple fix would be to simply convert those interfaces to Kotlin but that would mean adding the Kotlin compiler to the core Gradle build. I'm guessing that will cause a heated discussion.

JLLeitschuh avatar Aug 10 '18 15:08 JLLeitschuh

@JLLeitschuh don't forget that the Gradle Java API is consumed by codebases in Java, Groovy, Kotlin, etc...

eskatos avatar Aug 10 '18 16:08 eskatos

Having these interfaces implemented in Kotlin I don't think would add a dependency upon the Kotlin standard lib.

I think that this compiled by the java compiler

interface Named {
    String getName();
}

And this compiled by the kotlin compiler:

interface Named {
    val name: String
}

Will produce pretty much the same bytecode. The kotlin compiler simply adds a bunch of meta annotations to the interface.

Converting these interfaces to Kotlin wouldn't impact consumers in Java or Groovy as far as I'm aware.

JLLeitschuh avatar Aug 10 '18 18:08 JLLeitschuh

This same problem arises when a plugin author overrides a task like SourceTask and wants to make it cacheable.

For example, if you have this before:

@CacheableTask
open class KtlintCheckTask @Inject constructor(
    private val objectFactory: ObjectFactory
) : SourceTask() {
}

Interacting with this task in Kotlin looks like this:

tasks.withType<KtlintCheckTask>().configure {
    source.matching { [whatever] }
}

When you want to make the task cacheable, you have to change it to this:

@CacheableTask
open class KtlintCheckTask @Inject constructor(
    private val objectFactory: ObjectFactory
) : SourceTask() {

    @InputFiles
    @SkipWhenEmpty
    @PathSensitive(PathSensitivity.RELATIVE)
    override fun getSource(): FileTree { return super.getSource() }
}

Which becomes an API breaking change requiring API consumers to change their code to this:

tasks.withType<KtlintCheckTask>().configure {
    getSource().matching { [whatever] }
}

JLLeitschuh avatar Oct 09 '18 16:10 JLLeitschuh