Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Fixed "scopeEnter" which should be "newScope"

...

 

Code Block
newMethod('foo') {
   // each time getReturnType on this method node will be called, this closure will be called!
   println 'Type checker called me!'
   classNodeFor(Foo) // return type
}

Should you need more than the name and return type, you can always create a new MethodNode by yourself.

Scoping

Scoping is very important in DSL type checking and is one of the reasons why we couldn't use a pointcut based approach to DSL type checking. Basically, you must be able to define very precisely when your extension applies and when it does not. Moreover, you must be able to handle situations that a regular type checker would not be able to handle, such as forward references:

Code Block
point a(1,1)
line a,b // b is referenced afterwards!
point b(5,2)

Say for example that you want to handle a builder:

Code Block
builder.foo {
   bar
   baz(bar)
}

Your extension, then, should only be active once you've entered the foo method, and inactive outside of this scope. But you could have complex situations like mutiple builders in the same file or embedded builders (builders in builders). While you should not try to fix all this from start (you must accept limitations to type checking), the type checker does offer a nice mechanism to handle this: a scoping stack, using the scopeEnter newScope and scopeExit methods.

  • scopeEnter newScope creates a new scope and puts it on top of the stack
  • scopeExits pops a scope from the stack
A scope consists of:
  • a parent scope
  • a map of custom data
If you want to look at the implementation, it's simply a LinkedHashMap (org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport.TypeCheckingScope), but it's quite powerful. For example, you can use such a scope to store a list of closures to be executed when you exit the scope. This is how you would handle forward references: 

 

Code Block
def scope = scopeEnternewScope()
...
scope.secondPassChecks << { println 'executed later' }
...
scopeExit {
   secondPassChecks*.run() // execute deferred checks
}

That is to say, that if at some point you are not able to determine the type of an expression, or that you are not able to check at this point that an assignment is valid or not, you can still make the check later... This is a very powerful feature. Now, scopeEnter newScope and scopeExit provide some interesting syntactic sugar:

Code Block
scopeEnternewScope { // create a new scope
   secondPassChecks = [] // initialize custom data in this scope (here, a list of closures to be executed when scopeExit is called)
}

At anytime in the DSL, you can access the current scope using getCurrentScope() or more simply currentScope. The general schema would be, then:

  • determine a "pointcut" where you push a new scope on stack and initialize custom variables within this scope
  • using the various events, you can use the information stored in your custom scope to perform checks, defer checks,...
  • determine a "pointcut" where you exit the scope, call scopeExit and eventually perform additional checks

...