This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git
The following commit(s) were added to refs/heads/asf-site by this push:
new 2533259 blog post about null checker proposal
2533259 is described below
commit 253325994a6e34d3142d7a173293c5625880186b
Author: Paul King <[email protected]>
AuthorDate: Thu Apr 2 08:02:52 2026 +1000
blog post about null checker proposal
---
site/src/site/blog/groovy-null-checker.adoc | 454 ++++++++++++++++++++++++++++
1 file changed, 454 insertions(+)
diff --git a/site/src/site/blog/groovy-null-checker.adoc
b/site/src/site/blog/groovy-null-checker.adoc
new file mode 100644
index 0000000..17fd9d4
--- /dev/null
+++ b/site/src/site/blog/groovy-null-checker.adoc
@@ -0,0 +1,454 @@
+= Compile-time null safety for Groovy™
+Paul King <paulk-asert|PMC_Member>
+:revdate: 2026-04-02T10:00:00+00:00
+:keywords: null safety, type checking, static analysis, annotations
+:description: This post looks at a proposed type-checking extension for Groovy
\
+which catches null-safety violations at compile time.
+
+== Introduction
+
+A proposed enhancement, targeted for Groovy 6,
+adds compile-time null-safety analysis as a type-checking extension
+(https://issues.apache.org/jira/browse/GROOVY-11894[GROOVY-11894],
+https://github.com/apache/groovy/pull/2426[PR \#2426]).
+Inspired by the
+https://checkerframework.org/manual/#nullness-checker[Checker Framework],
+https://jspecify.dev/[JSpecify], and similar tools in Kotlin and C#,
+the proposal catches null dereferences, unsafe assignments, and
+missing null checks _before_ your code ever runs.
+
+The extension plugs into Groovy's existing `@TypeChecked`
+infrastructure — no new compiler plugins, no separate build step,
+just an annotation on the classes or methods you want checked.
+You can also use a
+https://docs.groovy-lang.org/latest/html/documentation/#_configscript_example_static_compilation_by_default[compiler
configuration script]
+to apply it across your entire codebase without needing
+to explicitly add the `@TypeChecked` annotations.
+
+This post walks through a series of bite-sized examples showing
+what the day-to-day experience would feel like.
+To make things concrete, the examples follow a running theme:
+building the backend for _The Groovy Shelf_, a fictitious online
+bookshop where customers browse, reserve, and review books.
+
+== Two levels of strictness
+
+The proposal provides two checkers. Pick the level that suits
+your code:
+
+[cols="2,3,3", options="header"]
+|===
+| Checker | Behaviour | Best for
+
+| `NullChecker`
+| Checks code annotated with `@Nullable` / `@NonNull` (or equivalents).
Unannotated code passes without error.
+| Existing projects adopting null safety incrementally.
+
+| `StrictNullChecker`
+| Everything `NullChecker` does, _plus_ flow-sensitive tracking — even
unannotated `def x = null; x.toString()` is flagged.
+| New code or modules where you want full coverage.
+|===
+
+Both are enabled through `@TypeChecked`:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+class RelaxedCode { /* … */ }
+
+@TypeChecked(extensions = 'groovy.typecheckers.StrictNullChecker')
+class StrictCode { /* … */ }
+----
+
+For code bases with a mix of strictness requirements, apply the
+appropriate checker per class or per method.
+
+== The problem: the billion-dollar mistake at runtime
+
+Tony Hoare famously called null references his
+https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/["billion-dollar
mistake"].
+In Groovy and Java, nothing stops you from writing:
+
+[source,groovy]
+----
+String name = null
+println name.toUpperCase() // NullPointerException at runtime
+----
+
+The code compiles, the tests might even pass if they don't hit that
+path, and the exception surfaces in production. The NullChecker
+proposal moves this class of error to compile time.
+
+== Example 1: looking up a book — `@Nullable` parameters
+
+A customer searches for a book by title. The title might come from
+a form field that wasn't filled in, so the parameter is `@Nullable`:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+Book findBook(@Nullable String title) {
+ if (title != null) {
+ return catalog.search(title.trim()) // ok: inside null guard
+ }
+ return Book.FEATURED
+}
+----
+
+The checker verifies that `title` is only dereferenced inside the
+null guard. Remove the `if` and you get a compile-time error:
+
+[source]
+----
+[Static type checking] - Potential null dereference: 'title' is @Nullable
+----
+
+No runtime surprise — the mistake is caught before the code ships.
+
+== Example 2: greeting a customer — catching null arguments
+
+When a customer places an order, we greet them by name.
+The name is `@NonNull` — it must always be provided:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+class OrderService {
+ static String greet(@NonNull String name) {
+ "Welcome back, $name!"
+ }
+ static void main(String[] args) {
+ greet(null) // compile error
+ }
+}
+----
+
+[source]
+----
+[Static type checking] - Cannot pass null to @NonNull parameter 'name' of
'greet'
+----
+
+The checker also catches returning `null` from a `@NonNull` method
+and assigning `null` to a `@NonNull` field — the same principle
+applied consistently across assignments, parameters, and returns.
+
+== Example 3: safe access patterns — the checker is smart
+
+Groovy already offers the safe-navigation operator (`?.`) for
+working with nullable values. The NullChecker understands it,
+along with several other patterns:
+
+*Safe navigation:*
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+String displayTitle(@Nullable String title) {
+ title?.toUpperCase() // ok: safe navigation
+}
+----
+
+*Null guards:*
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+String formatTitle(@Nullable String title) {
+ if (title != null) {
+ return title.toUpperCase() // ok: null guard
+ }
+ return 'Untitled'
+}
+----
+
+*Early exit:*
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+String formatTitle(@Nullable String title) {
+ if (title == null) return 'Untitled' // early exit
+ title.toUpperCase() // ok: title is non-null here
+}
+----
+
+*Elvis assignment:*
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.StrictNullChecker')
+static main(args) {
+ def title = null
+ title ?= 'Untitled'
+ title.toUpperCase() // ok: elvis cleared nullable
state
+}
+----
+
+The checker performs the same kind of narrowing that a human reader
+does: once you've ruled out null — whether by an `if`, an early
+`return`, a `throw`, or an elvis assignment — the variable is safe.
+
+== Example 4: non-null by default — less annotation noise
+
+Annotating every parameter and field gets tedious. Class-level
+defaults let you flip the polarity: everything is `@NonNull` unless
+you say otherwise with `@Nullable`:
+
+[source,groovy]
+----
+@NonNullByDefault
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+class BookService {
+ String name // implicitly @NonNull
+
+ static String formatISBN(String isbn) { // isbn is implicitly @NonNull
+ "ISBN: $isbn"
+ }
+
+ static void main(String[] args) {
+ formatISBN(null) // compile error
+ }
+}
+----
+
+[source]
+----
+[Static type checking] - Cannot pass null to @NonNull parameter 'isbn' of
'formatISBN'
+----
+
+The checker recognises several class-level annotations for this:
+
+* `@NonNullByDefault` (SpotBugs, Eclipse JDT)
+* `@NullMarked` (https://jspecify.dev/[JSpecify])
+* `@ParametersAreNonnullByDefault` (JSR-305 — parameters only)
+
+JSpecify's `@NullUnmarked` can be applied to a nested class to opt
+out of a surrounding `@NullMarked` scope.
+
+=== Integration with `@NullCheck`
+
+Groovy's existing `@NullCheck` annotation generates _runtime_ null
+checks for method parameters. The NullChecker complements this by
+catching violations at _compile_ time:
+
+[source,groovy]
+----
+@NullCheck
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+class Greeter {
+ static String greet(String name) {
+ "Hello, $name!"
+ }
+ static void main(String[] args) {
+ greet(null) // caught at compile time
+ }
+}
+----
+
+With `@NullCheck` on the class, the checker treats all non-primitive
+parameters as effectively `@NonNull`. You still get the runtime
+guard as a safety net, but now you also get a compile-time error
+alerting you before the code ever executes. Parameters explicitly
+annotated `@Nullable` override this behaviour.
+
+== Example 5: lazy initialisation — `@MonotonicNonNull` and `@Lazy`
+
+Some fields start as `null` but, once initialised, should never be
+`null` again. The `@MonotonicNonNull` annotation expresses this
+"write once, then non-null forever" contract:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+class RecommendationEngine {
+ @MonotonicNonNull String cachedResult
+
+ String getRecommendation() {
+ if (cachedResult != null) {
+ return cachedResult.toUpperCase() // ok: null guard
+ }
+ cachedResult = 'Groovy in Action'
+ return cachedResult.toUpperCase() // ok: just assigned non-null
+ }
+}
+----
+
+The checker treats `@MonotonicNonNull` fields as nullable (requiring
+a null guard before use) but prevents re-assignment to `null` after
+initialisation:
+
+[source,groovy]
+----
+void reset() {
+ cachedResult = 'something'
+ cachedResult = null // compile error
+}
+----
+
+[source]
+----
+[Static type checking] - Cannot assign null to @MonotonicNonNull variable
'cachedResult' after non-null assignment
+----
+
+Groovy's `@Lazy` annotation is implicitly treated as
+`@MonotonicNonNull`. Since `@Lazy` generates a getter that handles
+initialisation automatically, property access through the getter is
+always safe and won't trigger null dereference warnings.
+
+== Example 6: going strict — flow-sensitive analysis
+
+The standard `NullChecker` only flags issues involving annotated
+code — unannotated code passes silently. The `StrictNullChecker`
+goes further, tracking nullability through assignments and control
+flow even without annotations:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.StrictNullChecker')
+static main(args) {
+ def x = null
+ x.toString() // compile error
+}
+----
+
+[source]
+----
+[Static type checking] - Potential null dereference: 'x' may be null
+----
+
+The checker tracks nullability through ternary expressions, elvis
+expressions, method return values, and reassignments. Assigning a
+non-null value clears the nullable state:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.StrictNullChecker')
+static main(args) {
+ def x = null
+ x = 'hello'
+ assert x.toString() == 'hello' // ok: reassigned non-null
+}
+----
+
+This is ideal for new modules where you want comprehensive null
+coverage from the start, without annotating every declaration.
+
+== Annotation compatibility
+
+The checker matches annotations by _simple name_, not by fully-qualified
+class name. This means it works with annotations from any library:
+
+[cols="2,3", options="header"]
+|===
+| Library | Annotations
+
+| https://jspecify.dev/[JSpecify]
+| `@Nullable`, `@NullMarked`, `@NullUnmarked`
+
+| JSR-305 (`javax.annotation`)
+| `@Nullable`, `@Nonnull`, `@ParametersAreNonnullByDefault`
+
+| JetBrains
+| `@Nullable`, `@NotNull`
+
+| SpotBugs / FindBugs
+| `@Nullable`, `@NonNull`, `@NonNullByDefault`
+
+| Checker Framework
+| `@Nullable`, `@NonNull`, `@MonotonicNonNull`
+|===
+
+If you prefer not to add an external dependency, you can define
+your own minimal annotations — the checker only cares about the
+simple name:
+
+[source,groovy]
+----
+@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
+@Retention(RetentionPolicy.CLASS)
+@interface Nullable {}
+
+@Target([ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD])
+@Retention(RetentionPolicy.CLASS)
+@interface NonNull {}
+----
+
+== How this compares to Java approaches
+
+Java developers wanting compile-time null safety currently reach
+for external tools — the
+https://checkerframework.org/manual/#nullness-checker[Checker Framework],
+https://errorprone.info/[Error Prone], or IDE-specific inspections
+(IntelliJ, Eclipse). Each brings its own setup, annotation flavour,
+and build integration.
+
+Groovy's NullChecker offers several advantages:
+
+* **Zero setup.** It's a type-checking extension — add one
+ annotation and you're done. No annotation processor configuration,
+ no extra compiler flags, no Gradle plugin.
+* **Works with any annotation library.** Simple-name matching means
+ you can use JSpecify, JSR-305, JetBrains, SpotBugs, Checker
+ Framework, or your own — interchangeably.
+* **Understands Groovy idioms.** Safe navigation (`?.`), elvis
+ assignment (`?=`), `@Lazy` fields, and Groovy truth are all
+ recognised. A Java-only tool can't help here.
+* **Two-tier strictness.** Start with annotation-only checking on
+ existing code, then enable flow-sensitive mode for new modules —
+ no all-or-nothing migration.
+* **Complements `@NullCheck`.** Catch violations at compile time
+ while keeping the runtime guard as a safety net.
+
+== The full picture
+
+The examples above cover the most common scenarios. The complete
+proposal also includes detection of nullable method return value
+dereferences, `@Nullable` values flowing into `@NonNull` parameters
+through variables, nullable propagation in ternary and elvis
+expressions, and integration with JSpecify's `@NullMarked` /
+`@NullUnmarked` scoping. The full spec is available in the
+https://github.com/apache/groovy/pull/2426[PR].
+
+For complementary null-related checks — such as detecting broken
+null-check logic, unnecessary null guards before `instanceof`, or
+`Boolean` methods returning `null` — consider using
+https://codenarc.org/[CodeNarc]'s null-related rules alongside
+these type checkers.
+
+== We'd love your feedback
+
+The NullChecker feature is currently a proposal in
+https://github.com/apache/groovy/pull/2426[PR #2426]
+(tracking issue
+https://issues.apache.org/jira/browse/GROOVY-11894[GROOVY-11894]).
+Null safety is a foundational concern, and we want to get the
+design right.
+
+* *Comment* on the https://github.com/apache/groovy/pull/2426[PR] or
+ the https://issues.apache.org/jira/browse/GROOVY-11894[JIRA issue]
+ with your thoughts, use cases, or design suggestions.
+* *Vote* on the JIRA issue if you'd like to see this feature land.
+
+Your feedback helps us gauge interest and shape the final design.
+
+== Conclusion
+
+Through our _Groovy Shelf_ bookshop examples we've seen how the
+proposed NullChecker catches null dereferences, unsafe assignments,
+and missing null checks at compile time — from looking up books
+with nullable titles, to enforcing non-null parameters, recognising
+Groovy's safe-navigation idioms, applying class-level defaults with
+`@NonNullByDefault`, handling lazy initialisation with
+`@MonotonicNonNull`, and tracking nullability through control flow
+with the `StrictNullChecker`. The setup is minimal, the annotation
+compatibility is broad, and the two-tier strictness model lets you
+adopt null safety at your own pace.
+
+== References
+
+* https://github.com/apache/groovy/pull/2426[PR #2426 — NullChecker
type-checking extension]
+* https://issues.apache.org/jira/browse/GROOVY-11894[GROOVY-11894 — Tracking
issue]
+* https://jspecify.dev/[JSpecify — Standard Java annotations for null safety]
+* https://checkerframework.org/manual/#nullness-checker[Checker Framework —
Nullness Checker]
+*
https://groovy-lang.org/objectorientation.html#_safe_navigation_operator[Groovy
safe navigation operator]
+* https://codenarc.org/[CodeNarc]