This spec contains stories and describes improvements to be made to how we “enhance” objects for usage in the DSL.
Objects designed for use in the DSL get enhanced with extra functionality to make their use more convenient and to support Gradle idioms. This enhancements provide:
- Extra properties
- Extensions
- Implicit type coercion when calling methods
- “set methods” (foo(String s) variant for setFoo(String s))
- Closure overloads for Action methods
- Dynamic properties (deprecated in favor of extra properties)
- Convention plugins (predecessor to extensions)
- Convention mapping (lazily derived values)
Enhancement is currently done by dynamically generating subclasses at runtime using ASM.
This spec has some crossover with the spec on improving the documentation.
For API such as:
class MyTask extends DefaultTask {
File someFile
}
The user should be able to set this value flexibly:
myTask {
someFile "project/relative/path"
}
Instead of the long hand:
myTask {
someFile project.file("project/relative/path")
}
There are two user oriented views of this feature:
- The user trying to set a file value
- The build “plugin” author accepting a file value from the “user”
This type coercion should be entirely transparent to group #2 and simple and predictable for group #1. That is, for any object exposed
in the DSL, a user should be able to set File
properties of objects using different (well defined) types.
There are other potential coercions, such as string to enum value.
Story: A DSL user specifies the value for a file property with a value supported by project.file() (no deferred evaluation)
A user sets a value for a file property, using a value that can be converted to a File
via project.file()
. No deferred evaluation is supported.
That is, all values are coerced immediately. This will be targeted at DSL (or more precisely, Groovy) users. This story does
not include a static Java friendly API for this functionality.
Only property setting is covered, so only setters will be decorated. Arbitrary methods that accept a File are not covered by this story.
Every DSL object currently lives in one of the following scopes. The scope associated with a DSL object determines the strategy for resolving a relative path to
a File
:
- Project scope: all objects owned by a project, including tasks, project extensions and project plugins. Relative paths are resolved using the project directory as the base directory.
- Gradle scope: all objects owned by a gradle object. Relative paths are not supported.
- Settings scope: all objects owned by a settings object. Relative paths are resolved using the settings directory as the base directory.
- Global scope: everything else. Relative paths are not supported.
Implementation-wise, the scope of a DSL object is encoded in the services that are visible to the object. In particular, a FileResolver
is available to each object
that is responsible for coercion toFile
. This story does not cover any changes to the above scopes.
Currently, every type is potentially convertible as the project.file()
coercion strategy includes a fallback of toString()
'ing any object and using its string representation as a file path. However, this has been deprecated and scheduled for 2.0 removal. Implicit coercion needs to initially support this fallback strategy but issue a deprecation warning similar to project.file()
.
- Documentation that states that it is possible to assign different types of values to
File
implicitly in the documentation. There is also no new API or change required by plugin/task etc. authors to leverage this feature. - Update 'writing custom tasks' user guide chapter and sample to make use of this feature.
The produced error message should indicate:
- The object that the property-to-be-set belongs to
- The name of the property-to-be-set
- The string representation of the value to be coerced
- They type of the value to be coerced
- A description of what the valid types are (including constraints: e.g. URI type values must be of the file:// protocol)
Values that cannot be coerced:
null
- An empty string (incl. an object whose
toString()
value is an empty string) - An effectively relative path where the target object has no "base" and can not resolve that path relative to anything
- A URL type value where the protocol is not
file
- A URL type value where the URL is malformed
- An object that will be coerced via its
toString()
representation where thetoString()
method throws an exception - An object that will be coerced via its
toString()
representation where thetoString()
method returns a value that cannot be interpreted as a path (will involve researching what kind of string values cannot be used as paths byFile
)
- User tries to assign relative path as String to File property (and is resolved project relative)
- User tries to assign absolute path as String to File property
- Variants on #1 and #2 using other values supported by Project.file()
- User tries to assign value using the =-less method variant (e.g. obj.someFileProperty("some/path"))
- User tries to assign value using a statically declared
setProperty(String, Object)
method (Task.setProperty(), Project.setProperty()) - User tries to assign value using Gradle's
DynamicObject
protocol (e.g. Project.setProperty())
High level:
- Add a type coercing
DynamicObject
implementation. - In
ExtensibleDynamicObject
(the dynamic object created in decorated classes), wrap the delegate dynamic object in the type coercing wrapper.
Detail:
A complicating factor is that the type coercing dynamic object must be contextual the scope that the object belongs to. It is not straightforward to “push the scope” down to the ExtensibleDynamicObject which will create the coercing wrapper. We have the same problem with pushing down the instantiator to this level to facilitate extension containers being able to construct decorated objects.
A new type will be created:
interface ObjectInstantiationContext {
ServiceRegistry getServices()
}
When a decorated object is created, there will be a thread local object of this type available for retrieval (like ThreadGlobalInstantiator
or AbstractTask.nextInstance
). MixInExtensibleDynamicObject
will read the thread global ObjectInstantiationContext
and use it when constructing
the backing dynamic objects.
Initially the coercion will be implemented by fetching a FileResolver
from the instantion context service registry and using it to coerce the value.
The "type coercing wrapper" will be implemented as a DynamicObject
implementation that can wrap any DynamicObject
implementation. It will intercept methods and identify coercions based on given argument types and parameter types of the methods provided by the real dynamic object.
Story: User reading DSL guide understands where type coercion is applicable for File
properties and what the coercion rules are
Story: Build logic authors (internal and third party) implement defined strategy for removing old, untyped, File setter methods
Having flexible file inputs is currently implemented by types implementing a setter that takes Object
and doing the coercion internally.
Having file type coercion implemented as a decoration means that such setters should be strongly typed to File
.
The strategy will be to simply overload setters and set methods with File
accepting variants, and @Deprecated
ing Object
accepting variants.
Story: A static API user (e.g. Java) specifies the value for a file property with a value supported by project.file() (no deferred evaluation)
Currently, object enhancement is a function of where/how it is used. This story makes enhancement opt-in in that types must explicitly declare what enhancements they want. This declaration can be used by tooling, such as our documentation generator or IDEs trying to provide content assistance.
This story also encompasses breaking up the enhancement from being all or nothing into discrete bits. Objects may need type coercion and other syntactic conveniences, but not require extensibility
Our current enhancement strategy is based on dynamically generating subclasses at runtime. This has the following drawbacks:
- Causes strange behaviour with access to private methods
- Requires reflective instantiation
- Requires pushing an Instantiator service around so enhanced objects can be created
This story makes enhancement a class load time function.
Currently, we ask objects if they can respond to a method/property invocation before actually proceeding with the invocation. This causes code duplication as typically the determination happens in the check and the actual invocation.
This story is about changing the protocol from being “Can you do this? Yes? Go ahead then.” to “Please do this and tell me if you can't”.
This will avoid code duplication and is potentially more efficient.
When calling methods (incl. setting properties) on enhanced objects with files representing relative paths, the target should receive an absolute file. It should be resolved to the logical base (e.g. project dir for a task). This should work when the caller is Java.
In order to achieve:
def foo = // create a Foo
foo {
bar {
}
}
We have to write:
class Foo {
Bar bar
void bar(Action<? super Bar> action) {
action.execute(bar)
}
}
This story is about avoiding the need to write the action accepting method in order to achieve this behaviour.