Name conflict resolution ordering

From Awkwardness with Gradle Kotlin DSL · Issue #624 · jreleaser/jreleaser · GitHub

Using jreleaser with kotlin-dsl, I need to use this.distributions because distributions binds to project.distributions. Is there a clean way to avoid this ambiguity?

Say a scope in JReleaserExtensions that causes it to prefer JReleaserExtension.distributions.

TL;DR: scroll down and read below the other horizontal ruler if you are not interested in the details


That’s more Kotlin precedence rules you are hitting here I’d say.

You have the two plugins distribution and org.jreleaser applied.

The distribution plugin adds an extension called distributions to the project, so when applied using the preferred plugins DSL method, the kotlin-dsl magic generates two accessors for this extension:

val Project.distributions: DistributionContainer get() = ...
fun Project.distributions(configure: Action<DistributionContainer>): Unit = ...

The org.jreleaser plugin has in its JReleaserExtension a method NamedDomainObjectContainer<Distribution> getDistributions().

The kotlin-dsl magic again has a shipped extension function

inline operator fun <T : Any, C : NamedDomainObjectContainer<T>> C.invoke(
    configuration: Action<NamedDomainObjectContainerScope<T>>
): C = ...

so that you can do distributions.invoke { ... } or as it is the invoke operator just distributions { ... }.

Additionally it is important for understanding this, that in Kotlin direct members win over extension functions or properties.

So what you now have available inside the JReleaserExtension is an extension function named distributions, an extension property named distributions, a member property named distributions and an extension operator function called invoke on the type of the member property.

If you now do distributions { ... }, the choice is between the direct extension function distributions and the extension operator function invoke on the distributions member property, so the direct extension function wins here by Kotlin language rules.


There are at least two things you could do to mitigate this precedence problem.

The imho worse one is to not apply the distribution plugin (or any plugin that applies it transitively like application) via the plugins DSL, but instead using the legacy apply syntax, because then no accessors are generated that then have higher precedence.

The better solution is within the org.jreleaser plugin. As I just shortly described the relevant precedence rules, you might have guessed it already. If the JReleaserExtensions has a member function called distributions which takes an Action<>, then it will win over the extensions function as member functions always win if possible.

Even an extension function JReleaserExtension.distributions would work as then the extension function on the inner scope would win over the extension function on the outer scope.

1 Like

Thanks for the thorough answer, much appreciated.

How would this member function in JReleaserExtensionImpl should look like? I tried

void distributions(Action? super NamedDomainObjectContainer<Distribution> action) { ... }

But didn’t work at all. TIA.

Why not?
Works fine here.
Or well, I didn’t really test it, but according to IntelliJ highlighting and code-linking it works fine.

I added

void distributions(Action<? super NamedDomainObjectContainer<Distribution>> action)

to the interface and

@Override
void distributions(Action<? super NamedDomainObjectContainer<Distribution>> action) {
}

to the implementation and it resolves as expected downstream according to IDE.

But actually you might want to return NamedDomainObjectContainer<Distribution> from your method though to be more consistent with the accessors that are generated.

Thanks. I should had been more clear. Adding the method is not a problem, the contents of the method are. Invoking action.execute(distributions) leads to a compiler error →

'execute' in 'org.gradle.api.Action<? super org.gradle.api.NamedDomainObjectContainer<org.jreleaser.gradle.plugin.dsl.Distribution>>' cannot be applied to '(org.gradle.api.NamedDomainObjectContainer<org.jreleaser.gradle.plugin.internal.dsl.DistributionImpl>)'

But this appears to compile

    @Override
    void distributions(Action<? super NamedDomainObjectContainer<? extends Distribution>> action) {
        action.execute(distributions)
    }

Which leads to usage such as

  distributions {
    create("okurl") {
      active.set(org.jreleaser.model.Active.RELEASE)
      distributionType.set(org.jreleaser.model.Distribution.DistributionType.NATIVE_IMAGE)
      artifact {
        platform.set("osx")
        path.set(file("build/distributions/okurl-graal-$version.zip"))
      }
    }
  }

Yeah, well, that’s because of that NamedDomainObjectContainer<DistributionImpl> distributions you have in the impl class. NamedDomainObjectContainer<DistributionImpl> is not compatible to NamedDomainObjectContainer<Distribution>. I wonder how that actually works as intended, but I guess that is some Groovy magic I forgot about.

Maybe it would be better to have it actually as NamedDomainObjectContainer<Distribution> and downcast it internally if needed or something like that.

But besides that, the usage you showed is exactly what you wnated to achieve, isn’t it?
So you are happy now, right?

Well, maybe besides that fact that org.jreleaser.model.Distribution.DistributionType is not resolvable.

You’ve lost me with this comment. What does it mean?

Hm, must be a problem with composite build in IntelliJ.
If I use the released version it is resolvable.

Which is why I was confused. It it wasn’t resolvable the the build file wouldn’t compile at all.