Skip to content

DSL Descriptors

Eric Milles edited this page Dec 31, 2022 · 16 revisions

The Problem

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:

Underlines screenshot

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:

No underlines screenshot

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.

New Features in Groovy-Eclipse 2.5.2

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

log expressions

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.

supportsVersion and assertVersion

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.

delegatesTo changes

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.

Use contribute instead of accept

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.

namedParams and optionalParams

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.

New pointcuts

Quite a few new pointcuts have been introduced. Some are fairly restrictive in the locations they can be used.

  • hasArgument: available inside of enclosingCall and enclosingMethod to match on declared arguments; useful to combine with the type, name, value and annotatedBy pointcuts
  • hasAttribute: available inside of annotatedBy to match on attributes; useful to combine with the type, name and value 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 the currentTypeIsEnclosingType pointcut, which is now deprecated

isDeprecated

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.

The infrastructure

To create a DSLD file inside of Eclipse, go to File -> New -> Groovy DSL Descriptor.

Groovy Wizards

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:

DSLD Preferences page

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).

Reminder

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).

An Overview

The DSLD language is an aspect-oriented domain specific language. The main components of a DSLD script are:

pointcuts

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))

contribution blocks

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 Numbers:

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 Numbers and their subclasses. Also, references to cm on Numbers 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.

Under the hood

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.

  1. 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.
  2. 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.

More formally

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:

  1. A set of reference points that can be referred to; these are the join points.
  2. A means of identifying (a set of) join points; these are the pointcuts.
  3. 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:

  1. The join points are expression AST nodes in a Groovy file
  2. DSLD defines a pointcut language to specify a sub-set of expression node join points
  3. And Contribution blocks augment the semantics of Groovy-Eclipse's inferencing engine to be enhanced with new type suggestions

A deep dive

In this section, we go into more detail on the DSLD language and document all of its features.

Pointcuts

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 is currentType.
  • 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.

Semantic pointcuts

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.

currentType

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.

Pointcut Arguments

Expects a String, Class, or ClassNode corresponding to the type to match against. Alternatively, a filtering pointcut can be used.

Evaluation arguments

Always a top-level pointcut.

Return values

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

isThisType

Matches when the current type is the same as the enclosing type. In other words, the current type is this.

Pointcut Arguments

None

Evaluation arguments

Always a top-level pointcut.

Return values

The current type as a singleton ClassNode collection.

Example

isThisType()

currentTypeIsEnclosingType

(Deprecated, use isThisType instead.) Matches when the current type is the same as the enclosing type.

Pointcut Arguments

None

Evaluation arguments

Always a top-level pointcut.

Return values

The current type as a singleton ClassNode collection.

Example

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) }
}

sourceFolderOfCurrentType

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.

Pointcut Arguments

A string specifying the source folder to match on.

Evaluation arguments

Always a top-level pointcut.

Return values

The source folder as a singleton string.

Example

Matches when the current type is defined in src/main/groovy:

sourceFolderOfCurrentType('src/main/groovy')

Matches on all source types:

sourceFolderOfCurrentType()

Filtering pointcuts

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.

field/method/property

Matches on a set of fields, methods, or properties.

Pointcut Arguments

A string corresponding to the field/method/property name, or another filtering pointcut to further refine the match.

Evaluation arguments

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.

Return values

Returns a collection of fields/methods/properties that have the characteristics specified by pointcut argument.

Example

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.

hasConstructor

Matches on constructors.

Pointcut Arguments

A string corresponding to the constructor signature (comma-separated simple type names), or another filtering pointcut to further refine the match.

Evaluation arguments

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.

Return values

Returns a collection of fields/methods/properties that have the characteristics specified by pointcut argument.

Example

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()))

subType

Matches if the type passed in as an evaluation argument is a sub type of the pointcut argument.

Pointcut Arguments

A String, Class, or ClassNode corresponding to the type that the current type will be checked against.

Evaluation arguments

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.

Return values

A collection of super-types that match the conditions described in the pointcut argument.

Example

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'))

isPublic/isPrivate/isSynchronized/isStatic/isFinal

Matches when one or more of the evaluation arguments is declared with the given modifier.

Pointcut Arguments

None.

Evaluation arguments

Expecting a collection of AnnotatedNodes.

Return values

A subset of the evaluation arguments that all have the given modifier.

Example

Matches if the current type has any static fields:

fields(isStatic())

annotatedBy

Matches when the evaluation arguments correspond to the pointcut argument.

Pointcut Arguments

A String, Class, or ClassNode corresponding to the annotation to match. Alternatively, another pointcut can be used to further constrain the match.

Evaluation arguments

A collection of AnnotatedNodes.

Return values

A collection of all AnnotationNodes matched inside of the evaluation arguments.

Example

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'))

hasAttribute

Matches when an annotation passed in has an attribute with the appropriate name, type, or value.

Pointcut Arguments

A string corresponding to the name of the annotation to match, or a combination of name, type, and value pointcuts to match against.

Evaluation arguments

A collection of AnnotationNodes.

Return values

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.

Example

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))))))

type

Matches on the type of a field, argument, parameter, annotation attribute, or method return type. See hasAttribute and hasArgument for examples.

declaringType

Matches on the declaring type of a field, method, or property. See enclosingCall and hasArgument for an example.

value

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.

name

Matches on the fully qualified name of an element.

Pointcut Arguments

A string corresponding to the name to match

Evaluation arguments

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.

Return values

The name or names that were matched as a collection.

Example

Matches if the name of the current type is "p.Bar" and it has a method named "foo":

currentType(name('p.Bar') & methods('foo'))

Lexical pointcuts

These are pointcuts that depend on the lexical structure of the surrounding AST.

enclosingScript/enclosingClass

Matches when the enclosing type is a script/class.

Pointcut Arguments

A String, Class, or ClassNode corresponding to the type name to match on. Alternatively, a pointcut argument can be used.

Evaluation arguments

These pointcuts are top-level only.

Return values

The type that was matched.

Example

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))

enclosingField/enclosingMethod

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!' }
}

Pointcut Arguments

A string, Class, or ClassNode corresponding to the type name to match on. Alternatively, a pointcut argument can be used.

Evaluation arguments

These pointcuts are top-level pointcuts only.

Return values

A singleton collection of the enclosing method/field if the current location matches.

Example

Matches when the enclosing method or field has the given annotation:

enclosingMethod(annotatedBy('com.foo.MyAnnotation')) | enclosingField(annotatedBy('com.foo.MyAnnotation'))

enclosingCall

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))).

Pointcut Arguments

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.

Evaluation arguments

This pointcut is top-level only.

Return values

The method call expression as a Groovy AST node.

Example

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')))

hasArgument

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.

Pointcut Arguments

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.

Evaluation arguments

This pointcut must be used inside of enclosingCall or enclosingMethod.

Return values

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.

Example

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)))

enclosingCallName/enclosingCallDeclaringType

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.

Pointcut Arguments

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.

Evaluation arguments

This pointcut is top-level only.

Return values

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.

Example

Matches when enclosed by a method call foo declared by com.bar.MyClass:

enclosingCallName("foo") & enclosingCallDeclaringType("com.bar.MyClass")

enclosingClosure

Matches when enclosed by a closure.

Pointcut Arguments

No arguments.

Evaluation arguments

This pointcut is top-level only.

Return values

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.

Example

Here is a pointcut describing when inside of a SwingBuilder closure:

enclosingClosure() & enclosingCallDeclaringType("groovy.swing.SwingBuilder") & enclosingCallName("edt")

assignedVariable

Matches within a variable assignment.

Pointcut Arguments

None (to match all) or a string/pattern specifying the accepted assigned variable name(s) or a filtering pointcut.

Evaluation arguments

This pointcut is top-level only.

Return values

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 = ....

Example

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
  ...
}

Structural pointcuts

These are pointcuts that depend on the structure and naming conventions of the file system.

nature

Matches on the Eclipse project nature of the current project.

Pointcut Arguments

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

Evaluation arguments

This pointcut is top level only.

Return values

Returns the name of the nature as a singleton collection.

Example

Matches domain classes in grails projects:

nature("com.springsource.sts.grails.core.nature") & sourceFolderOfCurrentType("grails-app/domain")

fileName/fileExtension

Matches on the full file name or on the file extension of the current file being inferred.

Pointcut Arguments

A string corresponding to the file name or extension (excluding the '.') to match.

Evaluation arguments

This pointcut is top level only.

Return values

Returns the name of the file that was matched.

Example

Matches on gradle files:

fileExtension("gradle")

Matches on MyScript.groovy:

fileName("MyScript.groovy")

sourceFolderOfCurrentFile

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.

Pointcut Arguments

A string corresponding to the source folder to match on.

Evaluation arguments

This pointcut is top level only.

Return values

The matches source folder as a singleton collection.

Example

Matches when in the src folder:

sourceFolderOfCurrentFile("src")

Combinatorial pointcuts

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).

and/or/not

Pointcut Arguments

These are the only pointcuts that can take more than one argument. The arguments must be other pointcuts.

Evaluation arguments

These pointcuts can take any evaluation arguments. They are simply passed on to their contained pointcuts.

Return values

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.

Example

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")))

Things to be careful about

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")

Binding pointcuts

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.

bind

Pointcut Arguments

A single pointcut

Evaluation arguments

Any evaluation arguments. They are passed to the contained pointcut.

Return values

Returns the value returned by the contained pointcut.

Example

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)))

User-defined pointcuts

It is possible to register your own pointcuts that will be available in the current DSLD script only. See the registerPointcut section below.

Other pointcuts

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.

Some subtleties about java.lang.Class references

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.

Assigning pointcuts to variables

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:

  1. 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.
  2. Next, notice the enclosingField pointcut. This component matches when the enclosing field name is "constraints" and the field is static.
  3. 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.
  4. Next, the expression must be inside of a closure
  5. The isThisType pointcut means that the type of the current expression must be the type of the enclosing class. Thus, references to this will match, but references to other types (such as new String() will not).

Contribution Blocks

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:

  1. name is the only required argument
  2. type defaults to java.lang.Object and the argument can be of type java.lang.String, java.lang.Class, or org.codehaus.groovy.ast.ClassNode.
  3. declaringType defaults to the currentType and can accept a String, Class, or ClassNode
  4. doc is the javadoc that will show up in hovers and accepts html syntax
  5. 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):

property

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"

method

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:

  1. params : regular parameters are added via content assist on the method
  2. 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.
  3. 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]

params

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
}

delegatesTo

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.

delegatesTo (alternative form with named arguments)

Adds all of the public methods in the delegated type to the current type. There is a possibility to parameterize using named arguments:

  1. type : (required) the type to delegate to.
  2. 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.
  3. useNamed : (optional) if true, content assist will be applied using named arguments.
  4. except : (optional) a list of method names to exclude from the delegation (useful if these methods are already being added through some other contribution).
  5. 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

delegatesToUseNamedArgs

(deprecated, delegatesTo type:"com.Foo", useNamed:true instead) similar to delegatesTo, but uses named arguments when applying content assist proposals

And the following properties

provider

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.

currentNode

Accesses the current Groovy AST node (an expression node)

enclosingNode

Accesses the Groovy AST node (an expression node) that encloses the currentNode. This is the parent node of the currentNode in the AST.

wormhole

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.

Binding

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"
}

supportsVersion and assertVersion

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
  }
}

Register pointcut

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()
  }
}

Wormhole

It is possible to pass state between contribution blocks using the wormhole. More about this later...

The dark side of DSLDs

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:

  1. Add System.exit() to any of your scripts (take a wild guess as to what will happen...)
  2. Make any changes to global state
  3. 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.

Clone this wiki locally