This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 4890b75804 tweak wording
4890b75804 is described below
commit 4890b75804b687a3525095fc17b2ef4576c4b6ea
Author: Paul King <[email protected]>
AuthorDate: Fri Apr 10 20:11:55 2026 +1000
tweak wording
---
.../src/spec/doc/typecheckers.adoc | 173 ++++++++++++++++++++-
1 file changed, 165 insertions(+), 8 deletions(-)
diff --git a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
index cd6a55bac8..b73c097aff 100644
--- a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
+++ b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
@@ -21,6 +21,7 @@
ifndef::reldir_typecheckers[]
:reldir_typecheckers: .
endif::[]
+:icons: font
= Built-in auxiliary type checkers
@@ -30,15 +31,146 @@ Groovy's static nature includes an extensible
type-checking mechanism.
This mechanism allows users to selectively strengthen or weaken type checking
as needed to cater for scenarios where the standard type checking isn't
sufficient.
-In addition to allowing you to write your own customer checkers,
-Groovy offers a handful of useful built-in type checkers.
-These provide additional checks for specific scenarios.
+In addition to allowing you to write your own custom checkers,
+Groovy offers a suite of built-in type checkers that provide additional
+compile-time verification for specific concerns:
-In the examples which follow, we'll explicitly show declaring
-the type checker we want to use.
-As usual, Groovy's compiler customization mechanisms would allow you to
-simplify application of such checkers, e.g. make it apply globally
-using a compiler configuration script, as just one example.
+[cols="3,10",options="header"]
+|===
+| Checker | What it does
+
+| <<Checking Regular Expressions,RegexChecker>>
+a| Validates regex patterns at compile time -- no more
`PatternSyntaxException` at runtime
+
+[listing,subs="+quotes,macros"]
+----
+assert 'Apple' =~ /(a\|A).*/ [.green]#// icon:check[] okay#
+assert 'apple' =~ /(a\|A.*/ [.red]#// icon:times[] compile error: unclosed
group#
+----
+
+| <<Checking Format Strings,FormatStringChecker>>
+a| Validates `printf`/`format` specifiers against argument types -- catches
mismatches at compile time
+
+[listing,subs="+quotes,macros"]
+----
+String.format('%d: %s', 42, 'ok') [.green]#// icon:check[] okay#
+String.format('%d: %s', 'x', 42) [.red]#// icon:times[]
IllegalFormatConversion: d != String#
+----
+
+| <<Checking Null Safety (Incubating),NullChecker>>
+a| Detects potential null dereferences using annotations and optional flow
analysis
+
+[listing,subs="+quotes,macros"]
+----
+if (name != null) name.size() [.green]#// icon:check[] null guard#
+name?.size() [.green]#// icon:check[] safe navigation#
+name.size() [.red]#// icon:times[] potential null
dereference#
+----
+
+| <<Checking @Modifies Frame Conditions (Incubating),ModifiesChecker>>
+a| Verifies methods only modify declared fields -- humans and AI know what
changed and what didn't
+
+[listing,subs="+quotes,macros"]
+----
+@Modifies({ this.balance })
+void deposit(n) { balance += n } [.green]#// icon:check[] balance is
declared#
+
+@Modifies({ })
+void withdraw(n) { balance -= n } [.red]#// icon:times[] balance not in
@Modifies#
+----
+
+| <<Checking @Pure Purity (Incubating),PurityChecker>>
+a| Enforces that `@Pure` methods have no side effects, with configurable
strictness
+
+[listing,subs="+quotes,macros"]
+----
+@Pure int twice() { value * 2 } [.green]#// icon:check[] reads only#
+@Pure int bad() { count++ } [.red]#// icon:times[] field mutation#
+----
+|===
+
+These checkers work with annotations from multiple libraries (JSpecify,
JetBrains,
+Checker Framework, and others) -- matching by simple name so you can use
whichever
+annotation library your project already depends on.
+
+=== Why these checkers matter
+
+In a world which seems to be having an ever-increasing focus on AI,
+here are numerous reasons you might want to make liberal use of checkers and
their annotations:
+
+* The checker declarations and associated annotations, along with other Groovy
annotations
+used with AST transforms, e.g. `@Invariant` from `groovy-contracts`, form a
declarative
+specification of program behavior. This specification replaces "read the body"
reasoning allowing humans and AI to understand system behavior at scale.
+* When an AI agent generates or modifies code, no human may fully understand
the result. Type checkers act as a compile-time safety net on what those
programs can do.
+* If we can treat our systems as composable black boxes each with well
specified behavior we can more easily start to apply compositional reasoning to
our systems.
+* Groovy's dynamic features (metaprogramming, runtime dispatch, invokeMethod)
are powerful
+but can make static reasoning hard. AI models can hallucinate about what
dynamic code does.
+Type checkers let teams selectively fill in the missing gaps in static
reasoning,
+that AI would otherwise struggle with.
+* As AI agents increasingly write, review, and deploy code autonomously, we
need machine-checkable invariants which form trust boundaries in agentic
workflows.
+
+As an example, let's explore how combining some checkers and
design-by-contract annotations
+allow both humans and AI to reason about method behavior
+without reading method implementations. Consider this annotated class:
+
+[source,groovy]
+----
+@Invariant({ balance >= 0 })
+class Account {
+ BigDecimal balance = 0
+ List<String> log = []
+
+ @Requires({ amount > 0 })
+ @Ensures({ balance == old.balance + amount })
+ @Modifies({ [this.balance, this.log] })
+ void deposit(BigDecimal amount) {
+ balance += amount
+ log.add("deposit $amount")
+ }
+
+ @Requires({ amount > 0 && amount <= balance })
+ @Ensures({ balance == old.balance - amount })
+ @Modifies({ [this.balance, this.log] })
+ void withdraw(BigDecimal amount) {
+ balance -= amount
+ log.add("withdraw $amount")
+ }
+
+ @Pure
+ BigDecimal available() { balance }
+}
+----
+
+When analyzing a sequence of calls:
+
+[source,groovy]
+----
+account.deposit(100)
+account.withdraw(30)
+def bal = account.available()
+----
+
+*With annotations*, each call is a self-contained specification -- 3 linear
reasoning steps:
+
+1. `deposit(100)`: `@Requires` met (100 > 0), `@Ensures` gives `balance == old
+ 100`,
+ `@Modifies` proves only `balance` and `log` changed
+2. `withdraw(30)`: `@Requires` met (30 > 0, 30 <= 100), `@Ensures` gives
`balance == 100 - 30 = 70`,
+ `@Modifies` proves `withdraw` didn't undo the deposit
+3. `available()`: `@Pure` proves no side effects -- just returns `balance` (70)
+
+This is human and AI reasoning that scales!
+
+*Without annotations*, the analyzer must read every method body, verify what
each one
+modifies (2 fields × 3 calls = 6 "did this change?" questions), re-verify
earlier state
+after later calls, and check whether `available()` has hidden side effects.
This grows
+as _O(fields × calls × call_depth)_.
+
+=== Using the checkers
+
+In the examples which follow, we explicitly show declaring the type checker to
use.
+Groovy's compiler customization mechanisms allow you to simplify application of
+such checkers, e.g. make it apply globally using a compiler configuration
script,
+as just one example.
== Checking Regular Expressions
@@ -375,6 +507,13 @@ Null pointer dereferences are one of the most common
sources of runtime errors.
The `NullChecker` performs compile-time null-safety analysis, detecting
potential
null dereferences and null-safety violations before code is executed.
+Languages like Kotlin and Swift distinguish nullable and non-nullable types
throughout
+the type system, eliminating null errors by construction. Groovy takes a more
pragmatic
+approach: flow analysis can catch many null issues without annotations (in
`strict` mode),
+and `@Nullable`/`@NonNull` annotations provide additional precision where
needed.
+This lets you adopt null safety incrementally -- annotate the critical
boundaries first,
+and let flow analysis handle the rest.
+
Two modes are provided:
* `NullChecker` — *Annotation-based*: Checks code annotated with `@Nullable`
and
@@ -674,6 +813,13 @@ condition declarations from the `groovy-contracts` module.
It checks that method
modify the fields and parameters they declare, helping both humans and AI
reason about
what a method changes -- and crucially, what it does not.
+Frame conditions are a well-established concept in formal verification.
Dafny's `modifies`
+clause and JML's `assignable` clause serve the same purpose -- declaring what
a method may
+change so that everything else is known to be preserved. Groovy's `@Modifies`
brings this
+idea to the JDK ecosystem in a pragmatic, opt-in form. For projects already
using JetBrains
+annotations, the checker also recognises `@Contract(mutates = "...")` which
expresses
+similar semantics.
+
This checker is opt-in and only activates for methods annotated with
`@Modifies`:
[source,groovy]
@@ -830,6 +976,17 @@ method bodies -- each method becomes a self-contained
specification.
The `PurityChecker` verifies that `@Pure` methods have no side effects. By
default, strict
purity is enforced. The `allows` option declares which effect categories are
tolerated.
+Languages like Haskell enforce purity through the type system -- all side
effects must be
+expressed via IO and State monads, making impure code structurally impossible.
Frege (a
+Haskell dialect for the JVM) takes the same approach. This is powerful but
requires
+incorporating these patterns into your application's type hierarchies, which
can be
+impractical when integrating with JDK libraries that don't follow these
conventions.
+
+Groovy's `PurityChecker` provides an opt-in way to achieve similar levels of
reasoning about
+method purity while staying pragmatic about the JDK ecosystem. The `allows`
option
+acknowledges that real-world "pure" methods sometimes log or emit metrics --
effects that
+don't affect program correctness but would violate strict Haskell-style purity.
+
=== Basic usage
[source,groovy]