Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

Version 1 Next »

This document describes Groovy-Eclipse's DSL descriptors. DSL descriptors (or dslds) are groovy files that describe domain-specific extensions to Groovy-Eclipse's inferencing engine and content assist. They allow end-users to script extensibility to Groovy-Eclipse's editing support.

The Problem

The Groovy language is an excellent platform for easy creation of domain specific languages (DSLs). However, these DSLs are not directly supported by the editor, and when DSLs are used heavily, standard IDE features like content assist, search, hovers, and navigation loose their value. Creating a DSL descriptor for a DSL is a way to make DSLs become first class citizens of Groovy-Eclipse.

The infrastructure

To create a dsld file inside of Eclipse, go to File -> New -> Text file and select a name ending with *.dsld. Choose a location in the Groovy project that you want to extend (preferably in a source folder, but this is not necessary).

Groovy-Eclipse's DSL support will process all dsld files that are in a 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 coming from another project). Note that each project has its own set of dsld files and they are not shared by default. 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, all scripts can be refreshed and recompiled from this page.

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.

An Introduction to The DSLD language

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

1. 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 with in a file with an extension of "*.gradle" and whose the type is String.

fileExtension("gradle") & currentType(String)

2. contribution blocks: a code block that describes the extra properties and methods available when a pointcut matches. A pointcut is useless unless it is associated with a contribution block. Here is how to associate a pointcut with a contribution block:

(fileExtension("gradle") & currentType(String)).accept {
method name : "isNameOfTask", type: Boolean
}

The accept method on pointcuts takes a closure and inside that closure, you can specify a set of methods or properties that should be added to the type of the expression being analyzed. The contribution block above adds the isNameOfTask method to all Strings in gradle scripts.

When this script is added to a Groovy project, and when editing inside of a gradle file isNameOfTask will be included in content assist of all Strings. Furthermore, references to isNameOfTask on Strings will not be underlined.

Here is an example:

1. def x = "blah"
2. def b = x.isNameOfTask()
3. b // hover over b and you will see "java.lang.Boolean"

Groovy-Eclipse's inferencing engine uses Pointcuts and their associated contribution blocks when determining the type of expressions. Here is a general description of how the inferencing engine works:

1. Visit each expression in a groovy file
2. For each expression, infer its type, declaring type, and declaration:

  • go through each available pointcut and look for matches. if there is a match, then execute each contribution block in order to determine the extra information
  • if there are no matches, then use other means to perform the inferencing of the expression.
    3. Do something with the inferenced result, such as underline it, send it to the search pane, use it for content assist. highlight it, etc.
    4. Remember the inferenced result so that it is available for use by the next expression.

In the example above, the 'x' declaration is inferred to be of type String on line 1, and so on line 2, the inferencing engine determines that the pointcut defined above matches. The inferencing engine then knows that isNameOfTask is a valid method call on this expression. When the next expression is visited (ie- the 'isNameOfTask' method call) the inferencing engine can determine its return type, which gets shunted to the variable declaration 'b'. And so finally on line 3, 'b' is inferred to be of type boolean.

More formally

This is the core of the dsld language, which we can now describe in slightly more formally in terms of a join point model. In AOP, a Join Point model requires 3 things (link):

1. A set of elements to describe
2. A way to match against a sub-set of these elements
3. A way to alter behavior whenever a match is made

And this is matched to concepts in the dsld language as follows:

1. Expression ASTNodes in a Groovy file
2. The pointcut language described above
3. Contribution blocks allow the inferencing engine to be enhanced with new type suggestions

Pointcuts

There are a fixed set of standard pointcuts which can be composed using '&' or '|' and negated using `~`. Pointcuts are generally self documenting using content assist and hovers inside of the dsld file, but here we describe a few of the more important and complicated pointcuts

  • currentType - matches on the type of the current expression being evaluated
  • currentTypeIsEnclosingType - matches when the current type being evaluated is the type being declared (ie- this)
  • findField/findProperty/findMethod - finds a set of fields, methods or properties
  • isScript/isClass/isEnum - matches when inferencing is occurring inside of a script/class/enum
  • enclosingClass/enclosingField/enclosingMethod - matches on the enclosing class/field/method declaration
  • enclosingCallDeclaringType/enclosingCallName - These two pointcuts match on either the name of the enclosing call, or its declared type. A method call is enclosing an expression when that expression is an argument to that method. Eg- myMethod(1) the '1' constant has an enclosing method call of 'myMethod'. Similarly, in here: myMethod
    Unknown macro: { print 'a closure' }
    , the closure is enclosed by the call to myMethod. Note that method calls can be nested ( myMethod( yourMethod( 3 ) ) ) and that these pointcuts match on the first call found that matches working from inside to outside.
  • nature - This pointcut matches on the project nature of the current project. Eclipse uses the notion of project natures to describe different kinds of capabilities of projects. A groovy project has 'org.eclipse.jdt.groovy.core.groovyNature' as its nature. A grails project has the groovy nature and addtionally 'com.springsource.sts.grails.core.nature' as a project nature.
  • fileName/fileExtension - these pointcuts match on the file name or the file extension of the file being inferred.
  • sourceFolderOfCurrentFile - this pointcut matches on the source folder of the current file (eg- 'src' or 'grails-app/controllers')
  • sourceFolderOfCurrentType - this pointcut matches on the source folder of the file of the current type (eg- 'src' or 'grails-app/controllers'). Note that this is different from the previous pointcut. This pointcut depends on the type of the current expression, whereas sourceFolderOfCurrentFile depends on the current file.
  • isStatic/isPublic/isPrivate - these pointcuts match on whether or not the current declaration has the given modifier.

This is not a complete list of pointcuts, but this does describe the most common pointcuts. The remainder can be seen in content assist when creating scripts.

Poincut arguments

Most pointcuts take a single, optional argument. This argument can be a String, java.lang.Class, ClassNode, or another pointcut. Here are some examples:

currentType() - matches on the current type (ie- always)
currentType("java.lang.String") or currentType(String) - matches when the current type is a string
currentType(annotatedBy(Singleton)) or currentType(annotatedBy("groovy.lang.Singleton")) - matches when the current type is annotated by the @Singleton annotation
currentType(findField(annotatedBy(Delegate))) - matches when you can find one or more fields on the current type that has an @Delegate annoation on it
enclosingClass(findField(annotatedBy(Delegate))) - matches when the enclosing class has a delegate method

In many cases, the currentType pointcut is implicit. So, this:
findField(annotatedBy(Delegate))
is the same as this:
currentType(findField(annotatedBy(Delegate)))

The &, | and ~ pointcut combinators work by combining pointcuts:

currentType(findMethod(annotatedBy(Override) & isSynchronized())) - matches when you can find one or more fields in the current type that are synchronized and override a method from a super class.
currentType( ~ isPublic()) - matches when the current type is not public

If you want to match on the name as well as something else, you must explicitly use the name() pointcut:

currentType( (~ isPublic() ) | name("java.lang.String") ) - matches when the current type is not public or it is a String. The name pointcut only accepts Strings. (Although this may change in the future and it will also accept regular expressions.

Things to be careful about

Due to Groovy's operator precedence rules, parens muse be used around not ('~') or else the ~ will apply to the pointcut name (without the parens), instead of the pointcut expression (with the parends). You should do this:

(~ isPublic() ) | name("java.lang.String")

instead of this:

~ isPublic() | name("java.lang.String")

Similarly, you must put parens around expressions using & or | and before the accept. Otherwise, the accept call will apply to the final pointcut, rather than the entire expression. You should do this:

((~ isPublic() ) | name("java.lang.String")).accept {
...
}

instead of this:

(~ isPublic() ) | name("java.lang.String").accept {
...
}

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") & (~isScript())
}

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:

(domainClass & enclosingField(name("constraints") & isStatic()) &
inClosure() &
currentTypeIsEnclosingType() &
(bind(props : findProperty()))).accept

Unknown macro: { ... }

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. 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.
3. Next, the expression must be inside of a closure
4. The currentTypeIsEnclosingType() pointcut means that the type of the current expression must be the type of the enclosing class. Thus, references to 'this' will match as well as empty expressions/

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 method to the type of the expression matched in the accepting pointcut:

...accept {
method name : "isNameOfTask", type: Boolean
}

**The full form of 'method' is:

method name : "isNameOfTask", type: Boolean, declaringType: "java.lang.String", params : [ arg1 : String, arg2 : Class], isStatic : false, useNamedArgs : false, doc : "<b>Enter javadoc here</b> html is supported", provider : "A readable name for your DSL"

A few notes:

  • type defaults to Object and can accept a String, Class, or ClassNode
  • declaringType defaults to the currentType and can accept a String, Class, or ClassNode
  • 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
  • name is the only required argument

Additionally, the following methods are available

**property: declares a new property. The full forrm is like this. Name is the only required field:

property name : "nameOfTask", type: String, declaringType: "java.lang.String", isStatic : false, doc : "<b>Enter javadoc here</b> html is supported", provider : "A readable name for your DSL"

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

**delegatesToUseNamedArgs: 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)
**wormhole: Allows a means to pass state between contribution blocks. An example is given below.

Binding

Sometimes, the items matched in the accepting pointcut are required in the contribution block. You can use named arguments for pointcuts to bind a name and make it available inside of the contribution block.

For example, here is the DSL for the @Delegate AST transform:

currentType(fields : findField(annotatedBy(Delegate))).accept {
if (fields instanceof Collection) {
for (field in fields)

Unknown macro: { delegatesTo(field.declaringType) }

} else if (fields instanceof FieldNode)

Unknown macro: { delegatesTo fields }

}

The 'field' 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>.

Now, 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

bind( type :currentType(annotatedBy(Singleton))).accept {
method name:"instance", type:type, isStatic:true, declaringType:type, doc:"Get the singleton instance of this Class"
}

SupportsVersion

If you want to ensure that a script only runs when particular features are installed, you can use the 'supportsVersion' top-level method. This method call should go at the top of a script, since scripts are executed sequentially.

The syntax looks like this:

supportsVersion(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:

supportsVersion(groovy:"1.7.8",groovyEclipse:"2.1.3")

Wormhole (Not yet)

Some larger examples

Not yet...

  • No labels