Type Checking Extensions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14). The static type checker described on
Static Type Checking & @CompileStatic knows
nothing about what a program does through the runtime metaobject protocol: a builder that answers any method name, a script whose
variables come from a binding, a class that overrides methodMissing. A type checking extension is a piece of compiler-side code,
usually a short Groovy script, that plugs into the checker so that it can understand such code, or that makes the checker reject
code it would normally accept. The extension runs while the compiler type checks the code, so mistakes are reported at compile
time with a normal error message and a line number. The checkers Groovy ships with are built on the same mechanism and are
described on Built-in Type Checkers; this page is about writing
your own.
When an extension is the right tool
Without help, type checking and runtime dynamism pull in opposite directions. A markup builder is the standard example: in
html { body { p 'Hello' } } none of html, body or p is a method that exists anywhere, and there is no finite list of
tag names the checker could learn. For most code that is a reason not to type check the builder. For a domain-specific language
that end users type in as scripts, it is a problem: the user may not even realise that what they wrote is code, and a typo found
only at run time can be expensive. The Groovy documentation uses a rover on Mars as the example, where a message needs about
fifteen minutes to arrive, so a script that fails half-way both delays the feedback and leaves the rover in a state nobody
planned for. The goal of an extension is to fail early: reject the script before any of it runs, with an understandable
message. The same idea fits a greenhouse controller that accepts scripts from growers.
An extension can do two opposite things:
-
Weaken the checker where it fails: declare that a variable has a type, that a method exists, that an assignment is acceptable.
-
Strengthen the checker where it is silent: forbid certain calls, limit how often something is used, check naming rules.
How DSLs are built (script base classes, @DelegatesTo, builders) is on
Domain-Specific Languages and
Builders; compilation customizers, used below to switch type checking on for
every script, are on
Compilation Customizers & Secure Scripting.
Activating an extension
@TypeChecked and @CompileStatic have an extensions attribute holding a list of extension names. A name is either the
path of an extension script that is found on the compilation classpath, or the fully qualified name of a precompiled extension
class (shown later). The scripts are looked up through the class loader that compiles the code, so the directory holding them
must be on that class path. On the command line that is groovy -cp ext script.groovy; when embedding the compiler, put the
directory in a class loader that is passed to GroovyShell. In the run for this page, setting
CompilerConfiguration.classpath did not make the extension visible: the compiler answered that the extension "was not found
on the classpath" until the directory was supplied through a URLClassLoader parent, which is what the driver below does.
A robot with one method, and a script that is just robot.move 100 with robot supplied by the host through a binding. With
@TypeChecked applied to the script by a compilation customizer, the checker rejects robot because no declaration is visible.
The extension script teaches it that an unresolved variable called robot has type Robot:
// ext/robotextension.groovy
unresolvedVariable { var ->
if ('robot' == var.name) {
storeType(var, classNodeFor(Robot))
handled = true
}
}
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.transform.TypeChecked
class Robot {
int position
Robot move(int qt) { position += qt; this }
}
void check(String script, Map opts = [:]) {
def config = new CompilerConfiguration()
config.addCompilationCustomizers(new ASTTransformationCustomizer(opts, TypeChecked))
def loader = new URLClassLoader([new File('ext').toURI().toURL()] as URL[], this.class.classLoader)
def shell = new GroovyShell(loader, new Binding(), config)
def robot = new Robot()
shell.setVariable('robot', robot)
try {
shell.evaluate(script)
println "ran: position=${robot.position}"
} catch (e) {
println e.message.readLines().findAll { it.contains('[Static type checking]') }*.replaceFirst(/^Script1.groovy: /, '')*.trim().join('\n')
}
}
println '--- no extension'
check('robot.move 100')
println '--- with extension'
check('robot.move 100', [extensions: ['robotextension.groovy']])
println '--- typo caught before anything runs'
check('robot.move 100\nrobot.mvoe 5', [extensions: ['robotextension.groovy']])
--- no extension
1: [Static type checking] - The variable [robot] is undeclared.
--- with extension
ran: position=100
--- typo caught before anything runs
2: [Static type checking] - Cannot find matching method Robot#mvoe(int). Please check if the declared type is correct and if the method exists.
The first run reproduces the error that motivates extensions. The second one succeeds and the call executes. The third shows the
point of the exercise: the second line contains a typo, mvoe, and because type checking happens before execution the first
line is not executed either (no ran: line is printed). Source:
Groovy documentation: A DSL for type checking
Applying the annotation directly in source works the same way, as long as the extension script is on the class path. With
groovy -cp ext and the events.groovy extension listed in the next sections:
import groovy.transform.TypeChecked
@TypeChecked(extensions = 'events.groovy')
class Shopping {
int words(String list) {
list.trim().split(' ').length
}
}
println new Shopping().words(' eggs milk flour ')
type checker saw 2 call(s) on String
3
Adding one more call on String to the method (list.trim().toLowerCase().split(' ').length) makes the compiler refuse the
class; the run printed type checker saw 3 call(s) on String followed by this compilation error:
type checker saw 3 call(s) on String
e8.groovy: 6: [Static type checking] - At most 2 calls on String are allowed
Parameterized extensions
An extension name can carry options, written like named arguments in parentheses after the name: 'params.groovy(limit: 1, label: "demo")'. Supported values are booleans, integers, strings in single or double quotes, and null; entries are separated
by commas, and string values cannot contain commas or colons because those are the delimiters. Inside a script extension the
options are available as the options property, an unmodifiable map; precompiled Java extensions call getOptions(). The same
syntax is accepted by ASTTransformationCustomizer. This is the feature the bundled NullChecker(strict: true) and
PurityChecker(allows: "LOGGING") use. It is new in Groovy 6.0.
// ext/params.groovy
beforeVisitMethod { m ->
int limit = options?.limit ?: 3
if (m.parameters.size() > limit) {
addStaticTypeError("${options?.label ?: 'extension'}: ${m.name} takes ${m.parameters.size()} parameters, limit is ${limit}", m)
}
}
The Driver class used by the remaining examples applies @TypeChecked or @CompileStatic with the given extensions to a code
string, with ext and pre (precompiled classes) on the class path. It either compiles and runs the code or prints the
type checking errors:
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.transform.TypeChecked
import groovy.transform.CompileStatic
class Driver {
static Object run(String code, Class ann, List ext, Map vars = [:], String scriptBase = null) {
compile(code, ann, ext, vars, scriptBase, true)
}
static Object compile(String code, Class ann, List ext, Map vars = [:], String scriptBase = null, boolean exec = false) {
def config = new CompilerConfiguration()
config.addCompilationCustomizers(new ASTTransformationCustomizer(ann, extensions: ext))
if (scriptBase) config.scriptBaseClass = scriptBase
def loader = new URLClassLoader([new File('ext').toURI().toURL(), new File('pre').toURI().toURL()] as URL[], Driver.classLoader)
def shell = new GroovyShell(loader, new Binding(vars), config)
try {
if (exec) return shell.evaluate(code)
shell.parse(code)
println 'compiles'
return null
} catch (e) {
println e.message.readLines().findAll { it.contains('[Static type checking]') || it.startsWith('Script1.groovy') && it.contains('extension') }*.replaceFirst(/^Script1.groovy: /, '')*.trim().join('\n')
return null
}
}
}
The parameters extension is exercised in the program under "Scopes" below. Source: Groovy documentation: Parameterized extensions
The event API
The extension script is written in a small DSL. Each construct names an event; the closure runs when the type checker reaches
that point. A script may contain many blocks, including several for the same event. Inside a closure, an assignment to
handled = true tells the checker that the extension dealt with the situation, so the checker must skip its own handling (and
in particular its own error). The events are:
| Event | Closure arguments | When it fires, and typical use |
|---|---|---|
|
none |
After the checker is initialised, before anything else. Initialise counters and state. |
|
none |
After type checking is complete. Run checks that need the whole picture. |
|
|
A variable has no declaration. Give it a type with |
|
|
The checker cannot find a property on the receiver (dynamic properties). |
|
|
The checker cannot find an attribute ( |
|
|
Before the checker examines a call. With |
|
|
After the standard checks of a call, even when |
|
|
The checker found a target method. May fire for expressions other than method calls, such as binary expressions. Use it to react to a specific call or to enter the scope of a closure-taking method. |
|
receiver |
No target method was found. Return one |
|
|
Before the checker enters a method body. With |
|
|
After a method body was checked. Report problems found while collecting information. |
|
|
Before a type checked class, including inner classes of an annotated class, is visited. |
|
|
After a class was checked, including nested and anonymous classes that are not skipped. |
|
left type, right type, |
The checker considers an assignment invalid. Accept it with |
|
|
The checker considers a return value incompatible with the declared type. Accept it with |
|
|
Several methods are equally good. Return the one that should win. |
The first example uses setup, onMethodSelection, beforeVisitClass and finish. It counts calls on String and rejects
a third one, and it refuses class names that start with a lower case letter:
// ext/events.groovy
// counts calls on String and rejects the third one; also checks class names
setup { count = 0 }
onMethodSelection { expr, method ->
if (method.declaringClass.name == 'java.lang.String') {
if (++count > 2) {
addStaticTypeError('At most 2 calls on String are allowed', expr)
}
}
}
beforeVisitClass { classNode ->
if (!(classNode.nameWithoutPackage[0] in 'A'..'Z')) {
addStaticTypeError("Class '${classNode.nameWithoutPackage}' must start with an uppercase letter", classNode)
}
}
finish { println "type checker saw ${count} call(s) on String" }
println '--- two String calls: fine'
println Driver.run('''
def s = 'groove'
s.toUpperCase()
s.length()
''', groovy.transform.TypeChecked, ['events.groovy'])
println '--- three String calls: rejected'
Driver.run('''
def s = 'groove'
s.toUpperCase()
s.length()
s.trim()
''', groovy.transform.TypeChecked, ['events.groovy'])
println '--- lower-case class name'
Driver.run('''
class widget { }
''', groovy.transform.TypeChecked, ['events.groovy'])
--- two String calls: fine
type checker saw 2 call(s) on String
6
--- three String calls: rejected
type checker saw 3 call(s) on String
5: [Static type checking] - At most 2 calls on String are allowed
--- lower-case class name
type checker saw 0 call(s) on String
2: [Static type checking] - Class 'widget' must start with an uppercase letter
The finish closure prints its summary even when an error was reported, and the line number in an error is that of the AST node
passed to addStaticTypeError (counted from the start of the code string, which begins with a newline).
An extension that handles a call before the checker looks at it, and one that accepts an assignment the checker would reject,
use beforeMethodCall and incompatibleAssignment:
// ext/rules2.groovy
// incompatibleAssignment: allow a Closure to be assigned to an int-typed slot (a runtime setProperty would convert it)
incompatibleAssignment { lhsType, rhsType, expr ->
if (lhsType == classNodeFor(int) && rhsType == classNodeFor(Closure)) {
handled = true
}
}
// beforeMethodCall: forbid toUpperCase everywhere
beforeMethodCall { call ->
if (isMethodCallExpression(call) && call.methodAsString == 'toUpperCase') {
addStaticTypeError('toUpperCase is not allowed in this DSL', call)
handled = true
}
}
import groovy.transform.TypeChecked
println '--- without extension'
Driver.compile('''int n = { 42 }''', TypeChecked, [])
println '--- with extension'
Driver.compile('''int n = { 42 }''', TypeChecked, ['rules2.groovy'])
Driver.compile('''String s = 'a'.toUpperCase()''', TypeChecked, ['rules2.groovy'])
--- without extension
1: [Static type checking] - Cannot assign value of type groovy.lang.Closure<java.lang.Integer> to variable of type int
--- with extension
compiles
1: [Static type checking] - toUpperCase is not allowed in this DSL
Working with the AST
The API is low level: arguments are nodes of the abstract syntax tree (ClassNode, MethodNode, VariableExpression,
MethodCallExpression and so on), so you need to know the AST. The script is delegated to
org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport, which gives it these variables:
-
context: the type checking context, aTypeCheckingContext, holding the stack of enclosing method calls, closures and binary expressions; -
typeCheckingVisitor: the checker itself, aStaticTypeCheckingVisitor, so an extension can call its methods; -
generatedMethods: the list of phantom methods created withnewMethod.
The script also imports the static members of ClassHelper and StaticTypeCheckingSupport, so constants such as
OBJECT_TYPE, STRING_TYPE or THROWABLE_TYPE and helpers such as missesGenericsTypes(ClassNode) are available directly.
Class nodes
Inside an extension, types are ClassNode objects, not class literals: the tree may be incomplete while a class is being
checked, so String the class and String the node are different things. classNodeFor(String) returns the resolved node for a
class. A variant taking a string name exists but creates an unresolved node without methods or properties, so it should be kept
for special cases. When the type is one that is being compiled in the same batch and does not exist as a class yet, use
lookupClassNodeFor('Foo').
Telling the checker about types
storeType(node, type) records the type of any expression, not only variables; getType(node) returns the inferred type of an
expression. Inferred is the important word: for a variable declared as Object that holds a String at that point, getType
returns String because of flow typing. To get the declared type, ask the AST node itself.
Reporting errors
addStaticTypeError(message, node) reports a compilation error. Pass the AST node that is most responsible, since its position
gives the line and column in the message. For readability the DSL also provides isBinaryExpression(node) and its relatives,
which stand for node instanceof BinaryExpression.
Virtual methods
When the code calls a method that does not exist but is known to work at run time, such as a dynamic finder findByName, the
extension can invent a phantom method: newMethod(name, returnType), where the type is a Class, a ClassNode or a closure
that computes the ClassNode lazily (useful for forward references). The method is added to generatedMethods, so
isGenerated(methodNode) can recognise it. methodNotFound returns such a method, and unresolvedProperty stores a type on the
property expression. The extension below teaches the checker a French vocabulary that a runtime metaclass would provide:
longueur() on a String returns an int, and the property majuscules is a String.
// ext/french.groovy
// teach the checker about a "French" vocabulary that a runtime MOP provides
methodNotFound { receiver, name, argList, argTypes, call ->
if (receiver == classNodeFor(String) && name == 'longueur' && argList.size() == 0) {
handled = true
return newMethod('longueur', classNodeFor(int))
}
}
unresolvedProperty { pexp ->
if (pexp.propertyAsString == 'majuscules' && getType(pexp.objectExpression) == STRING_TYPE) {
storeType(pexp, STRING_TYPE)
handled = true
}
}
Scopes
Extensions must know exactly when they apply. In a builder (builder.foo { bar; baz(bar) }), rules should be active inside
foo only, and nested or repeated builders complicate it further. A scope stack handles this. newScope() pushes a scope, a
map of custom data with a parent; currentScope reads it; scopeExit { ... } registers a closure that runs when the scope
is popped, and is the place to run deferred checks such as forward references (a variable used before its declaration is
resolved later). newScope { count = 0 } combines creating a scope and initialising its data. The general scheme is: push a
scope at a pointcut such as entering a method that takes a closure, collect information with the other events, and check in
scopeExit.
// ext/scoped.groovy
// a scope per method: count method calls and complain at the end of the method
beforeVisitMethod { methodNode ->
newScope { calls = 0 }
}
onMethodSelection { expr, method ->
if (currentScope?.calls != null) currentScope.calls++
}
afterVisitMethod { methodNode ->
scopeExit {
if (calls > 2) {
addStaticTypeError("Method ${methodNode.name} contains more than 2 method calls (found ${calls})", methodNode)
}
}
}
The helper methods worth knowing beyond these: isDynamic(variableExpression) (a script variable declared without type or
def), isGenerated(methodNode), isAnnotatedBy(node, SomeAnnotation), getTargetMethod(call) (the method the checker
selected) and delegatesTo(...), which emulates @DelegatesTo for an argument. The complete list is in the Javadoc of
GroovyTypeCheckingExtensionSupport and TypeCheckingExtension.
Here is a program that runs the French, scoped and parameters extensions. Driver.compile only compiles the code, it does not
execute it (the French methods do not exist at run time, so running the code would fail):
import groovy.transform.TypeChecked
println '--- unknown method and property, taught by the extension'
Driver.compile('''
class Greeting {
int size(String s) { s.longueur() }
String shout(String s) { s.majuscules }
}
new Greeting().size('bonjour')
''', TypeChecked, ['french.groovy'])
println '--- same code without the extension'
Driver.compile('''
class Greeting {
int size(String s) { s.longueur() }
}
''', TypeChecked, [])
println '--- scopes'
Driver.compile('''
class Chain {
String a(String s) { s.trim().toUpperCase().reverse() }
String b(String s) { s.trim() }
}
''', TypeChecked, ['scoped.groovy'])
println '--- parameters'
Driver.compile('''
class Wide { void f(int a, int b) { } }
''', TypeChecked, ['params.groovy(limit: 1, label: "demo")'])
Driver.compile('''
class Wide { void f(int a, int b) { } }
''', TypeChecked, ['params.groovy'])
--- unknown method and property, taught by the extension
compiles
--- same code without the extension
3: [Static type checking] - Cannot find matching method java.lang.String#longueur(). Please check if the declared type is correct and if the method exists.
--- scopes
3: [Static type checking] - Method a contains more than 2 method calls (found 3)
--- parameters
2: [Static type checking] - demo: f takes 2 parameters, limit is 1
compiles
The first compile succeeds only thanks to the extension; without it the checker reports that longueur() cannot be found. The
scoped extension counts the calls selected inside each method and reports a (three calls) but not b (one call). The
parameters extension is configured with a limit of 1 through the options, and with no options it uses its default limit of 3 and
the same class compiles. Source:
Groovy documentation: Working with extensions
Precompiled extensions
An extension script is compiled again for every source unit that uses it, which slows compilation. A precompiled extension is
compiled once and referenced by its fully qualified class name instead of a path. Written in Groovy, the script becomes the
body of run() in a subclass of GroovyTypeCheckingExtensionSupport.TypeCheckingDSL, and all the events work unchanged:
import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
class PrecompiledExtension extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
@Override
Object run() {
unresolvedVariable { var ->
if ('robot' == var.name) {
storeType(var, classNodeFor(Robot))
handled = true
}
}
}
}
Written in Java there is no DSL: the class extends AbstractTypeCheckingExtension and overrides the handle... methods. The
same extension in Java:
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.transform.stc.AbstractTypeCheckingExtension;
import org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor;
public class PrecompiledJavaExtension extends AbstractTypeCheckingExtension {
public PrecompiledJavaExtension(final StaticTypeCheckingVisitor typeCheckingVisitor) {
super(typeCheckingVisitor);
}
@Override
public boolean handleUnresolvedVariableExpression(final VariableExpression vexp) {
if ("robot".equals(vexp.getName())) {
storeType(vexp, ClassHelper.make(Robot.class));
setHandled(true);
return true;
}
return false;
}
}
Both were compiled in the run for this page (groovyc -d pre Robot.groovy src/PrecompiledExtension.groovy, then javac with
groovy-6.0.0.jar and the pre directory on the class path) and used by class name:
import groovy.transform.TypeChecked
['PrecompiledExtension', 'PrecompiledJavaExtension'].each { name ->
println name
println Driver.run('robot.move 7\nrobot.position', TypeChecked, [name], [robot: new Robot()])
}
PrecompiledExtension
7
PrecompiledJavaExtension
7
An extension may use @Grab to pull in libraries that are needed at compile time only, at the cost of a longer first
compilation. To share extensions, bundle the scripts or classes in a jar on the compilation class path. Groovy has no way to
apply an extension globally just by having it on the class path: it must be named in extensions or added by a compilation
customizer. Source:
Groovy documentation: Precompiled type checking extensions
Extensions and @CompileStatic: mixed mode
Extensions also work with @CompileStatic, with an important restriction: telling the checker about a type is not enough to tell
the code generator how to call something. According to the documentation, setting handled = true on an unresolved variable triggers a dynamic lookup of the
variable by default, so the robot example above also works under @CompileStatic. A method call is different. Take the script move 100,
run with a groovy.util.DelegatingScript base class whose delegate is the robot, so move is resolved on the delegate at run
time. An extension that declares the method with newMethod satisfies the checker, but the static compiler has no idea which
method it is or what the receiver is. makeDynamic(call, returnType) fixes this: it creates the virtual method, sets
handled, and marks the call so that the compiler generates a dynamic call for it, while code after it that uses the declared
return type is still compiled statically. makeDynamic accepts a MethodNode, a VariableExpression or a PropertyExpression.
// ext/robotextension2.groovy -- newMethod only
methodNotFound { receiver, name, argList, argTypes, call ->
if (isMethodCallExpression(call) && call.implicitThis && 'move' == name
&& argTypes.length == 1 && argTypes[0] == classNodeFor(int)) {
handled = true
newMethod('move', classNodeFor(Robot))
}
}
// ext/robotextension3.groovy -- makeDynamic
methodNotFound { receiver, name, argList, argTypes, call ->
if (isMethodCallExpression(call) && call.implicitThis && 'move' == name
&& argTypes.length == 1 && argTypes[0] == classNodeFor(int)) {
makeDynamic(call, classNodeFor(Robot))
}
}
import groovy.transform.CompileStatic
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
def runWith = { String extension ->
def config = new CompilerConfiguration()
config.scriptBaseClass = 'groovy.util.DelegatingScript'
config.addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic, extensions: [extension]))
def loader = new URLClassLoader([new File('ext').toURI().toURL()] as URL[], Robot.classLoader)
def script = new GroovyShell(loader, new Binding(), config).parse('move 100')
def robot = new Robot()
script.setDelegate(robot)
try {
script.run()
println "${extension}: position=${robot.position}"
} catch (Throwable t) {
println "${extension}: ${t.class.simpleName}: ${t.message}"
}
}
runWith('robotextension2.groovy')
runWith('robotextension3.groovy')
robotextension2.groovy: NoSuchMethodError: 'Robot java.lang.Object.move()'
robotextension3.groovy: position=100
The version that only declares the method compiles but fails at run time with a NoSuchMethodError, because the compiler emitted a
static call to a method that does not exist on Object; the makeDynamic version runs. This is deliberate: unresolved calls are
not made dynamic automatically, because then typos would stop being compile errors under @CompileStatic. Mixed mode must be
asked for, so the author of the DSL knows exactly where typing rules are relaxed.
An extension used with @CompileStatic can also be used only to add errors, without relaxing anything. A checker of SQL
queries is the example in the documentation: it makes sense with or without static compilation, because the code passes the
standard checks anyway. Source:
Groovy documentation: Type checking extensions and @CompileStatic
Transforming the AST from an extension
Since an extension has access to the AST, it could modify it. That is not recommended. Type checking is supposed only to annotate
the tree, so that code behaves the same with and without @TypeChecked; and type checking is the last phase before bytecode
generation, so a change made there must already be a fully valid tree because no later phase repairs it. If the extension is meant
for @CompileStatic, where the semantics already differ from dynamic Groovy, changing the AST is more acceptable, but a
transformation that runs before type checking is usually easier. See
Writing AST Transformations & Macros.
Extensions for DSL safety
For a DSL, the pieces fit together like this: a host application compiles user scripts with a CompilerConfiguration that has a
script base class or binding for the DSL vocabulary, a customizer that applies @TypeChecked (or @CompileStatic) to every
script, and an extension that declares the variables and virtual methods of the vocabulary and adds the project’s own rules, as
the robot examples above do. @DelegatesTo and @ClosureParams (see
Closure Delegation: Owner, Delegate & This)
cover the common case of a closure whose delegate type is known; an extension covers what annotations cannot express. The
documentation points to real extensions in the Groovy source tree (the scripts used by the TypeCheckingExtensionsTest
test class), and notes that the markup template engine, described on
Template Engines, uses a complex extension plus AST transformations to
turn templates into statically compiled code. Source:
Groovy documentation: Domain-specific languages
Where the book is outdated
Books written before Groovy 2.0 do not cover static type checking, so they cannot cover extensions to it; extensions arrived
after the checker itself, in the 2.x line. Parameterized extensions (an option list inside the extension name) and the bundled
checkers in the groovy.typecheckers package are new in Groovy 6.0 and exist in no earlier book. Code samples that use the
@TypeChecked(extensions=...) form from the 2.x era still work; the support classes remain in the
org.codehaus.groovy.transform.stc package named in the current documentation.