-
Notifications
You must be signed in to change notification settings - Fork 193
DSL Descriptors
The Groovy language is an excellent platform for the easy creation of domain specific languages (DSLs). However, these DSLs are not directly supported by the Groovy editor. When DSLs are used heavily, standard IDE features like content assist, search, hovers, and navigation lose their value. For a while now, it has been possible to write an Eclipse plugin to extend Groovy-Eclipse, which requires specific knowledge of the Eclipse APIs. This is no longer necessary. Creating a DSL descriptor for a DSL is a way to make DSLs become first class citizens of Groovy-Eclipse.
As an example, Joachim Baumann describes how to implement a simple DSL for working with distances. Using this DSL, you can write things like this to calculate the total distance travelled:
3.m + 2.yd + 2.mi - 1.km
This is a simple and expressive DSL, but when you type this into a Groovy Editor in Groovy-Eclipse:
You see underlines and no hovers. Using a DSLD, it is possible teach the editor some of the semantics behind these custom DSLs. It is also possible to provide documentation hovers:
This document will describe how to extend Groovy-Eclipse through DSLDs. Note: Larger examples can be found at DSLD Examples and Writing Groovy DSL descriptors (DSLD) for Eclipse.
In Groovy-Eclipse 2.5.2, we have introduced quite a few new features. A brief description of these new features are in this section, and they are described in more detail below, mixed in with the rest of the documentation
For debugging and logging inside of DSLDs, there is now the log method. Like println, the log method takes a (g)string. Instead of printing to sysout, it prints the string to the Groovy console if it is open. log is a no-op method if the console is closed.
Previously, supportsVersion
prevented the script from running if the version test had failed. Now, it returns a boolean and is therefore suitable for use inside of if statements so that different functionality can be run depending on the current version. assertVersion
now behaves the way that supportsVersion
used to. It will prevent the script from being applied.
The delegatesTo
operation has become more flexible. It now accepts a set of named arguments, including type
, asCategory
, useNamed
, except
, and isDeprecated
.
Here is an example:
delegatesTo type: 'com.foo.MyCategory', asCategory: true, except: ['ignoreMethodA', 'ignoreMethodB']
This snippet will add all methods from MyCategory
to the current type as category methods. In other words, MyCategory
is treated like a category. Only static methods whose first argument type matches the current type will be included in the delegation. And the except
clause will exclude all methods named in the list. This is useful for when these particular methods are handle by some other means.
The old style of calling delegatesTo
with a single type name is still applicable.
The use of accept
has been deprecated and is replaced by contribute
, with a slightly different syntax. Here are some examples:
New:
contribute(currentType('com.foo.Sumthin')) {
// contributions here
}
Old:
currentType('com.foo.Sumthin').accept {
// contributions here
}
The accept
style of connecting a pointcut to a contribution group is still available, but may go away in future versions.
A method
contribution now recognizes three kinds of parameters: regular (aka positional), named and optional. Regular and named parameters appear in content assist; optional do not. Named and optional parameters are inserted with the parameter name prefixed.
Here is an example:
method name: 'meth',
params: [first: String, second: 'foo.MyObj'],
namedParams: [third: Integer, fourth: Long],
optionalParams: [fifth: 'foo.Optional', sizth: 'foo.OtherOptional']
Note: This new feature replaces the (now deprecated) useNamedArgs
parameter.
Quite a few new pointcuts have been introduced. Some are fairly restrictive in the locations they can be used.
-
hasArgument
: available inside ofenclosingCall
andenclosingMethod
to match on declared arguments; useful to combine with thetype
,name
,value
andannotatedBy
pointcuts -
hasAttribute
: available inside ofannotatedBy
to match on attributes; useful to combine with thetype
,name
andvalue
pointcuts -
type
: available to match on the declared type of method arguments, annotation parameters, field types and method (return) types -
value
: available to match on the value of an annotation -
isThisType
: supersedes thecurrentTypeIsEnclosingType
pointcut, which is now deprecated
It is now possible to mark contributions as deprecated. In the UI, these contributions will be displayed with a line running through them (just like other kinds of deprecated references).
There is a new isDeprecated
argument available to method
, property
, and delegatesTo
contributions.
To create a DSLD file inside of Eclipse, go to File -> New -> Groovy DSL Descriptor.
Choose a location if a suitable one has not been selected by default. In order to get content assist and hover support for your DSLD, it is recommended to choose a location inside of a source folder, but this is not required.
Groovy-Eclipse's DSL support will process all DSLD files that are in the project as well as all DSLD files that are on the project's classpath in a package named DSLD (this could be in a jar file an external class folder, or in coming from another project). Note that each project has its own set of DSLD files and they are not shared unless there is an explicit dependency and DSLD files are placed in the dsld
package. In the future, we will have a single location where global scripts can reside, but this is not yet implemented.
To see what scripts are currently available for each project, you can go to Preferences -> Groovy -> DSLD. Here, you can see a list of all projects and their DSLDs:
Each file can be disabled by deselecting it from the tree viewer. Also, the set of DSLD files can be refreshed (i.e., the current set of files are dropped and the entire workspace is searched again for DSLD files) and all scripts can be recompiled from this page using the buttons on the right. In the normal workflow, these last two options should not be necessary to use, but if you are seeing problems with your DSLDs, then you may want to try use them.
For development work on DSLDs themselves, it is strongly recommended that you place them in a source folder. This will give you editing features like hover support and content assist, and additionally, many syntax errors will appear in the Problems view. If you want to consume a DSLD in your project, then it is not necessary to place them in a source folder.
Lastly, when doing DSLD work, it is recommended that you open up both the Groovy Event Trace Console and the Eclipse Error Log. If there are syntax errors or other kinds of problems with your script, they will be printed to the Groovy Event Trace Console. If there are problems with the DSL infrastructure itself, there will be entries in the error log (these exceptions are likely bugs in Groovy-Eclipse and should be reported to the mailing list or in the issue tracker).
When doing DSLD work, it is recommended that you place your DSLDs in a source folder. Also, you will benefit by opening up both the Groovy Event Trace console and the Eclipse Error Log view. You may note that these .dsld files will be decorated with a small slashed circle. This comes from the Groovy Script Folders setting in Window > Preferences > Groovy Compiler and is perfectly fine.
However, when consuming existing DSLDs, they do not need to be in a source folder. They can be in any folder in the project. Alternatively, DSLDs can live in jars or class folders in the dsld
package. If this jar or class folder is on the classpath project, then these DSLDs will be used by the project (but only if they are in the dsld
package).
The DSLD language is an aspect-oriented domain specific language. The main components of a DSLD script are:
A query that describes a set of Groovy expressions in a program, for example the following is a simple pointcut that "matches" for all expressions whose the type is a subtype of Number.
currentType(subType(Number))
A code block that describes the extra properties and methods available when a pointcut matches. Contribution blocks are analogous to advice in AspectJ. A pointcut is ignored unless it is associated with a contribution block (and vice versa). Here is a simple contribution block:
{
property name: 'cm', type: 'Distance', doc: """A <code>cm</code> from <a href="http://joesgroovyblog.blogspot.com/2007/09/and-miles-to-go-before-i-sleep.html">http://joesgroovyblog.blogspot.com/2007/09/and-miles-to-go-before-i-sleep.html</a>"""
}
This block adds a new property called cm with a type of Distance. The javadoc is specified in the GString and can use standard HTML tags. However, without being associated with a pointcut, this contribution block will be used and will not add the method to any type.
To associate a pointcut with a contribution block, you call the contribute method. For example, this adds the cm property to all Number
s:
contribute(currentType(subType(Number))) {
property name:"cm", type:"Distance", doc:
"""A <code>cm</code> from <a href="http://joesgroovyblog.blogspot.com/2007/09/and-miles-to-go-before-i-sleep.html">http://joesgroovyblog.blogspot.com/2007/09/and-miles-to-go-before-i-sleep.html</a>"""
}
The contribute
method takes a pointcut and a closure. Inside that closure, you can specify a set of methods or properties that should be added to the type of the expression being analyzed.
When this script is copied into a Groovy project, cm
will be included in content assist of all Number
s and their subclasses. Also, references to cm
on Number
s will not be underlined as we showed in the example above. Finally, hovering over these references will display the doc defined in the contribution block.
The pointcuts and contribution blocks are used by Groovy-Eclipse's inferencing engine. The inferencing engine walks the AST and delegates the calculation of expression types to type lookup objects. The type lookups have some capacity for sharing state. DSLD is implemented as a type lookup.
There are two phases for DSLD files.
- The initial compile and processing: at startup or whenever a DSLD file is added or changed, the file is (re-)compiled and the resulting script is executed. Calls to
contribute
will store the pointcut and its associated contribution block in a table per project. - Invocation by the inferencing engine: for each expression in the file, each pointcut stored by the project is evaluated. If any pointcut evaluates to true, then that pointcut's contribution block(s) is evaluated and all contributions are added to the current type being evaluated.
We previously showed the core of the DSLD language, which we can now describe slightly more formally in terms of a join point model. As defined by Masuhara, et al, a Join Point model requires 3 things:
- A set of reference points that can be referred to; these are the join points.
- A means of identifying (a set of) join points; these are the pointcuts.
- A way to specify semantics at a particular set of join points; such as through advice, intertype declarations, or contribution blocks.
The DSLD language uses a syntactic join point model, where the join points are expression nodes in a Groovy abstract syntax tree (AST). DSLD affects the edit-time semantics of a program by contributing properties and methods to types in the IDE. This is different from most other AOP languages that affect a program's the semantics at runtime or compile time.
The individual components of a join point model are matched to concepts in the DSLD language as follows:
- The join points are expression AST nodes in a Groovy file
- DSLD defines a pointcut language to specify a sub-set of expression node join points
- And Contribution blocks augment the semantics of Groovy-Eclipse's inferencing engine to be enhanced with new type suggestions
In this section, we go into more detail on the DSLD language and document all of its features.
There are a fixed set of standard pointcuts which can be composed using '&' or '|' and negated using '~'. All pointcuts define their own javadoc and will be displayed when hovered over (but only if the DSLD meta script is available in the current project). Pointcuts are generally self documenting using content assist and hovers inside of the DSLD file, but here we describe the more important ones.
The parts of a pointcut are as follows:
- Arguments: in the DSLD script the text inside of a pointcut's '( )' are the arguments. Most pointcuts take one or zero arguments.
- Evaluation arguments: during evaluation, a pointcut is passed a value to match on. This value is called the evaluation argument. For example,
currentType(annotatedBy(Delegate))
,currentType
is the top-level pointcut and is always passed in the current type as a ClassNode object.annotatedBy
is passed in what is matched by its surrounding pointcut, in this case it iscurrentType
. - Return values: the value returned when a match happens. This is typically, a collection of AST nodes. These return values can be bound to names that are available in a contribution block.
When a match is found, all pointcuts return the objects that are matched in a java.util.Collection
object. For consistency, a collection is returned even when a single object is matched.
These are pointcuts that are directly dependent on the inferencing engine. All semantic pointcuts are top level pointcuts only and expect the current type as a class node as an evaluation argument.
Matches if the current type matches the pointcut argument. The type erasure is used, so the match will be the same if either List<String>
or List<Class>
are passed in.
Expects a String
, Class
, or ClassNode
corresponding to the type to match against. Alternatively, a filtering pointcut can be used.
Always a top-level pointcut.
The current type as a singleton ClassNode
collection.
Example:
currentType() // matches always
currentType(GString) // matches if the current type is a GString
currentType(subType(Number)) // matches if the current type is a sub type of java.lang.Number
Matches when the current type is the same as the enclosing type. In other words, the current type is this.
None
Always a top-level pointcut.
The current type as a singleton ClassNode
collection.
isThisType()
(Deprecated, use isThisType
instead.) Matches when the current type is the same as the enclosing type.
None
Always a top-level pointcut.
The current type as a singleton ClassNode
collection.
currentTypeIsEnclosingType()
In the following code, this pointcut will match on this
and x()
, but not substring
:
class Foo {
def x() { '1' }
def y() { this.x().substring(0) }
}
Matches when the source folder that the current type is declared in the source folder of the argument. This pointcut does not match for binary types.
A string specifying the source folder to match on.
Always a top-level pointcut.
The source folder as a singleton string.
Matches when the current type is defined in src/main/groovy:
sourceFolderOfCurrentType('src/main/groovy')
Matches on all source types:
sourceFolderOfCurrentType()
These are pointcuts that are typically used as arguments to other pointcuts and can further filter a result to refine what exactly matches. They can be used as a top-level pointcut, but in this case they are implicitly passed in the current type.
Matches on a set of fields, methods, or properties.
A string corresponding to the field/method/property name, or another filtering pointcut to further refine the match.
These pointcuts expect either a ClassNode or a Collection of class members. If passed in a ClassNode, then all of the fields/methods/properties will be extracted to match against.
Returns a collection of fields/methods/properties that have the characteristics specified by pointcut argument.
Matches if the current type has any fields:
currentType(fields())
Matches if any method in the current type is named "myMethod". Returns all matched methods as a collection:
currentType(methods('myMethod'))
Matches if the current type is a sub type of a class that defines a method "myMethod":
currentType(subType(methods('myMethod')))
Note that currentType
is optional and if omitted, then the current type is implicitly passed in.
Matches on constructors.
A string corresponding to the constructor signature (comma-separated simple type names), or another filtering pointcut to further refine the match.
These pointcuts expect either a ClassNode or a Collection of constructors. If passed in a ClassNode, then all of the constructors will be extracted to match against.
Returns a collection of fields/methods/properties that have the characteristics specified by pointcut argument.
Matches if the current type has a constructor that accepts an int
(note: currentType
is optional):
currentType(hasConstructor('int'))
Matches if the current type has a constructor that has a parameter named "value":
currentType(hasConstructor(hasArgument('value')))
Matches if the current type has a constructor that has a parameter of type "java.util.Map" tagged with "@groovy.transform.NamedParam":
currentType(hasConstructor(hasArgument(type(Map) & annotatedBy(NamedParam))))
Matches if the enclosing type declaration has a constructor that is public:
enclosingClass(hasConstructor(isPublic()))
Matches if the type passed in as an evaluation argument is a sub type of the pointcut argument.
A String
, Class
, or ClassNode
corresponding to the type that the current type will be checked against.
A ClassNode, collection of ClassNodes to match against. Alternatively, a MethodNode, FieldNode, PropertyNode, or collection of them can be matched against. In this case, the declaring type of the declarations will be used.
A collection of super-types that match the conditions described in the pointcut argument.
Matches when the current type is a sub type of something annotated by @Delegate
:
currentType(subType(annotatedBy(Delegate)))
// or
subType(annotatedBy(Delegate))
Matches when the enclosing type is a subtype of TestCase
:
enclosingClass(subType('junit.framework.TestCase'))
Matches when one or more of the evaluation arguments is declared with the given modifier.
None.
Expecting a collection of AnnotatedNode
s.
A subset of the evaluation arguments that all have the given modifier.
Matches if the current type has any static fields:
fields(isStatic())
Matches when the evaluation arguments correspond to the pointcut argument.
A String
, Class
, or ClassNode
corresponding to the annotation to match. Alternatively, another pointcut can be used to further constrain the match.
A collection of AnnotatedNode
s.
A collection of all AnnotationNode
s matched inside of the evaluation arguments.
Matches when the current type is annotated by @Deprecated
:
annotatedBy('java.lang.Deprecated')
Matches when the current type has any field with any annotation
fields(annotatedBy())
Matches when the current type is annotated by an annotation with the @Retention
meta-annotation:
annotatedBy(annotatedBy('java.lang.annotation.Retention'))
Matches when an annotation passed in has an attribute with the appropriate name, type, or value.
A string corresponding to the name of the annotation to match, or a combination of name
, type
, and value
pointcuts to match against.
A collection of AnnotationNode
s.
The value or values of the attribute(s) that are matched as a collection. Values that are constants will be reified so that the constants will be available to the DSLD as primitives and strings.
Matches if a field in the current type is annotated by an annotation with an attribute called "value".
currentType(field(annotatedBy(hasAttribute('value'))))
Matches if a field in the current type is annotated by an annotation with an attribute called "value" and this attribute's value is true
.
currentType(field(annotatedBy(hasAttribute(name('value') & value(true)))))
Matches if a field in the current type is annotated by an annotation with an attribute called "value" and this attribute's type is java.lang.Boolean
. Additionally, the value of the attribute is bound to val
.
currentType(field(annotatedBy(hasAttribute(name('value') & type(Boolean) & bind(val: value(true))))))
Matches on the type of a field, argument, parameter, annotation attribute, or method return type. See hasAttribute
and hasArgument
for examples.
Matches on the declaring type of a field, method, or property. See enclosingCall
and hasArgument
for an example.
Matches on the value of an argument or annotation attribute. See hasAttribute
and hasArgument
for examples. If the matched value is bound to a name, then the value is reified. This means that the value can be used inside of the DSLD as a primitive or a string. For example:
contribute(enclosingCall(hasArgument(name('flar') & bind(vals: value())))) {
vals.each { val ->
if (val > 9) {
log 'val is greater than 9'
} else {
log 'val is less than or equal to 9'
}
}
}
In the example above, the bound values can be directly compared to primitives or strings in the DSLD.
Matches on the fully qualified name of an element.
A string corresponding to the name to match
A collection of any objects can be passed in. The pointcut argument will be matched against the name of the AST node if a method, field, property, or class declaration. Else, toString()
will be called on the evaluation argument.
The name or names that were matched as a collection.
Matches if the name of the current type is "p.Bar" and it has a method named "foo":
currentType(name('p.Bar') & methods('foo'))
These are pointcuts that depend on the lexical structure of the surrounding AST.
Matches when the enclosing type is a script/class.
A String
, Class
, or ClassNode
corresponding to the type name to match on. Alternatively, a pointcut argument can be used.
These pointcuts are top-level only.
The type that was matched.
Matches when inferring inside of a script:
enclosingScript()
Matches when inferring inside of a script named "pack.MyScript":
enclosingScript('pack.MyScript')
Matches when inferring inside of a class that has a @Singleton
annotation:
enclosingClass(annotatedBy(Singleton))
Matches when enclosed by a field or method of the given characteristics. A code block is enclosed by a field if it is inside of a field initializer. For example, the print statement is enclosed by the x
field:
class F {
def x = { print 'I am enclosed!' }
}
A string, Class, or ClassNode corresponding to the type name to match on. Alternatively, a pointcut argument can be used.
These pointcuts are top-level pointcuts only.
A singleton collection of the enclosing method/field if the current location matches.
Matches when the enclosing method or field has the given annotation:
enclosingMethod(annotatedBy('com.foo.MyAnnotation')) | enclosingField(annotatedBy('com.foo.MyAnnotation'))
Matches on the enclosing method call. The enclosing method call is the method call of which the current expression is an argument. There are multiple enclosing calls if the current expression is nested inside of multiple method calls (e.g., foo(bar(arg))
).
Inner pointcuts are required. The inner pointcuts accepted are: name
to match on the method name, type
to match on the return type, declaringType
to match on the method's declaring type, and hasArgument
to match on the method call's arguments.
This pointcut is top-level only.
The method call expression as a Groovy AST node.
Matches when the enclosing method call is named "myMethod" and the declaring type is Bar and the call has a named argument of either "first" or "second":
enclosingCall(name('myMethod') & declaringType('com.foo.Bar') & (hasArgument('first') | hasArgument('second')))
Matches on an argument to the enclosing method call or the enclosing method declaration. Note that when this pointcut is used to match against a method declaration, the value
inner pointcut cannot be used.
If a string is passed as the pointcut argument, then the match is against the name of the parameter. Alternatively, any of name
, value
, and type
can be used as enclosed pointcuts.
This pointcut must be used inside of enclosingCall
or enclosingMethod
.
The value of a named argument if matching on a named argument in a method call, the variable expression if matching on a positional argument in a method call, otherwise the parameter if matching on a method declaration.
Matches when the enclosing method call is named "myMethod" and the declaring type is "com.foo.Bar" and the call has a named argument of "first" whose type is "java.lang.String". Additionally, binds vals
to a collection containing the value of the named argument.
enclosingCall(name('myMethod') & declaringType('com.foo.Bar') & hasArgument(name('first') & type(String) & bind(vals: value())))
Matches when the enclosing method declaration is named "myMethod" and the declaring type is "com.foo.Bar" and the call has a named argument of "first" whose type is "java.lang.String". Note that here, the value
pointcut is not applicable since there is no value to match on.
enclosingCall(name('myMethod') & declaringType('com.foo.Bar') & hasArgument(name('first') & type(String)))
Matches when the enclosing method call has the given name or the given type. The enclosing method call is the method call of which the current expression is an argument. For example:
myVariable.someMethod(bar, baz) {
it.otherMethod(inside)
}
In the code above, bar
and baz
are enclosed by the someMethod
method call. Additionally, all expressions in the closure are enclosed. The reference to inside
is enclosed by two method calls: someMethod
and otherMethod
.
enclosingCallName
takes a string corresponding to the name of the method call. enclosingCallDeclaringType
takes a string, Class, ClassNode, or other pointcut to describe the type to match on.
This pointcut is top-level only.
Returns all enclosing method name/declaring types as an ordered set. The ordering is from the lexically closest call to the furthest. In the example above, the ordering would be otherMethod and then someMethod.
Matches when enclosed by a method call foo
declared by com.bar.MyClass
:
enclosingCallName("foo") & enclosingCallDeclaringType("com.bar.MyClass")
Matches when enclosed by a closure.
No arguments.
This pointcut is top-level only.
An ordered collection of ClosureExpressions corresponding to the lexically closest closure to the furthest. For example:
def constraints = {
tryThis {
print "Me"
}
}
In the previous code snippet, when inferring on the print method call, enclosingClosure would return two closure expressions: first the closure attached to tryThis
and then the closure attached to constraints
.
Here is a pointcut describing when inside of a SwingBuilder
closure:
enclosingClosure() & enclosingCallDeclaringType("groovy.swing.SwingBuilder") & enclosingCallName("edt")
Matches within a variable assignment.
None (to match all) or a string/pattern specifying the accepted assigned variable name(s) or a filtering pointcut.
This pointcut is top-level only.
The BinaryExpression corresponding to the lexically closest variable assignment. For example:
def constraints = {
tryThis {
print "Me"
}
}
In the previous code snippet, when inferring on the print method call, assignedVaribale
would return the expression def constraints = ...
.
Here is a pointcut describing when inside of a Newify
-transformed assignment:
assignedVariable(annos: annotatedBy(Newify))
A simple contribution that matches within all assignment expressions:
contribute(bind(exprs: assignedVariable())) {
Variable var = exprs[0].leftExpression
...
}
These are pointcuts that depend on the structure and naming conventions of the file system.
Matches on the Eclipse project nature of the current project.
A string corresponding to the desired project nature. Examples are:
- Grails:
com.springsource.sts.grails.core.nature
- GWT nature:
com.google.gwt.eclipse.core.gwtNature
This pointcut is top level only.
Returns the name of the nature as a singleton collection.
Matches domain classes in grails projects:
nature("com.springsource.sts.grails.core.nature") & sourceFolderOfCurrentType("grails-app/domain")
Matches on the full file name or on the file extension of the current file being inferred.
A string corresponding to the file name or extension (excluding the '.') to match.
This pointcut is top level only.
Returns the name of the file that was matched.
Matches on gradle files:
fileExtension("gradle")
Matches on MyScript.groovy
:
fileName("MyScript.groovy")
Matches on the source folder of the file currently being inferred. Note that this is different from sourceFolderOfCurrentType
that will match on the current type. sourceFolderOfCurrentFile
will return the same value for the entire file, whereas sourceFolderOfCurrentType
will have a different value depending on the type of the current expression.
A string corresponding to the source folder to match on.
This pointcut is top level only.
The matches source folder as a singleton collection.
Matches when in the src folder:
sourceFolderOfCurrentFile("src")
No need to explicitly invoke these pointcuts. They are included here for completeness and are implicitly created when using &
, |
, and ~
(meaning the and
, or
, and not
pointcuts respectively).
These are the only pointcuts that can take more than one argument. The arguments must be other pointcuts.
These pointcuts can take any evaluation arguments. They are simply passed on to their contained pointcuts.
For or
and and
, a collection of all elements matched by the containing pointcuts. For not
, this will return a singleton collection with an Object
value if the containing pointcut does not match.
It is generally good to use parens around the pointcut argument of not. Matches when the current type has no fields:
~(fields())
// or
not(fields())
Matches when inside a deprecated class, and there is a field or method in the class named "bar":
enclosingClass(annotatedBy(Deprecated)) & (fields("bar") | methods("bar"))
// or
and(enclosingClass(annotatedBy(Deprecated)), or(fields("bar"), methods("bar")))
Due to Groovy's operator precedence rules, parens must be used around not ('~') or else the ~
will apply to the pointcut name (without the parens), instead of the pointcut expression (with the parens). You should do this:
(~ isPublic() ) | name("java.lang.String")
instead of this:
~ isPublic() | name("java.lang.String")
There is only one binding pointcut, called bind
. This pointcut explicitly binds the return value to a name to make it available in a contribution block.
The explicit use of the bind
pointcut can be omitted when it is surrounded by another pointcut. There is a bigger discussion of this pointcut below.
A single pointcut
Any evaluation arguments. They are passed to the contained pointcut.
Returns the value returned by the contained pointcut.
Binds a collection of deprecated annotations:
fields(bind(dep: annotatedBy(Deprecated)))
// or
fields(dep: annotatedBy(Deprecated))
Binds to deprecated fields in the current type. In this case, bind is necessary:
bind(dep: fields(annotatedBy(Deprecated)))
It is possible to register your own pointcuts that will be available in the current DSLD script only. See the registerPointcut
section below.
The DSLD language is a work in progress. Here are some pointcuts that are not yet implemented, but may be based on the needs of the community: regex
, instanceof
, superType
, enclosingType
, enclosingEnum
, and others.
Even though the DSLD script is being edited in the context of your project, the script is actually loaded by Groovy-Eclipse. And so, the runtime classpath of the script corresponds to Groovy-Eclipse's classpath, rather than the classpath of your project.
Consequently, you cannot reference class objects for types defined in your project. However, you can reference class objects that are available to Groovy-Eclipse. This might be confusing since the compiler will not show compile errors when types defined in your project are referenced as class objects, but it will show compile errors when Groovy-Eclipse types are referenced. This is because the Groovy-Eclipse compiler works off of the project's classpath. It is not yet aware that DSLD files will be run with a different classpath.
More specifically:
- Instead of referencing the class
MyLocalType
directly, you can reference it as a String"com.mycompany.MyLocalType"
- Standard JDK, GDK, and all types defined in
groovy-all
are available directly in your DSLD and will show compile errors. - It is possible to reference types in packages beginning with org.eclipse.jdt. and org.codehaus.groovy.eclipse. if all references are fully qualified. However, this is not recommended unless you really know what you are doing.
Sometimes it is useful to assign pointcuts to variables. For example, here is how we might describe a Grails domain class and a controller class:
def grailsArtifact = { String folder ->
sourceFolderOfCurrentType("grails-app/"+ folder) &
nature("com.springsource.sts.grails.core.nature") & (~enclosingScript())
}
def domainClass = grailsArtifact("domain")
def controllerClass = grailsArtifact("controllers")
Notice how it is possible to use a closure so that pointcut components can be shared and parameterized.
We can use the domainClass
pointcut above as a component in a larger pointcut that describes where the Grails constraints DSL is applicable:
contribute(domainClass & enclosingField(name("constraints") & isStatic()) & inClosure() & isThisType() & bind(props: properties())) {
...
}
Let's break this down a bit:
- The first thing to notice is that the
domainClass
reference doesn't require and parens. This is because parens have already been used when the pointcut was first declared. - Next, notice the
enclosingField
pointcut. This component matches when the enclosing field name is "constraints" and the field is static. - Usually, the
name
pointcut is implicit and optional, but since&
requires pointcuts on either side, we need to wrap the "constraints" string inside of a poincut. - Next, the expression must be inside of a closure
- The
isThisType
pointcut means that the type of the current expression must be the type of the enclosing class. Thus, references tothis
will match, but references to other types (such asnew String()
will not).
Now that we have described the pointcut language, we can delve into what happens in contribution blocks.
You have already been introduced to the following form, which adds a property to the type of the expression matched in the contributing pointcut:
contribute(...) {
property name : "cm", type: Number, doc : "..."
}
The full form of property
is:
property name : "cm", type: "Distance", declaringType: Number, isStatic : false,
doc : "<b>Enter javadoc here</b> html is supported",
provider : "A readable name for your DSL (no html)"
A few notes:
-
name
is the only required argument -
type
defaults tojava.lang.Object
and the argument can be of typejava.lang.String
,java.lang.Class
, ororg.codehaus.groovy.ast.ClassNode
. -
declaringType
defaults to thecurrentType
and can accept aString
,Class
, orClassNode
-
doc
is the javadoc that will show up in hovers and accepts html syntax -
provider
is a human readable name for the current DSL and appears in content assist to give hints as to how the given completion proposal was calculated
The following methods are available (for completeness, we include property
again):
declares a new property. The full form is like this. Name is the only required field:
property name : "nameOfTask", type: String, declaringType: "java.lang.String", readOnly: true,
isStatic : false, isDeprecated: false, provider : "A readable name for your DSL",
doc : "<b>Enter javadoc here</b> html is supported"
declares a new method. The full form is like this. Name is the only required field:
method name : "nameOfTask", type: String, declaringType: "java.lang.String",
params : [ arg1 : String, arg2 : Class],
namedParams : [arg3 : Long, arg4 : Short],
optionalParams: [arg5: Byte], isStatic : false,
isDeprecated: false, // if true, then any uses of this method will have a line through it
noParens: false, // if true content assist will assume this is a groovy command chain expression and avoid using parens
??doc : "<b>Enter javadoc here</b> html and javadoc tags supported",
provider : "A readable name for your DSL"
A note on parameter kinds:
-
params
: regular parameters are added via content assist on the method -
namedParams
: named parameters are added via content assist on the method prefixed by a name. And if content assist is invoked after a paren or a comma (with no text prefix), unused named parameters will be suggested. -
optionalParams
: optional parameters are only available when performing completion after a paren or comma (with no text prefix). They are not included during normal method content assist.
Parameter names may include whitespace and special characters as long as the parameter is quoted. For example, it is possible to simulate varargs like this:
method name : "meth", params : ["... values", String]
returns parameter types for a given MethodNode
or ConstructorNode
for (ConstructorNode ctor : type.declaredConstructors.findAll { !it.isPrivate() }) {
method name: name, params: params(ctor), type: type, declaringType: type, isStatic: true
}
adds all of the public methods in the delegated type to the current type. For example:
delegatesTo List
will add all public methods of list to the current type for content assist, underlining, hovers, and navigation.
Adds all of the public methods in the delegated type to the current type. There is a possibility to parameterize using named arguments:
-
type
: (required) the type to delegate to. -
asCategpry
: (optional) if true, treat the delegated type as a category and only include static methods where the first parameter matches the type of the current type. -
useNamed
: (optional) if true, content assist will be applied using named arguments. -
except
: (optional) a list of method names to exclude from the delegation (useful if these methods are already being added through some other contribution). -
isDeprecated
: (optional) if true then this contribution is deprecated and all references to delegated methods will appear in the UI with a line through them.
For example:
delegatesTo type:List, asCategory:false, useNamed:false, except:["addAll", "removeAll"], isDeprecated:true
(deprecated, delegatesTo type:"com.Foo", useNamed:true
instead) similar to delegatesTo
, but uses named arguments when applying content assist proposals
sets the provider for the entire contribution block. Eg,
provider = "My Groovy DSL"
will ensure that "My Groovy DSL" appears in content assist next to all contributions added by this block.
Accesses the current Groovy AST node (an expression node)
Accesses the Groovy AST node (an expression node) that encloses the currentNode
. This is the parent node of the currentNode
in the AST.
A means to pass state between contribution blocks. An example is given below.
This list of available properties and methods will likely change as we continue to work on DSLD.
Sometimes, the items matched in the contributing pointcut are required in the contribution block. You can use named arguments for pointcuts to bind a name to the object that the pointcut matched. This binding is available inside of the contribution block.
For example, here is the DSL for the @Delegate
AST transform:
contribute(currentType(fields : findField(annotatedBy(Delegate)))) {
fields.each { FieldNode field ->
delegatesTo type:field.declaringType
}
}
The fields
argument is bound to all of the fields in the current type with the Delegate
annotation. If there is only one field, then the result is a single Groovy FieldNode
, if there are multiple matches, then fields is bound to an object of type List<FieldNode>
. As a general rule, bound variables are never null inside of a contribution block. If they were null, then that would imply that a match had not occurred.
Sometimes, it might be necessary to bind on the outermost pointcut component. In this case, you can use the bind() pointcut. For example, the following is the syntax for the @Singleton
AST transform
contribute( bind( type :currentType(annotatedBy(Singleton)))) {
method name:"instance", type:type, isStatic:true, declaringType:type,
doc:"Get the singleton instance of this Class"
}
To ensure that a script only runs when particular features are installed, you can use the assertVersion
top-level method. This method call should go at the top of a script to prevent any other part of the script from executing if the script is not supported.
The syntax looks like this:
assertVersion(component1:"x.y.z", component2:"a.b.c")
This means that the script is only active if all components are active with a version greater than or equal to the ones supplied. If anything does not match, then the entire script is disabled.
Currently, only 'groovy', 'groovyEclipse', and 'grailsTooling' are supported, but we may add other component kinds later. A real example is here:
assertVersion(groovy:"1.7.8",groovyEclipse:"2.1.3")
Alternatively, you can use the supportsVersion
top-level method for a similar purpose. This method returns a boolean (true iff the current version matches the version requirements in the method arguments). This allows you to execute different contribution blocks depending on version constraints. For example:
// Power asserts have changed packages between 1.7 and 1.8
if (supportsVersion(groovy:"1.8.0")) {
contribute(currentType("org.codehaus.groovy.runtime.powerassert.PowerAssertionError")) {
// contribute methods and properties
}
} else {
contribute(currentType("org.codehaus.groovy.transform.powerassert.PowerAssertionError")) {
// contribute methods and properties
}
}
It is possible to create and register a locally defined pointcut. You can do so by invoking the registerPointcut
closure at the top level of a DSLD file. registerPointcut
takes two arguments: the name of the pointcut to register and a closure that serves as the test to see whether or not the pointcut matches.
For example, the following defines twoArgs, a pointcut that matches when the evaluation argument is a MethodNode with two arguments with the proper names:
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.Parameter;
registerPointcut("twoArgs", {
if (it instanceof MethodNode) {
Parameter[] params = it.parameters
if (params?.length >= 2 && params[0].name == "firstName" && params[1].name == "lastName") {
return params.collectEntries{ [it.name, it.type] }
}
}
})
This pointcut can be used as follows:
contribute( (enclosingMethod(twoArgs() & name("processName")) & isThisType())) {
method name: "checkDatabaseForNames"
}
This will match on the following code:
def processName(String firstName, String lastName, String middleName) {
// in here
}
Notice that when there is a match, the return value is a collection of all of the matched parameter types. This return value can be bound to a name and made available in a contribution block, like so:
contribute((enclosingMethod(bind(paramNameTypeCollection : twoArgs()) & name("processName")) & isThisType())) {
for (paramNameTypes in paramNameTypeCollection) {
method name: "checkDatabaseForNames", params: paramNameTypes
}
}
Now, the generated method will have the same parameter names and types as the method declaration that encloses it. We need to iterate through paramNameTypeCollection
because all named bindings are collections.
There is more that you can do with registerPointcut
. The pointcut closure recognizes all named arguments that are passed in to it. For example:
registerPointcut("alsoTwoArgs", {
def result = null
if (!methods || !current) {
return null
}
// since current and methods are passed in as bindings, they are collections and need an iterator
ClassNode type = current?.iterator()?.next()
if (type) {
for (elt in methods) {
if (elt instanceof MethodNode) {
Parameter[] ps = elt.parameters
if (ps?.length >= 2 && ps[0].name == "firstName" && ps[1].name == "lastName") {
return "findProcessorForClass" + type.getNameWithoutPackage()
}
}
}
}
})
contribute(bind(names: alsoTwoArgs(methods: enclosingMethod("processName"), current: isThisType()))) {
for (name in names) {
property name:name, type:GroovyObject
}
}
This matches the following code:
class FullName {
def processName(firstName, lastName) {
this.findProcessorForClassFullName()
}
}
It is possible to pass state between contribution blocks using the wormhole. More about this later...
Since DSLDs are running in the same process as your current Eclipse is, there is some danger. Unless you really know what you are doing, some things that you should not try:
- Add
System.exit()
to any of your scripts (take a wild guess as to what will happen...) - Make any changes to global state
- Access
org.eclipse.core.internal.runtime.AdapterManager
or any other Eclipse singleton.
I think you get the point. We give you the power and you must decide what to do with it. Just like many parts of the Groovy eco-system, the DSLD language gives you all the rope you need to hang yourself.
In the future, it is likely that DSLDs will be executed in the context of a security manager to prevent these kinds of problems.