What is the best way to hold references to a TaskP...
# plugin-development
l
What is the best way to hold references to a TaskProvider, allow groovy to configure it. And have intellij know its type correctly? If I use
tasks.named(name, type){}
Intellij correctly determines the closure type. When held in a variable it does not. See attached screenshot Both configuration handlers work (the json is printed twice) However when i try to do the same thing that tasks.named does with my own code I run into issues. Long story short, I am trying to write a helper method that registers multiple tasks.
Copy code
this.json = tasks.register(name + "Json", InstallerJson.class);
public TaskProvider<@NotNull InstallerJson> getJson() {
    return this.json;
}
public void json(Action<? super InstallerJson> action) {
    getJson().configure(action);
}
When I do:
Copy code
inst.json {
  prop = value
}
IntelliJ sees the argument type correctly, but I get
Could not set unknown property 'prop' for root project 'root' of type org.gradle.api.Project.
When I do:
Copy code
inst.json.configure {
  prop = value
}
IntelliJ says the argument is of type
T
but it executes correctly. Any suggestions on how to make both sides happy?
v
Do not create the value for
inst
yourself but let Gradle create it. One of the magic things Gradle does is creating
Closure
overloads for
Action
taking methods that behave like you expect. With
Copy code
abstract class InstallerJson extends DefaultTask {
   @Input
   abstract Property<String> getProp()

   @TaskAction
   void execute() {
   }
}
class Inst {
   final TaskProvider<InstallerJson> json
   @Inject
   Inst(TaskContainer tasks) {
      this.json = tasks.register('name' + 'Json', InstallerJson.class)
   }
   TaskProvider<InstallerJson> getJson() {
      return this.json
   }
   void json(Action<? super InstallerJson> action) {
      getJson().configure(action)
   }
}
def inst = new Inst(tasks)
you have to use
it
as the
Action
method is used, so you need to do
Copy code
inst.json {
   it.prop = 'value'
}
By changing
def inst = new Inst(tasks)
to
def inst = objects.newInstance(Inst)
, you can successfully do
Copy code
inst.json {
   prop = 'value'
}
Otherwise you probably need an own
Closure
overload, but you should really always let Gradle instantiate things, especially things that might be used by a consumer in a build script. You then also for example automatically have
ExtensionAware
implemented (and best is to declare it explicitly so that non-Gradle consumers do not need to cast the instance) and can inject various Gradle services you might need, or let Gradle provide various abstract method or field implementations like for
Property
properties and so on.
l
Is there any documentation on what exactly that magic function adds to the class? Changing from a record, to a normal class, and using object.newInstance did indeed solve the immediate issue, but I'd like to know why. Coming from actual Java its very weird to have to run everything through a magic function. Luckally a lot of what im doing is just helper methods that dont take Closures. But if I do run into a case where I want that the option would either bet to refactor every case where that object is created to go through newInstace so I can get access to ObjectFactory or figure out how to implement the magic Closure method myself. I would much prefer the second option. I'm also assuming there is no static instance of ObjectFactory (or other services) available anywhere. Is this correct? It also doesn't explain why intellij doesn't like the first configure call in my screenshot.
v
Is there any documentation on what exactly that magic function adds to the class?
None I'm aware of, but for best developer and user experience you should always use it. Some things I have in mind that it does: • Implement
ExtensionAware
so that you can set
ext
/
extra
properties on it, and add other extensions to dynamically extend the DSL. This is done even if you don't declare
ExtensionAware
but best is you do so that someone that wants to use it does not need to cast it • Care for proper
Closure
usage of
Action
methods if not done manually • Inject various services like
ObjectFactory
,
Project
,
ProviderFactory
,
TaskContainer
, and many more (no complete list available, doc bug pending) • Implement for you
Property
properties and friends (managed types)
Changing from a record, to a normal class, and using object.newInstance did indeed solve the immediate issue, but I'd like to know why
Because the decoration automatically adds a
Closure
overload if no manual one is present so that
Action
methods are conveniently usable by Groovy consumers.
Coming from actual Java its very weird to have to run everything through a magic function.
Gradle is written in Java and best way to write build logic also is Java, so this is just normal Java. It might not be an idiom that you are used to, but it still is pure Java. There are also many similar things like using decorators or using annotation processing to generate things and so on that are also all coming from pure Java.
or figure out how to implement the magic Closure method myself.
There is the internal
ConfigureUtil
that could be used for that iirc, but ... well ... it is internal so could break with any minor version update of Gradle. You can probably also just manually do it by writing the overload, handling it properly inside, maybe wrapping it in an
Action
that forwards to the closure and give it to the
Action
method or similar, adding IDE-helping annotations like
@DelegatesTo
so that the IDE knows to what type the Closure will delegate and thus provide proper IntelliSense, ... But it imho really is preferable to just follow the best practices and let Gradle instantiate stuff that eventually ends up in the build script of a user, also for
ExtensionAware
and so on, and then you can also not do it wrongly or forget it somewhere.
I would much prefer the second option.
Really, do yourself and your users a favor and use the best practice. 🙂
I'm also assuming there is no static instance of ObjectFactory (or other services) available anywhere. Is this correct?
Yes, none I'm aware of. And it is usually not necessary, as in best practice world, you can just let Gradle inject it.
It also doesn't explain why intellij doesn't like the first configure call in my screenshot.
I'm not sure, might be a bug somewhere. 🤷‍♂️
Changing from a record
Using a record in something that might end up in the build script is anyway a very bad idea, because in the record you will not have lazy types. In the DSL-surface you should practically only ever use
Property
and friends (
Property
,
RegularFileProperty
,
DirectoryProperty
,
ConfigurableFileCollection
, ...), especially with properties that are only needed at execution time, so that a user can properly wire in other `Provider`s, so that you get out of the typical
afterEvaluate
ordering problems, timing problems, and race conditions, because one can properly wire `Provider`s to `Property`s and also preserve task dependencies that way if used properly. So for example instead of
Copy code
public record Type(String prop) { }
you would have
Copy code
public interface Type {
   Property<String> getProp();
}
and that's it. You don't need to provide an implementation but just use
objectFactory.newInstance(Type::class)
and Gradle cares about the rest.
r
Somewhat tangentially related question, but is there a factory available for an empty task provider? It's not an available managed property type, but I'd like to make an empty reference to a task in my DSL that's set to a registered task provider later in evaluation
v
Just use a
Property<Task>
?