This is an automated email from the ASF dual-hosted git repository. paulk-asert pushed a commit to branch groovy12039 in repository https://gitbox.apache.org/repos/asf/groovy.git
commit a604e3e7abe4a970778f42439ed88bfe589ad4f5 Author: Paul King <[email protected]> AuthorDate: Mon May 25 16:23:09 2026 +1000 GROOVY-12039: Graduate RegexChecker and FormatStringChecker from incubating to stable --- src/spec/doc/_type-checking-extensions.adoc | 15 +++- .../src/spec/doc/_monadic-comprehensions.adoc | 18 +++++ .../groovy/groovy/typecheckers/FormatMethod.groovy | 2 + .../groovy/typecheckers/FormatStringChecker.groovy | 4 +- .../groovy/typecheckers/MonadicChecker.groovy | 4 +- .../groovy/groovy/typecheckers/RegexChecker.groovy | 4 +- .../groovy/groovy/typecheckers/package-info.groovy | 35 ++++++++- .../src/spec/doc/typecheckers.adoc | 84 +++++++++++++++++++++- 8 files changed, 156 insertions(+), 10 deletions(-) diff --git a/src/spec/doc/_type-checking-extensions.adoc b/src/spec/doc/_type-checking-extensions.adoc index b496d68e20..d74374476b 100644 --- a/src/spec/doc/_type-checking-extensions.adoc +++ b/src/spec/doc/_type-checking-extensions.adoc @@ -19,8 +19,18 @@ ////////////////////////////////////////// +[[type-checking-extensions]] = Type checking extensions +[TIP] +==== +This chapter is the SDK reference for *writing* a type checking extension. +For the extensions Groovy already ships with (regex checking, format-string +checking, null safety, frame conditions, purity, parallel-reduction +associativity, and monadic comprehensions), see the +<<built-in-auxiliary-type-checkers,Built-in auxiliary type checkers>> chapter. +==== + == Writing a type checking extension === Towards a smarter type checker @@ -114,7 +124,10 @@ use those type checking extension scripts. ==== Parameterized extensions -Extensions can accept named parameters using Groovy's named-argument style within the extension string: +Extensions can accept named parameters using Groovy's named-argument style within the extension string. +The example below uses `NullChecker`, one of the +<<built-in-auxiliary-type-checkers,bundled checkers>>; the same syntax applies +to any extension that reads the `options` map. [source,groovy] ------------------------------------------------------ diff --git a/subprojects/groovy-macro-library/src/spec/doc/_monadic-comprehensions.adoc b/subprojects/groovy-macro-library/src/spec/doc/_monadic-comprehensions.adoc index b3d92dba9d..b7b7169128 100644 --- a/subprojects/groovy-macro-library/src/spec/doc/_monadic-comprehensions.adoc +++ b/subprojects/groovy-macro-library/src/spec/doc/_monadic-comprehensions.adoc @@ -156,6 +156,24 @@ checking error reporting that some operation cannot be found on `Object` `Object`). Adding `extensions = 'groovy.typecheckers.MonadicChecker'` to the `@CompileStatic`/`@TypeChecked` annotation resolves it. +=== Lint for hand-written chains: MonadicShapeChecker + +If you write `flatMap` / `map` chains by hand (without `DO`) over the same +participating carriers, the sister `MonadicShapeChecker` extension provides +a compile-time lint: + +[source,groovy] +---- +@TypeChecked(extensions = 'groovy.typecheckers.MonadicShapeChecker') +---- + +It catches the two classic shape mistakes — a `bind` (i.e. `flatMap`) +closure that returns a non-carrier value, and a `map` closure that returns +the same carrier where `flatMap` was meant. A `strict` option tightens +what is flagged; the default lenient mode reports only high-confidence +violations. Both checkers are documented from the type-checker side in the +<<built-in-auxiliary-type-checkers,Built-in auxiliary type checkers>> chapter. + [[monadic-when]] == When to use `DO` diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatMethod.groovy b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatMethod.groovy index 0e3b5410a5..74b5053419 100644 --- a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatMethod.groovy +++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatMethod.groovy @@ -37,6 +37,8 @@ import java.lang.annotation.Target * * The {@code FormatStringChecker} ensures that the format string is valid and the * remaining arguments are compatible with the embedded format specifiers. + * + * @since 5.0.0 */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy index e77bfca59c..a08a7e7c01 100644 --- a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy +++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/FormatStringChecker.groovy @@ -18,7 +18,6 @@ */ package groovy.typecheckers -import org.apache.groovy.lang.annotation.Incubating import org.apache.groovy.typecheckers.CheckingVisitor import org.codehaus.groovy.ast.ASTNode import org.codehaus.groovy.ast.ClassNode @@ -65,8 +64,9 @@ import static org.codehaus.groovy.ast.ClassHelper.makeCached * https://checkerframework.org/manual/#formatter-checker * https://homes.cs.washington.edu/~mernst/pubs/format-string-issta2014-abstract.html * https://github.com/typetools/checker-framework/tree/master/checker/src/main/java/org/checkerframework/checker/formatter + * + * @since 5.0.0 */ -@Incubating class FormatStringChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL { private static final ClassNode LOCALE_TYPE = makeCached(Locale) diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/MonadicChecker.groovy b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/MonadicChecker.groovy index 50f94ba77a..94437327de 100644 --- a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/MonadicChecker.groovy +++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/MonadicChecker.groovy @@ -18,12 +18,13 @@ */ package groovy.typecheckers +import org.apache.groovy.lang.annotation.Incubating +import org.apache.groovy.runtime.MonadicCarrierRegistry import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.GenericsType import org.codehaus.groovy.ast.MethodNode import org.codehaus.groovy.ast.expr.ClosureExpression import org.codehaus.groovy.ast.expr.MethodCallExpression -import org.apache.groovy.runtime.MonadicCarrierRegistry import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport import org.codehaus.groovy.transform.stc.StaticTypesMarker @@ -61,6 +62,7 @@ import static org.objectweb.asm.Opcodes.ACC_BRIDGE * * Activate with {@code @CompileStatic(extensions='groovy.typecheckers.MonadicChecker')}. */ +@Incubating class MonadicChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL { private static final String DISPATCHER = 'org.apache.groovy.runtime.Comprehensions' diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy index 04d671a401..69b8896dce 100644 --- a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy +++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/RegexChecker.groovy @@ -18,7 +18,6 @@ */ package groovy.typecheckers -import org.apache.groovy.lang.annotation.Incubating import org.apache.groovy.typecheckers.CheckingVisitor import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode @@ -99,8 +98,9 @@ import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.checkC * https://checkerframework.org/manual/#regex-checker * https://homes.cs.washington.edu/~mernst/pubs/regex-types-ftfjp2012.pdf * https://github.com/typetools/checker-framework/tree/master/checker/src/main/java/org/checkerframework/checker/regex + * + * @since 4.0.0 */ -@Incubating class RegexChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL { private static final ClassNode MATCHER_TYPE = ClassHelper.make(Matcher) diff --git a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/package-info.groovy b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/package-info.groovy index 78423b9ef1..e8a38d84fc 100644 --- a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/package-info.groovy +++ b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/package-info.groovy @@ -18,11 +18,40 @@ */ /** - * Custom type checking extensions for compile-time validation. + * Custom type checking extensions for compile-time validation, activated via + * {@code @TypeChecked(extensions = '...')} or {@code @CompileStatic(extensions = '...')}. * * <p> - * FormatStringChecker, NullChecker, RegexChecker, PurityChecker extend type checking - * capabilities for DSLs and domain-specific validation. + * Stable checkers: * </p> + * <ul> + * <li>{@link groovy.typecheckers.RegexChecker} – validates regex patterns and + * group counts at compile time</li> + * <li>{@link groovy.typecheckers.FormatStringChecker} – validates + * {@code printf} / {@code String.format} specifiers against argument types, + * paired with the {@link groovy.typecheckers.FormatMethod @FormatMethod} marker + * annotation for user-defined format methods</li> + * </ul> + * <p> + * Incubating in 6.0.0 (semantics may evolve in a subsequent 6.x release): + * </p> + * <ul> + * <li>{@link groovy.typecheckers.NullChecker} – null-safety analysis using + * {@code @Nullable} / {@code @NonNull} / {@code @MonotonicNonNull} annotations, + * with an optional strict flow-sensitive mode</li> + * <li>{@link groovy.typecheckers.ModifiesChecker} – verifies method bodies + * comply with their {@code groovy.contracts.@Modifies} frame conditions</li> + * <li>{@link groovy.typecheckers.PurityChecker} – enforces that {@code @Pure} + * methods have no side effects, with configurable {@code allows} categories</li> + * <li>{@link groovy.typecheckers.CombinerChecker} – verifies that the combiner + * passed to a parallel reduction ({@code sumParallel}, {@code injectParallel}, + * {@code Stream.reduce}) carries the associativity contract those methods + * silently require</li> + * <li>{@link groovy.typecheckers.MonadicChecker} and + * {@link groovy.typecheckers.MonadicShapeChecker} – type-checking support + * for the {@code DO} macro and hand-written monadic chains over the standard + * carriers ({@code Optional}, {@code Stream}, {@code Awaitable}) and + * {@code @Monadic}-annotated types</li> + * </ul> */ package groovy.typecheckers; diff --git a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc index 1d82136bd8..a625747562 100644 --- a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc +++ b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc @@ -23,6 +23,7 @@ ifndef::reldir_typecheckers[] endif::[] :icons: font +[[built-in-auxiliary-type-checkers]] = Built-in auxiliary type checkers == Introduction @@ -31,7 +32,10 @@ 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 custom checkers, +This chapter catalogues the type-checking extensions that ship with Groovy. +For the API used to write your own, see the +<<type-checking-extensions,Type checking extensions>> chapter. + Groovy offers a suite of built-in type checkers that provide additional compile-time verification for specific concerns: @@ -96,12 +100,38 @@ a| Verifies a combiner passed to a parallel reduction carries the associativity list.injectParallel(0, Maths.&add) [.green]#// icon:check[] @Associative method# list.injectParallel(0) { a, b -> a - b } [.red]#// icon:times[] non-associative: undefined result# ---- + +| <<Checking the DO Macro (Incubating),MonadicChecker>> +a| Teaches `@CompileStatic`/`@TypeChecked` about the `DO` macro's desugared output so monadic comprehensions over `Optional`, `Stream`, `Awaitable`, and `@Monadic` types type-check correctly + +[listing,subs="+quotes,macros"] +---- +@CompileStatic(extensions = 'groovy.typecheckers.MonadicChecker') +def m = DO { x <- Optional.of(1); yield x + 1 } +---- + +| <<Checking Hand-written Monadic Chains (Incubating),MonadicShapeChecker>> +a| Lint for hand-written `flatMap`/`map` chains over the standard carriers -- catches `bind` returning a non-carrier or `map` returning the same carrier + +[listing,subs="+quotes,macros"] +---- +Optional.of(1).flatMap { x -> x + 1 } [.red]#// icon:times[] bind returned Integer, not Optional# +Optional.of(1).map { x -> Optional.of(x) } [.red]#// icon:times[] map returned Optional, use flatMap# +---- |=== 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. +[NOTE] +==== +`RegexChecker` (since Groovy 4.0.0) and `FormatStringChecker` (since Groovy 5.0.0) +are stable. The remaining checkers are new in Groovy 6.0.0 and remain incubating; +their options and semantics may evolve in a subsequent 6.x release based on +usage feedback. +==== + === Why these checkers matter In a world which seems to be having an ever-increasing focus on AI, @@ -183,6 +213,8 @@ as just one example. == Checking Regular Expressions +_Stable since Groovy 4.0.0._ + A regular expression (regex) defines a search pattern for text. The pattern can be as simple as a single character, a fixed string, or a complex expression containing special characters describing a pattern. The JDK has special regex classes and Groovy adds some special syntactic sugar and functionality on top of the JDK classes. @@ -341,6 +373,8 @@ Over-and-above these examples, detected errors include: == Checking Format Strings +_Stable since Groovy 5.0.0._ + The `format` methods in `java.util.Formatter`, and other similar methods, support formatted printing in the style of C's `printf` method with a format string and zero or more arguments. @@ -1391,3 +1425,51 @@ This is a safety net, not a guarantee: A parallel combiner should normally also be side-effect-free; pairing this checker with `<<Checking @Pure Purity (Incubating),PurityChecker>>` (which already treats `@Memoized` methods as purity-obligated) gives both guarantees. + +== Checking the DO Macro (Incubating) + +_New in Groovy 6.0.0._ + +The `MonadicChecker` type-checking extension supports the `DO` macro for +monadic comprehensions, teaching `@CompileStatic` / `@TypeChecked` how to +look through the macro's desugared output (a runtime-dispatched +`Comprehensions.bind` / `Comprehensions.map` chain). Without it, generator +bound names erase to `Object` and the body fails to type-check. + +[source,groovy] +---- +@CompileStatic(extensions = 'groovy.typecheckers.MonadicChecker') +def addOne = DO { x <- Optional.of(1); yield x + 1 } +---- + +The checker also rejects a non-participating carrier with a compile error +naming the missing shape (the carrier must be in the registry allow-list, +expose structural `flatMap` / `map`, or be annotated with `@Monadic`). + +The macro itself, its desugaring strategy, the participating-carrier rules, +and the `@Monadic` annotation are documented in the +<<monadic-comprehensions,Monadic Comprehensions>> chapter. This section +covers only the type-checking surface. + +== Checking Hand-written Monadic Chains (Incubating) + +_New in Groovy 6.0.0._ + +The `MonadicShapeChecker` is a lint for monadic chains you write by hand +(without the `DO` macro) over the standard carriers (`Optional`, `Stream`, +`Awaitable`). It catches the two classic shape mistakes: + +* `bind` (a.k.a. `flatMap`) closure returning a non-carrier value +* `map` closure returning the same carrier, where `flatMap` was meant + +Activate via: + +[source,groovy] +---- +@TypeChecked(extensions = 'groovy.typecheckers.MonadicShapeChecker') +---- + +The checker has a `strict` option that tightens what it flags; in lenient +(default) mode only high-confidence violations are reported. The sister +`MonadicChecker` (above) covers the `DO`-macro path; this checker covers +the hand-written path.
