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 132bc66de2 GROOVY-11908: Support parameterized type checking extensions
132bc66de2 is described below

commit 132bc66de2559eff4fc2d458d4aae9770883b58f
Author: Paul King <[email protected]>
AuthorDate: Tue Apr 7 06:47:04 2026 +1000

    GROOVY-11908: Support parameterized type checking extensions
---
 .../stc/GroovyTypeCheckingExtensionSupport.java    |  72 +++-
 .../transform/stc/TypeCheckingExtension.java       |  25 ++
 src/spec/doc/_type-checking-extensions.adoc        |  42 +++
 .../groovy/groovy/typecheckers/NullChecker.groovy  | 331 +++++++++++++++++-
 .../groovy/typecheckers/StrictNullChecker.groovy   |  61 ----
 .../groovy/typecheckers/NullCheckingVisitor.groovy | 372 ---------------------
 .../src/spec/doc/typecheckers.adoc                 |  16 +-
 .../src/spec/test/NullCheckerTest.groovy           |   4 +-
 .../groovy/typecheckers/NullCheckerTest.groovy     |   4 +-
 9 files changed, 467 insertions(+), 460 deletions(-)

diff --git 
a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
 
b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
index e837def17d..1aa2586137 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
@@ -49,6 +49,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -85,6 +86,12 @@ public class GroovyTypeCheckingExtensionSupport extends 
AbstractTypeCheckingExte
 
     private final String scriptPath;
 
+    /** The class/script path with any parameter suffix removed. */
+    private final String resolvedPath;
+
+    /** Parameters parsed from the extension spec, e.g. {@code (strict: 
true)}. */
+    private final Map<String, Object> extensionParameters;
+
     private final CompilationUnit compilationUnit;
 
     private TypeCheckingExtension delegateExtension;
@@ -94,9 +101,11 @@ public class GroovyTypeCheckingExtensionSupport extends 
AbstractTypeCheckingExte
 
     /**
      * Builds a type checking extension relying on a Groovy script (type 
checking DSL).
+     * The {@code scriptPath} may include parameters in Groovy named-argument 
style, e.g.
+     * {@code "groovy.typecheckers.NullChecker(strict: true)"}.
      *
      * @param typeCheckingVisitor the type checking visitor
-     * @param scriptPath the path to the type checking script (in classpath)
+     * @param scriptPath the path to the type checking script (in classpath), 
optionally with parameters
      * @param compilationUnit
      */
     public GroovyTypeCheckingExtensionSupport(
@@ -105,6 +114,54 @@ public class GroovyTypeCheckingExtensionSupport extends 
AbstractTypeCheckingExte
         super(typeCheckingVisitor);
         this.scriptPath = scriptPath;
         this.compilationUnit = compilationUnit;
+        int parenIdx = scriptPath.indexOf('(');
+        if (parenIdx >= 0 && scriptPath.endsWith(")")) {
+            this.resolvedPath = scriptPath.substring(0, parenIdx).trim();
+            this.extensionParameters = 
parseParameters(scriptPath.substring(parenIdx + 1, scriptPath.length() - 1));
+        } else {
+            this.resolvedPath = scriptPath;
+            this.extensionParameters = Collections.emptyMap();
+        }
+    }
+
+    /**
+     * Parses a parameter string of the form {@code "key1: value1, key2: 
value2"} into a map.
+     * Supports boolean, integer, long, double, and string (quoted) values.
+     * String values cannot contain commas or colons since these are used as 
delimiters.
+     */
+    private static Map<String, Object> parseParameters(final String paramStr) {
+        if (paramStr == null || paramStr.isBlank()) return 
Collections.emptyMap();
+        Map<String, Object> result = new LinkedHashMap<>();
+        for (String pair : paramStr.split(",")) {
+            pair = pair.trim();
+            if (pair.isEmpty()) continue;
+            int colonIdx = pair.indexOf(':');
+            if (colonIdx < 0) continue;
+            String key = pair.substring(0, colonIdx).trim();
+            String value = pair.substring(colonIdx + 1).trim();
+            result.put(key, parseValue(value));
+        }
+        return Collections.unmodifiableMap(result);
+    }
+
+    private static Object parseValue(final String value) {
+        if ("true".equals(value)) return Boolean.TRUE;
+        if ("false".equals(value)) return Boolean.FALSE;
+        if ("null".equals(value)) return null;
+        if (value.length() >= 2
+                && ((value.startsWith("'") && value.endsWith("'"))
+                 || (value.startsWith("\"") && value.endsWith("\"")))) {
+            return value.substring(1, value.length() - 1);
+        }
+        try { return Integer.valueOf(value); } catch (NumberFormatException 
ignored) { }
+        try { return Long.valueOf(value); } catch (NumberFormatException 
ignored) { }
+        try { return Double.valueOf(value); } catch (NumberFormatException 
ignored) { }
+        return value;
+    }
+
+    @Override
+    public Map<String, Object> getOptions() {
+        return extensionParameters;
     }
 
     @Override
@@ -144,7 +201,7 @@ public class GroovyTypeCheckingExtensionSupport extends 
AbstractTypeCheckingExte
         // since Groovy 2.2, it is possible to use FQCN for type checking 
extension scripts
         TypeCheckingDSL script = null;
         try {
-            Class<?> clazz = transformLoader.loadClass(scriptPath, false, 
true);
+            Class<?> clazz = transformLoader.loadClass(resolvedPath, false, 
true);
             if (TypeCheckingDSL.class.isAssignableFrom(clazz)) {
                 script = (TypeCheckingDSL) 
clazz.getDeclaredConstructor().newInstance();
             } else if (TypeCheckingExtension.class.isAssignableFrom(clazz)) {
@@ -152,6 +209,7 @@ public class GroovyTypeCheckingExtensionSupport extends 
AbstractTypeCheckingExte
                 try {
                     Constructor<?> declaredConstructor = 
clazz.getDeclaredConstructor(StaticTypeCheckingVisitor.class);
                     delegateExtension = (TypeCheckingExtension) 
declaredConstructor.newInstance(typeCheckingVisitor);
+                    delegateExtension.setOptions(extensionParameters);
                     
typeCheckingVisitor.addTypeCheckingExtension(delegateExtension);
                     delegateExtension.setup();
                     return;
@@ -159,7 +217,7 @@ public class GroovyTypeCheckingExtensionSupport extends 
AbstractTypeCheckingExte
                     addLoadingError(config);
                 } catch (NoSuchMethodException e) {
                     context.getErrorCollector().addFatalError(
-                            new SimpleMessage("Static type checking extension 
'" + scriptPath + "' could not be loaded because it doesn't have a constructor 
accepting StaticTypeCheckingVisitor.",
+                            new SimpleMessage("Static type checking extension 
'" + resolvedPath + "' could not be loaded because it doesn't have a 
constructor accepting StaticTypeCheckingVisitor.",
                                     config.getDebug(), 
typeCheckingVisitor.getSourceUnit())
                     );
                 }
@@ -172,20 +230,20 @@ public class GroovyTypeCheckingExtensionSupport extends 
AbstractTypeCheckingExte
         if (script == null) {
             ClassLoader cl = 
typeCheckingVisitor.getSourceUnit().getClassLoader();
             // cast to prevent incorrect @since 1.7 warning
-            InputStream is = 
((ClassLoader)transformLoader).getResourceAsStream(scriptPath);
+            InputStream is = 
((ClassLoader)transformLoader).getResourceAsStream(resolvedPath);
             if (is == null) {
                 // fallback to the source unit classloader
-                is = cl.getResourceAsStream(scriptPath);
+                is = cl.getResourceAsStream(resolvedPath);
             }
             if (is == null) {
                 // fallback to the compiler classloader
                 cl = GroovyTypeCheckingExtensionSupport.class.getClassLoader();
-                is = cl.getResourceAsStream(scriptPath);
+                is = cl.getResourceAsStream(resolvedPath);
             }
             if (is == null) {
                 // if the input stream is still null, we've not found the 
extension
                 context.getErrorCollector().addFatalError(
-                        new SimpleMessage("Static type checking extension '" + 
scriptPath + "' was not found on the classpath.",
+                        new SimpleMessage("Static type checking extension '" + 
resolvedPath + "' was not found on the classpath.",
                                 config.getDebug(), 
typeCheckingVisitor.getSourceUnit()));
             }
             try {
diff --git 
a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java 
b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java
index c662a08158..b408ae7eca 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java
@@ -39,6 +39,7 @@ import org.codehaus.groovy.ast.stmt.ReturnStatement;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * This interface defines a high-level API for handling type checking errors. 
As a dynamic language and a platform
@@ -55,10 +56,34 @@ public class TypeCheckingExtension {
 
     protected final StaticTypeCheckingVisitor typeCheckingVisitor;
 
+    private Map<String, Object> options = Collections.emptyMap();
+
     public TypeCheckingExtension(final StaticTypeCheckingVisitor 
typeCheckingVisitor) {
         this.typeCheckingVisitor = typeCheckingVisitor;
     }
 
+    /**
+     * Returns the options passed to this extension via the parameterized 
extension syntax,
+     * e.g. {@code @TypeChecked(extensions='my.Extension(key: value)')}.
+     *
+     * @return the options map, never null
+     * @since 6.0.0
+     */
+    public Map<String, Object> getOptions() {
+        return options;
+    }
+
+    /**
+     * Sets the options for this extension. Called by the framework when 
loading
+     * a parameterized extension.
+     *
+     * @param options the options map
+     * @since 6.0.0
+     */
+    public void setOptions(final Map<String, Object> options) {
+        this.options = options != null ? options : Collections.emptyMap();
+    }
+
     /**
      * Subclasses should implement this method whenever they need to perform
      * special checks before the type checker starts working.
diff --git a/src/spec/doc/_type-checking-extensions.adoc 
b/src/spec/doc/_type-checking-extensions.adoc
index 334cecaeac..b496d68e20 100644
--- a/src/spec/doc/_type-checking-extensions.adoc
+++ b/src/spec/doc/_type-checking-extensions.adoc
@@ -112,6 +112,48 @@ checker supports multiple mechanisms to implement type 
checking
 extensions (including plain old java code), the recommended way is to
 use those type checking extension scripts.
 
+==== Parameterized extensions
+
+Extensions can accept named parameters using Groovy's named-argument style 
within the extension string:
+
+[source,groovy]
+------------------------------------------------------
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict: true)')
+void foo() { ... }
+------------------------------------------------------
+
+Parameters are passed as `key: value` pairs inside parentheses appended to the 
extension name.
+Supported value types include booleans (`true`/`false`), integers, strings 
(single or double quoted), and `null`.
+Multiple parameters are separated by commas.
+Note that string values cannot contain commas or colons since these are used 
as delimiters:
+
+[source,groovy]
+------------------------------------------------------
+@TypeChecked(extensions = 'com.example.MyChecker(strict: true, threshold: 5, 
mode: "lenient")')
+void foo() { ... }
+------------------------------------------------------
+
+Within an extension, the parameters are available via the `options` property, 
which returns an unmodifiable `Map<String, Object>`:
+
+[source,groovy]
+------------------------------------------------------
+// in a TypeCheckingDSL extension:
+Object run() {
+    boolean strict = options?.strict ?: false
+    // ...
+}
+------------------------------------------------------
+
+For precompiled Java extensions (subclasses of `TypeCheckingExtension` or 
`AbstractTypeCheckingExtension`),
+options are accessible via the `getOptions()` method.
+
+The parameterized syntax also works when configuring extensions 
programmatically:
+
+[source,groovy]
+------------------------------------------------------
+new ASTTransformationCustomizer(TypeChecked, extensions: ['my.Extension(debug: 
true)'])
+------------------------------------------------------
+
 === A DSL for type checking
 The idea behind type checking extensions is to use a DSL to extend the
 type checker capabilities. This DSL allows you to hook into the
diff --git 
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
 
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
index 18f77dcac8..2bb01234a9 100644
--- 
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
+++ 
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
@@ -19,16 +19,47 @@
 package groovy.typecheckers
 
 import org.apache.groovy.lang.annotation.Incubating
-import org.apache.groovy.typecheckers.NullCheckingVisitor
+import org.apache.groovy.typecheckers.CheckingVisitor
+import org.codehaus.groovy.ast.AnnotatedNode
+import org.codehaus.groovy.ast.FieldNode
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.Variable
+import org.codehaus.groovy.ast.expr.BinaryExpression
+import org.codehaus.groovy.ast.expr.CastExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.DeclarationExpression
+import org.codehaus.groovy.ast.expr.EmptyExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.PropertyExpression
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.ast.expr.TernaryExpression
+import org.codehaus.groovy.ast.expr.TupleExpression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.ast.stmt.BlockStatement
+import org.codehaus.groovy.ast.stmt.EmptyStatement
+import org.codehaus.groovy.ast.stmt.IfStatement
+import org.codehaus.groovy.ast.stmt.ReturnStatement
+import org.codehaus.groovy.ast.stmt.Statement
+import org.codehaus.groovy.ast.stmt.ThrowStatement
+import org.codehaus.groovy.syntax.Types
 import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
+import org.codehaus.groovy.transform.stc.StaticTypesMarker
+
+import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType
+import static org.codehaus.groovy.syntax.Types.isAssignment
 
 /**
  * A compile-time type checker that detects potential null dereferences and 
null-safety violations
  * in code annotated with {@code @Nullable}, {@code @NonNull}, and {@code 
@MonotonicNonNull} annotations.
  * <p>
- * This checker performs annotation-based null checking only. For additional 
flow-sensitive analysis
- * that tracks nullability through assignments and control flow (even in 
unannotated code),
- * use {@link StrictNullChecker} instead.
+ * By default, this checker performs annotation-based null checking only. For 
additional flow-sensitive
+ * analysis that tracks nullability through assignments and control flow (even 
in unannotated code),
+ * enable the {@code strict} option:
+ * <pre>
+ * {@code @TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict: 
true)')}
+ * </pre>
  * <p>
  * Supported annotations are recognized by simple name from any package:
  * <ul>
@@ -44,6 +75,7 @@ import 
org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
  *     <li>Dereferencing a {@code @Nullable} variable without a null check or 
safe navigation ({@code ?.})</li>
  *     <li>Dereferencing the result of a {@code @Nullable}-returning method 
without a null check</li>
  *     <li>Re-assigning {@code null} to a {@code @MonotonicNonNull} field 
after initialization</li>
+ *     <li>Dereferencing a variable known to be null through flow analysis 
({@code strict} mode only)</li>
  * </ul>
  * <p>
  * The checker recognizes null guards ({@code if (x != null)}), early exit 
patterns
@@ -62,15 +94,298 @@ import 
org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
  *
  * Over time, the idea would be to support more cases as per:
  * https://checkerframework.org/manual/#nullness-checker
- *
- * @see StrictNullChecker
- * @see NullCheckingVisitor
  */
 @Incubating
 class NullChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
 
+    private static final Set<String> NULLABLE_ANNOS = Set.of('Nullable', 
'CheckForNull', 'MonotonicNonNull')
+    private static final Set<String> NONNULL_ANNOS = Set.of('NonNull', 
'NotNull', 'Nonnull')
+    private static final Set<String> MONOTONIC_ANNOS = 
Set.of('MonotonicNonNull', 'Lazy')
+    private static final Set<String> NULLCHECK_ANNOS = Set.of('NullCheck', 
'ParametersAreNonnullByDefault', 'ParametersAreNonNullByDefault')
+    private static final Set<String> NONNULL_BY_DEFAULT_ANNOS = 
Set.of('NonNullByDefault', 'NonnullByDefault', 'NullMarked')
+    private static final Set<String> NULL_UNMARKED_ANNOS = 
Set.of('NullUnmarked')
+
     @Override
     Object run() {
-        NullCheckingVisitor.install(this, false)
+        boolean strict = options?.strict ?: false
+        afterVisitMethod { MethodNode method ->
+            method.code?.visit(makeVisitor(strict, method))
+        }
+    }
+
+    private CheckingVisitor makeVisitor(boolean flowSensitive, MethodNode 
method) {
+        boolean classNonNullByDefault = method.declaringClass != null && 
hasNonNullByDefaultAnno(method.declaringClass)
+        boolean methodNonNull = method.returnType != VOID_TYPE && 
(hasNonNullAnno(method) || (classNonNullByDefault && !hasNullableAnno(method)))
+        def initialNullable = method.parameters.findAll { hasNullableAnno(it) 
} as Set<Variable>
+
+        new CheckingVisitor() {
+            private final Set<Variable> nullableVars = new 
HashSet<>(initialNullable)
+            private final Set<Variable> monotonicInitialized = new HashSet<>()
+            private final Set<Variable> guardedVars = new HashSet<>()
+
+            @Override
+            void visitDeclarationExpression(DeclarationExpression decl) {
+                super.visitDeclarationExpression(decl)
+                def ve = decl.variableExpression
+                if (ve == null) return
+                if (decl.rightExpression instanceof ConstantExpression) {
+                    localConstVars.put(ve, decl.rightExpression)
+                }
+                if (hasNonNullAnno(ve) && isNullExpr(decl.rightExpression)) {
+                    addStaticTypeError("Cannot assign null to @NonNull 
variable '${ve.name}'", decl)
+                }
+                // Uninitialized non-primitive declaration (e.g. "String x") 
is implicitly null
+                boolean implicitlyNull = decl.rightExpression instanceof 
EmptyExpression && !isPrimitiveType(ve.type)
+                if (hasNullableAnno(ve) || isNullExpr(decl.rightExpression) || 
(flowSensitive && (implicitlyNull || canBeNull(decl.rightExpression) || 
isKnownNullable(decl.rightExpression)))) {
+                    nullableVars.add(ve)
+                }
+                if (flowSensitive) {
+                    trackNullableReturn(ve, decl.rightExpression)
+                }
+            }
+
+            @Override
+            void visitBinaryExpression(BinaryExpression expression) {
+                super.visitBinaryExpression(expression)
+                if (isAssignment(expression.operation.type) && 
expression.leftExpression instanceof VariableExpression) {
+                    def target = findTargetVariable(expression.leftExpression)
+                    boolean fieldNonNull = target instanceof AnnotatedNode && 
hasNonNullAnno(target)
+                    if (!fieldNonNull && target instanceof FieldNode && 
!hasNullableAnno(target)) {
+                        fieldNonNull = target.declaringClass != null && 
hasNonNullByDefaultAnno(target.declaringClass)
+                    }
+                    if (fieldNonNull && 
isNullExpr(expression.rightExpression)) {
+                        addStaticTypeError("Cannot assign null to @NonNull 
variable '${expression.leftExpression.name}'", expression)
+                    }
+                    // @MonotonicNonNull: once initialized with non-null, 
cannot assign null again
+                    if (target instanceof AnnotatedNode && 
hasMonotonicAnno(target)) {
+                        if (!isNullExpr(expression.rightExpression)) {
+                            monotonicInitialized.add(target)
+                        } else if (monotonicInitialized.contains(target)) {
+                            addStaticTypeError("Cannot assign null to 
@MonotonicNonNull variable '${expression.leftExpression.name}' after non-null 
assignment", expression)
+                        }
+                    }
+                    if (isNullExpr(expression.rightExpression)) {
+                        nullableVars.add(target)
+                        guardedVars.remove(target)
+                    } else if (flowSensitive && 
(canBeNull(expression.rightExpression) || 
isKnownNullable(expression.rightExpression))) {
+                        nullableVars.add(target)
+                        guardedVars.remove(target)
+                    } else {
+                        nullableVars.remove(target)
+                        if (flowSensitive) {
+                            trackNullableReturn(target, 
expression.rightExpression)
+                        }
+                    }
+                }
+            }
+
+            @Override
+            void visitMethodCallExpression(MethodCallExpression call) {
+                super.visitMethodCallExpression(call)
+                if (!call.safe && !call.implicitThis) {
+                    checkDereference(call.objectExpression, call)
+                }
+                checkMethodArguments(call)
+            }
+
+            @Override
+            void visitStaticMethodCallExpression(StaticMethodCallExpression 
call) {
+                super.visitStaticMethodCallExpression(call)
+                checkMethodArguments(call)
+            }
+
+            @Override
+            void visitPropertyExpression(PropertyExpression expression) {
+                super.visitPropertyExpression(expression)
+                if (!expression.safe) {
+                    checkDereference(expression.objectExpression, expression)
+                }
+            }
+
+            @Override
+            void visitReturnStatement(ReturnStatement statement) {
+                super.visitReturnStatement(statement)
+                if (methodNonNull) {
+                    if (isNullExpr(statement.expression)) {
+                        addStaticTypeError("Cannot return null from @NonNull 
method '${method.name}'", statement)
+                    } else if (isKnownNullable(statement.expression)) {
+                        addStaticTypeError("Cannot return @Nullable value from 
@NonNull method '${method.name}'", statement)
+                    }
+                }
+            }
+
+            @Override
+            void visitIfElse(IfStatement ifElse) {
+                ifElse.booleanExpression.visit(this)
+                def guard = findNullGuard(ifElse.booleanExpression.expression)
+                if (guard != null) {
+                    handleNullGuard(ifElse, guard.v1, guard.v2)
+                } else {
+                    ifElse.ifBlock.visit(this)
+                    ifElse.elseBlock.visit(this)
+                }
+            }
+
+            
//------------------------------------------------------------------
+
+            private void handleNullGuard(IfStatement ifElse, Variable 
guardVar, boolean isNotNull) {
+                if (isNotNull) {
+                    // if (x != null) { ... } else { ... }
+                    def saved = new HashSet<>(guardedVars)
+                    guardedVars.add(guardVar)
+                    ifElse.ifBlock.visit(this)
+                    guardedVars.clear()
+                    guardedVars.addAll(saved)
+                    ifElse.elseBlock.visit(this)
+                } else {
+                    // if (x == null) { ... } else { ... }
+                    ifElse.ifBlock.visit(this)
+                    if (!(ifElse.elseBlock instanceof EmptyStatement)) {
+                        def saved = new HashSet<>(guardedVars)
+                        guardedVars.add(guardVar)
+                        ifElse.elseBlock.visit(this)
+                        guardedVars.clear()
+                        guardedVars.addAll(saved)
+                    }
+                    // Early exit: if (x == null) return/throw → x is non-null 
after
+                    if (isEarlyExit(ifElse.ifBlock)) {
+                        nullableVars.remove(guardVar)
+                        guardedVars.add(guardVar)
+                    }
+                }
+            }
+
+            private void checkDereference(Expression receiver, Expression 
context) {
+                if (isNullExpr(receiver)) {
+                    addStaticTypeError('Cannot dereference null', context)
+                    return
+                }
+                if (receiver instanceof VariableExpression) {
+                    if (receiver.isThisExpression() || 
receiver.isSuperExpression()) return
+                    def target = findTargetVariable(receiver)
+                    boolean isMonotonicAndInitialized = target instanceof 
AnnotatedNode && hasMonotonicAnno(target) && 
monotonicInitialized.contains(target)
+                    if (target instanceof AnnotatedNode && 
hasNullableAnno(target) && !guardedVars.contains(target) && 
!isMonotonicAndInitialized) {
+                        addStaticTypeError("Potential null dereference: 
'${receiver.name}' is @Nullable", context)
+                    } else if (flowSensitive && nullableVars.contains(target) 
&& !guardedVars.contains(target)) {
+                        addStaticTypeError("Potential null dereference: 
'${receiver.name}' may be null", context)
+                    }
+                } else if (receiver instanceof MethodCallExpression || 
receiver instanceof StaticMethodCallExpression) {
+                    def targetMethod = 
receiver.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+                    if (targetMethod instanceof MethodNode && 
hasNullableAnno(targetMethod)) {
+                        addStaticTypeError("Potential null dereference: 
'${targetMethod.name}()' may return null", context)
+                    }
+                }
+            }
+
+            private void checkMethodArguments(call) {
+                def target = 
call.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+                if (!(target instanceof MethodNode)) return
+                def args = call.arguments
+                if (!(args instanceof TupleExpression)) return
+                def params = target.parameters
+                // @NullCheck/@ParametersAreNonnullByDefault/@NonNullByDefault 
on method or class makes non-primitive params effectively @NonNull
+                def declaringClass = target.declaringClass
+                boolean nullChecked = hasNullCheckAnno(target) || 
hasNonNullByDefaultAnno(target) ||
+                    (declaringClass != null && 
(hasNullCheckAnno(declaringClass) || hasNonNullByDefaultAnno(declaringClass)))
+                int limit = Math.min(args.expressions.size(), params.length)
+                for (int i = 0; i < limit; i++) {
+                    def arg = args.getExpression(i)
+                    boolean paramIsNonNull = hasNonNullAnno(params[i]) || 
(nullChecked && !isPrimitiveType(params[i].type) && !hasNullableAnno(params[i]))
+                    if (paramIsNonNull) {
+                        if (isNullExpr(arg)) {
+                            addStaticTypeError("Cannot pass null to @NonNull 
parameter '${params[i].name}' of '${target.name}'", call)
+                        } else if (isKnownNullable(arg)) {
+                            addStaticTypeError("Cannot pass @Nullable value to 
@NonNull parameter '${params[i].name}' of '${target.name}'", call)
+                        }
+                    }
+                }
+            }
+
+            private boolean isKnownNullable(Expression expr) {
+                if (expr instanceof VariableExpression) {
+                    def target = findTargetVariable(expr)
+                    if (target instanceof AnnotatedNode && 
hasNullableAnno(target)) return true
+                    if (nullableVars.contains(target)) return true
+                }
+                false
+            }
+
+            private void trackNullableReturn(Variable variable, Expression 
rhs) {
+                if (rhs instanceof MethodCallExpression || rhs instanceof 
StaticMethodCallExpression) {
+                    def target = 
rhs.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+                    if (target instanceof MethodNode && 
hasNullableAnno(target)) {
+                        nullableVars.add(variable)
+                    }
+                }
+            }
+
+            private Tuple2<Variable, Boolean> findNullGuard(Expression 
condition) {
+                if (condition instanceof BinaryExpression) {
+                    def op = condition.operation.type
+                    boolean isNotEqual = (op == Types.COMPARE_NOT_EQUAL || op 
== Types.COMPARE_NOT_IDENTICAL)
+                    boolean isEqual = (op == Types.COMPARE_EQUAL || op == 
Types.COMPARE_IDENTICAL)
+                    if (isNotEqual || isEqual) {
+                        Variable var = null
+                        if (isNullExpr(condition.rightExpression) && 
condition.leftExpression instanceof VariableExpression) {
+                            var = findTargetVariable(condition.leftExpression)
+                        } else if (isNullExpr(condition.leftExpression) && 
condition.rightExpression instanceof VariableExpression) {
+                            var = findTargetVariable(condition.rightExpression)
+                        }
+                        if (var != null) return new Tuple2<>(var, isNotEqual)
+                    }
+                }
+                null
+            }
+        }
+    }
+
+    
//--------------------------------------------------------------------------
+
+    private static boolean hasNullableAnno(AnnotatedNode node) {
+        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NULLABLE_ANNOS } ?: false
+    }
+
+    private static boolean hasNonNullAnno(AnnotatedNode node) {
+        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NONNULL_ANNOS } ?: false
+    }
+
+    private static boolean hasMonotonicAnno(AnnotatedNode node) {
+        node.annotations?.any { it.classNode?.nameWithoutPackage in 
MONOTONIC_ANNOS } ?: false
+    }
+
+    private static boolean hasNullCheckAnno(AnnotatedNode node) {
+        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NULLCHECK_ANNOS } ?: false
+    }
+
+    private static boolean hasNonNullByDefaultAnno(AnnotatedNode node) {
+        if (hasNullUnmarkedAnno(node)) return false
+        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NONNULL_BY_DEFAULT_ANNOS } ?: false
+    }
+
+    private static boolean hasNullUnmarkedAnno(AnnotatedNode node) {
+        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NULL_UNMARKED_ANNOS } ?: false
+    }
+
+    private static boolean isNullExpr(Expression expr) {
+        if (expr instanceof ConstantExpression) return ((ConstantExpression) 
expr).isNullExpression()
+        if (expr instanceof CastExpression) return 
isNullExpr(((CastExpression) expr).expression)
+        false
+    }
+
+    private static boolean canBeNull(Expression expr) {
+        if (isNullExpr(expr)) return true
+        if (expr instanceof TernaryExpression) {
+            return canBeNull(expr.trueExpression) || 
canBeNull(expr.falseExpression)
+        }
+        false
+    }
+
+    private static boolean isEarlyExit(Statement stmt) {
+        if (stmt instanceof ReturnStatement || stmt instanceof ThrowStatement) 
return true
+        if (stmt instanceof BlockStatement) {
+            def stmts = stmt.statements
+            return stmts && isEarlyExit(stmts.last())
+        }
+        false
     }
 }
diff --git 
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/StrictNullChecker.groovy
 
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/StrictNullChecker.groovy
deleted file mode 100644
index 722bca5d12..0000000000
--- 
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/StrictNullChecker.groovy
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package groovy.typecheckers
-
-import org.apache.groovy.lang.annotation.Incubating
-import org.apache.groovy.typecheckers.NullCheckingVisitor
-import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
-
-/**
- * A compile-time type checker that performs all the annotation-based null 
checks of {@link NullChecker}
- * plus flow-sensitive null tracking for unannotated code.
- * <p>
- * In addition to the annotation-based checks, this checker tracks nullability 
through variable
- * assignments and control flow. This means it can detect potential null 
dereferences even when
- * code does not use {@code @Nullable}/{@code @NonNull} annotations:
- *
- * <pre>
- * {@code @TypeChecked(extensions = 'groovy.typecheckers.StrictNullChecker')}
- * void process() {
- *     def x = null
- *     // x.toString()   // error: x may be null
- *     x = 'hello'
- *     x.toString()      // ok: x reassigned non-null
- * }
- * </pre>
- * <p>
- * The flow-sensitive tracking also recognizes return values from {@code 
@Nullable} methods
- * assigned to variables, null guards, and early exit patterns.
- * <p>
- * Use this checker for code that benefits from stricter null analysis. For 
code bases
- * with a mix of strictness requirements, apply {@code NullChecker} to relaxed 
code and
- * {@code StrictNullChecker} to strict code via per-class or per-method
- * {@code @TypeChecked} annotations.
- *
- * @see NullChecker
- * @see NullCheckingVisitor
- */
-@Incubating
-class StrictNullChecker extends 
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
-
-    @Override
-    Object run() {
-        NullCheckingVisitor.install(this, true)
-    }
-}
diff --git 
a/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy
 
b/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy
deleted file mode 100644
index 625887855a..0000000000
--- 
a/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy
+++ /dev/null
@@ -1,372 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- */
-package org.apache.groovy.typecheckers
-
-import groovy.lang.Tuple2
-import org.codehaus.groovy.ast.AnnotatedNode
-import org.codehaus.groovy.ast.FieldNode
-import org.codehaus.groovy.ast.MethodNode
-import org.codehaus.groovy.ast.Variable
-import org.codehaus.groovy.ast.expr.BinaryExpression
-import org.codehaus.groovy.ast.expr.ConstantExpression
-import org.codehaus.groovy.ast.expr.DeclarationExpression
-import org.codehaus.groovy.ast.expr.CastExpression
-import org.codehaus.groovy.ast.expr.EmptyExpression
-import org.codehaus.groovy.ast.expr.Expression
-import org.codehaus.groovy.ast.expr.MethodCallExpression
-import org.codehaus.groovy.ast.expr.PropertyExpression
-import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
-import org.codehaus.groovy.ast.expr.TernaryExpression
-import org.codehaus.groovy.ast.expr.TupleExpression
-import org.codehaus.groovy.ast.expr.VariableExpression
-import org.codehaus.groovy.ast.stmt.BlockStatement
-import org.codehaus.groovy.ast.stmt.EmptyStatement
-import org.codehaus.groovy.ast.stmt.IfStatement
-import org.codehaus.groovy.ast.stmt.ReturnStatement
-import org.codehaus.groovy.ast.stmt.Statement
-import org.codehaus.groovy.ast.stmt.ThrowStatement
-import org.codehaus.groovy.syntax.Types
-import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
-import org.codehaus.groovy.transform.stc.StaticTypesMarker
-
-import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE
-import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType
-import static org.codehaus.groovy.syntax.Types.isAssignment
-
-/**
- * Shared visitor implementation for null-safety type checking.
- * Used by both {@code NullChecker} (annotation-only) and {@code 
StrictNullChecker} (annotation + flow-sensitive).
- *
- * @see groovy.typecheckers.NullChecker
- * @see groovy.typecheckers.StrictNullChecker
- */
-class NullCheckingVisitor extends CheckingVisitor {
-
-    private static final Set<String> NULLABLE_ANNOS = Set.of('Nullable', 
'CheckForNull', 'MonotonicNonNull')
-    private static final Set<String> NONNULL_ANNOS = Set.of('NonNull', 
'NotNull', 'Nonnull')
-    private static final Set<String> MONOTONIC_ANNOS = 
Set.of('MonotonicNonNull', 'Lazy')
-    private static final Set<String> NULLCHECK_ANNOS = Set.of('NullCheck', 
'ParametersAreNonnullByDefault', 'ParametersAreNonNullByDefault')
-    private static final Set<String> NONNULL_BY_DEFAULT_ANNOS = 
Set.of('NonNullByDefault', 'NonnullByDefault', 'NullMarked')
-    private static final Set<String> NULL_UNMARKED_ANNOS = 
Set.of('NullUnmarked')
-
-    private final boolean flowSensitive
-    private final Closure errorReporter
-    private final MethodNode method
-    private final boolean methodNonNull
-    private final Set<Variable> nullableVars = new HashSet<>()
-    private final Set<Variable> monotonicInitialized = new HashSet<>()
-    private final Set<Variable> guardedVars = new HashSet<>()
-
-    NullCheckingVisitor(boolean flowSensitive, MethodNode method, Closure 
errorReporter) {
-        this.flowSensitive = flowSensitive
-        this.errorReporter = errorReporter
-        this.method = method
-        boolean classNonNullByDefault = method.declaringClass != null && 
hasNonNullByDefaultAnno(method.declaringClass)
-        this.methodNonNull = method.returnType != VOID_TYPE && 
(hasNonNullAnno(method) || (classNonNullByDefault && !hasNullableAnno(method)))
-        for (param in method.parameters) {
-            if (hasNullableAnno(param)) {
-                nullableVars.add(param)
-            }
-        }
-    }
-
-    /**
-     * Installs the null-checking visitor into a type checking DSL.
-     * Called from the DSL's {@code run()} method to set up the {@code 
afterVisitMethod} callback.
-     *
-     * @param dsl the type checking DSL instance
-     * @param flowSensitive whether to enable flow-sensitive null tracking
-     */
-    static void install(GroovyTypeCheckingExtensionSupport.TypeCheckingDSL 
dsl, boolean flowSensitive) {
-        dsl.afterVisitMethod { MethodNode method ->
-            def reporter = { String msg, node -> dsl.addStaticTypeError(msg, 
node) }
-            method.code?.visit(new NullCheckingVisitor(flowSensitive, method, 
reporter))
-        }
-    }
-
-    
//--------------------------------------------------------------------------
-
-    @Override
-    void visitDeclarationExpression(DeclarationExpression decl) {
-        super.visitDeclarationExpression(decl)
-        def ve = decl.variableExpression
-        if (ve == null) return
-        if (decl.rightExpression instanceof ConstantExpression) {
-            localConstVars.put(ve, decl.rightExpression)
-        }
-        if (hasNonNullAnno(ve) && isNullExpr(decl.rightExpression)) {
-            reportError("Cannot assign null to @NonNull variable 
'${ve.name}'", decl)
-        }
-        // Uninitialized non-primitive declaration (e.g. "String x") is 
implicitly null
-        boolean implicitlyNull = decl.rightExpression instanceof 
EmptyExpression && !isPrimitiveType(ve.type)
-        if (hasNullableAnno(ve) || isNullExpr(decl.rightExpression) || 
(flowSensitive && (implicitlyNull || canBeNull(decl.rightExpression) || 
isKnownNullable(decl.rightExpression)))) {
-            nullableVars.add(ve)
-        }
-        if (flowSensitive) {
-            trackNullableReturn(ve, decl.rightExpression)
-        }
-    }
-
-    @Override
-    void visitBinaryExpression(BinaryExpression expression) {
-        super.visitBinaryExpression(expression)
-        if (isAssignment(expression.operation.type) && 
expression.leftExpression instanceof VariableExpression) {
-            def target = findTargetVariable(expression.leftExpression)
-            boolean fieldNonNull = target instanceof AnnotatedNode && 
hasNonNullAnno(target)
-            if (!fieldNonNull && target instanceof FieldNode && 
!hasNullableAnno(target)) {
-                fieldNonNull = target.declaringClass != null && 
hasNonNullByDefaultAnno(target.declaringClass)
-            }
-            if (fieldNonNull && isNullExpr(expression.rightExpression)) {
-                reportError("Cannot assign null to @NonNull variable 
'${expression.leftExpression.name}'", expression)
-            }
-            // @MonotonicNonNull: once initialized with non-null, cannot 
assign null again
-            if (target instanceof AnnotatedNode && hasMonotonicAnno(target)) {
-                if (!isNullExpr(expression.rightExpression)) {
-                    monotonicInitialized.add(target)
-                } else if (monotonicInitialized.contains(target)) {
-                    reportError("Cannot assign null to @MonotonicNonNull 
variable '${expression.leftExpression.name}' after non-null assignment", 
expression)
-                }
-            }
-            if (isNullExpr(expression.rightExpression)) {
-                nullableVars.add(target)
-                guardedVars.remove(target)
-            } else if (flowSensitive && (canBeNull(expression.rightExpression) 
|| isKnownNullable(expression.rightExpression))) {
-                nullableVars.add(target)
-                guardedVars.remove(target)
-            } else {
-                nullableVars.remove(target)
-                if (flowSensitive) {
-                    trackNullableReturn(target, expression.rightExpression)
-                }
-            }
-        }
-    }
-
-    @Override
-    void visitMethodCallExpression(MethodCallExpression call) {
-        super.visitMethodCallExpression(call)
-        if (!call.safe && !call.implicitThis) {
-            checkDereference(call.objectExpression, call)
-        }
-        checkMethodArguments(call)
-    }
-
-    @Override
-    void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
-        super.visitStaticMethodCallExpression(call)
-        checkMethodArguments(call)
-    }
-
-    @Override
-    void visitPropertyExpression(PropertyExpression expression) {
-        super.visitPropertyExpression(expression)
-        if (!expression.safe) {
-            checkDereference(expression.objectExpression, expression)
-        }
-    }
-
-    @Override
-    void visitReturnStatement(ReturnStatement statement) {
-        super.visitReturnStatement(statement)
-        if (methodNonNull) {
-            if (isNullExpr(statement.expression)) {
-                reportError("Cannot return null from @NonNull method 
'${method.name}'", statement)
-            } else if (isKnownNullable(statement.expression)) {
-                reportError("Cannot return @Nullable value from @NonNull 
method '${method.name}'", statement)
-            }
-        }
-    }
-
-    @Override
-    void visitIfElse(IfStatement ifElse) {
-        ifElse.booleanExpression.visit(this)
-        def guard = findNullGuard(ifElse.booleanExpression.expression)
-        if (guard != null) {
-            handleNullGuard(ifElse, guard.v1, guard.v2)
-        } else {
-            ifElse.ifBlock.visit(this)
-            ifElse.elseBlock.visit(this)
-        }
-    }
-
-    
//--------------------------------------------------------------------------
-
-    private void handleNullGuard(IfStatement ifElse, Variable guardVar, 
boolean isNotNull) {
-        if (isNotNull) {
-            // if (x != null) { ... } else { ... }
-            def saved = new HashSet<>(guardedVars)
-            guardedVars.add(guardVar)
-            ifElse.ifBlock.visit(this)
-            guardedVars.clear()
-            guardedVars.addAll(saved)
-            ifElse.elseBlock.visit(this)
-        } else {
-            // if (x == null) { ... } else { ... }
-            ifElse.ifBlock.visit(this)
-            if (!(ifElse.elseBlock instanceof EmptyStatement)) {
-                def saved = new HashSet<>(guardedVars)
-                guardedVars.add(guardVar)
-                ifElse.elseBlock.visit(this)
-                guardedVars.clear()
-                guardedVars.addAll(saved)
-            }
-            // Early exit: if (x == null) return/throw → x is non-null after
-            if (isEarlyExit(ifElse.ifBlock)) {
-                nullableVars.remove(guardVar)
-                guardedVars.add(guardVar)
-            }
-        }
-    }
-
-    private void checkDereference(Expression receiver, Expression context) {
-        if (isNullExpr(receiver)) {
-            reportError('Cannot dereference null', context)
-            return
-        }
-        if (receiver instanceof VariableExpression) {
-            if (receiver.isThisExpression() || receiver.isSuperExpression()) 
return
-            def target = findTargetVariable(receiver)
-            boolean isMonotonicAndInitialized = target instanceof 
AnnotatedNode && hasMonotonicAnno(target) && 
monotonicInitialized.contains(target)
-            if (target instanceof AnnotatedNode && hasNullableAnno(target) && 
!guardedVars.contains(target) && !isMonotonicAndInitialized) {
-                reportError("Potential null dereference: '${receiver.name}' is 
@Nullable", context)
-            } else if (flowSensitive && nullableVars.contains(target) && 
!guardedVars.contains(target)) {
-                reportError("Potential null dereference: '${receiver.name}' 
may be null", context)
-            }
-        } else if (receiver instanceof MethodCallExpression || receiver 
instanceof StaticMethodCallExpression) {
-            def targetMethod = 
receiver.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
-            if (targetMethod instanceof MethodNode && 
hasNullableAnno(targetMethod)) {
-                reportError("Potential null dereference: 
'${targetMethod.name}()' may return null", context)
-            }
-        }
-    }
-
-    private void checkMethodArguments(call) {
-        def target = 
call.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
-        if (!(target instanceof MethodNode)) return
-        def args = call.arguments
-        if (!(args instanceof TupleExpression)) return
-        def params = target.parameters
-        // @NullCheck/@ParametersAreNonnullByDefault/@NonNullByDefault on 
method or class makes non-primitive params effectively @NonNull
-        def declaringClass = target.declaringClass
-        boolean nullChecked = hasNullCheckAnno(target) || 
hasNonNullByDefaultAnno(target) ||
-            (declaringClass != null && (hasNullCheckAnno(declaringClass) || 
hasNonNullByDefaultAnno(declaringClass)))
-        int limit = Math.min(args.expressions.size(), params.length)
-        for (int i = 0; i < limit; i++) {
-            def arg = args.getExpression(i)
-            boolean paramIsNonNull = hasNonNullAnno(params[i]) || (nullChecked 
&& !isPrimitiveType(params[i].type) && !hasNullableAnno(params[i]))
-            if (paramIsNonNull) {
-                if (isNullExpr(arg)) {
-                    reportError("Cannot pass null to @NonNull parameter 
'${params[i].name}' of '${target.name}'", call)
-                } else if (isKnownNullable(arg)) {
-                    reportError("Cannot pass @Nullable value to @NonNull 
parameter '${params[i].name}' of '${target.name}'", call)
-                }
-            }
-        }
-    }
-
-    private boolean isKnownNullable(Expression expr) {
-        if (expr instanceof VariableExpression) {
-            def target = findTargetVariable(expr)
-            if (target instanceof AnnotatedNode && hasNullableAnno(target)) 
return true
-            if (nullableVars.contains(target)) return true
-        }
-        false
-    }
-
-    private void trackNullableReturn(Variable variable, Expression rhs) {
-        if (rhs instanceof MethodCallExpression || rhs instanceof 
StaticMethodCallExpression) {
-            def target = 
rhs.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
-            if (target instanceof MethodNode && hasNullableAnno(target)) {
-                nullableVars.add(variable)
-            }
-        }
-    }
-
-    private Tuple2<Variable, Boolean> findNullGuard(Expression condition) {
-        if (condition instanceof BinaryExpression) {
-            def op = condition.operation.type
-            boolean isNotEqual = (op == Types.COMPARE_NOT_EQUAL || op == 
Types.COMPARE_NOT_IDENTICAL)
-            boolean isEqual = (op == Types.COMPARE_EQUAL || op == 
Types.COMPARE_IDENTICAL)
-            if (isNotEqual || isEqual) {
-                Variable var = null
-                if (isNullExpr(condition.rightExpression) && 
condition.leftExpression instanceof VariableExpression) {
-                    var = findTargetVariable(condition.leftExpression)
-                } else if (isNullExpr(condition.leftExpression) && 
condition.rightExpression instanceof VariableExpression) {
-                    var = findTargetVariable(condition.rightExpression)
-                }
-                if (var != null) return new Tuple2<>(var, isNotEqual)
-            }
-        }
-        null
-    }
-
-    private void reportError(String msg, node) {
-        errorReporter.call(msg, node)
-    }
-
-    
//--------------------------------------------------------------------------
-
-    static boolean hasNullableAnno(AnnotatedNode node) {
-        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NULLABLE_ANNOS } ?: false
-    }
-
-    static boolean hasNonNullAnno(AnnotatedNode node) {
-        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NONNULL_ANNOS } ?: false
-    }
-
-    static boolean hasMonotonicAnno(AnnotatedNode node) {
-        node.annotations?.any { it.classNode?.nameWithoutPackage in 
MONOTONIC_ANNOS } ?: false
-    }
-
-    static boolean hasNullCheckAnno(AnnotatedNode node) {
-        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NULLCHECK_ANNOS } ?: false
-    }
-
-    static boolean hasNonNullByDefaultAnno(AnnotatedNode node) {
-        if (hasNullUnmarkedAnno(node)) return false
-        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NONNULL_BY_DEFAULT_ANNOS } ?: false
-    }
-
-    static boolean hasNullUnmarkedAnno(AnnotatedNode node) {
-        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NULL_UNMARKED_ANNOS } ?: false
-    }
-
-    static boolean isNullExpr(Expression expr) {
-        if (expr instanceof ConstantExpression) return ((ConstantExpression) 
expr).isNullExpression()
-        if (expr instanceof CastExpression) return 
isNullExpr(((CastExpression) expr).expression)
-        false
-    }
-
-    static boolean canBeNull(Expression expr) {
-        if (isNullExpr(expr)) return true
-        if (expr instanceof TernaryExpression) {
-            return canBeNull(expr.trueExpression) || 
canBeNull(expr.falseExpression)
-        }
-        false
-    }
-
-    static boolean isEarlyExit(Statement stmt) {
-        if (stmt instanceof ReturnStatement || stmt instanceof ThrowStatement) 
return true
-        if (stmt instanceof BlockStatement) {
-            def stmts = stmt.statements
-            return stmts && isEarlyExit(stmts.last())
-        }
-        false
-    }
-}
diff --git a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc 
b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
index f33d36ddd4..30791d7b2e 100644
--- a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
+++ b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
@@ -375,16 +375,16 @@ 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.
 
-Two checkers are provided:
+Two modes are provided:
 
 * `NullChecker` — *Annotation-based*: Checks code annotated with `@Nullable` 
and
   `@NonNull` (or equivalent) annotations.
-* `StrictNullChecker` — *Annotation-based + flow-sensitive*: Includes all 
checks from
+* `NullChecker(strict: true)` — *Annotation-based + flow-sensitive*: Includes 
all checks from
   `NullChecker` plus flow-sensitive tracking through assignments and control 
flow,
   detecting potential null dereferences even in unannotated code.
 
 For code bases with a mix of strictness requirements, apply `NullChecker` to 
relaxed
-code and `StrictNullChecker` to strict code via per-class or per-method 
`@TypeChecked` annotations.
+code and `NullChecker(strict: true)` to strict code via per-class or 
per-method `@TypeChecked` annotations.
 
 === Typical usage
 
@@ -471,7 +471,7 @@ 
include::../test/NullCheckerTest.groovy[tags=null_guard,indent=0]
 include::../test/NullCheckerTest.groovy[tags=early_return,indent=0]
 ----
 
-*Ternary and elvis expressions*: in flow-sensitive mode (`StrictNullChecker`), 
the checker
+*Ternary and elvis expressions*: in flow-sensitive mode (`NullChecker(strict: 
true)`), the checker
 examines both branches of ternary (`condition ? a : b`) and elvis (`x ?: 
fallback`) expressions.
 If either branch can be null, the result is treated as nullable. The elvis 
assignment
 `x ?= nonNullValue` clears any nullable state on `x`.
@@ -626,13 +626,13 @@ We would see the following error at compile-time:
 
include::../test/NullCheckerTest.groovy[tags=nonnull_by_default_message,indent=0]
 ----
 
-=== Flow-sensitive mode with StrictNullChecker
+=== Flow-sensitive mode with strict option
 
-`NullChecker` only flags issues involving annotated code. `StrictNullChecker` 
adds
+`NullChecker` only flags issues involving annotated code. `NullChecker(strict: 
true)` adds
 flow-sensitive tracking, detecting potential null dereferences in unannotated 
code
 by tracking nullability through variable assignments and control flow:
 
-With `StrictNullChecker`, the following code would be flagged:
+With `NullChecker(strict: true)`, the following code would be flagged:
 
 [source,groovy]
 ----
@@ -661,7 +661,7 @@ The complete list of errors detected include:
 * dereferencing a `@Nullable` variable without a null check or safe navigation
 * dereferencing the return value of a `@Nullable` method without a null check
 * re-assigning `null` to a `@MonotonicNonNull` or `@Lazy` field after 
initialization
-* dereferencing a variable known to be null through flow analysis 
(`StrictNullChecker` only)
+* dereferencing a variable known to be null through flow analysis (`strict` 
mode only)
 
 NOTE: For complementary null-related checks such as detecting broken 
null-check logic,
 unnecessary null guards before `instanceof`, or `Boolean` methods returning 
`null`,
diff --git 
a/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy 
b/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy
index 59c5c00d80..5d3dae7629 100644
--- a/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy
+++ b/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy
@@ -384,7 +384,7 @@ class NullCheckerTest {
         def err = shouldFail('''
         import groovy.transform.TypeChecked
 
-        @TypeChecked(extensions='groovy.typecheckers.StrictNullChecker')
+        @TypeChecked(extensions='groovy.typecheckers.NullChecker(strict: 
true)')
         static main(args) {
             // tag::flow_sensitive[]
             def x = null
@@ -406,7 +406,7 @@ class NullCheckerTest {
         import groovy.transform.TypeChecked
 
         // tag::flow_reassign[]
-        @TypeChecked(extensions='groovy.typecheckers.StrictNullChecker')
+        @TypeChecked(extensions='groovy.typecheckers.NullChecker(strict: 
true)')
         static main(args) {
             def x = null
             x = 'hello'
diff --git 
a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
 
b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
index 9a2ed0c5c6..ecace95c67 100644
--- 
a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
+++ 
b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
@@ -53,7 +53,7 @@ final class NullCheckerTest {
         })
         strictShell = new GroovyShell(new CompilerConfiguration().tap {
             def customizer = new 
ASTTransformationCustomizer(groovy.transform.TypeChecked)
-            customizer.annotationParameters = [extensions: 
'groovy.typecheckers.StrictNullChecker']
+            customizer.annotationParameters = [extensions: 
'groovy.typecheckers.NullChecker(strict: true)']
             addCompilationCustomizers(customizer)
         })
     }
@@ -804,7 +804,7 @@ final class NullCheckerTest {
         assert err.message.contains("Cannot assign null to @MonotonicNonNull 
variable 'name' after non-null assignment")
     }
 
-    // === StrictNullChecker: flow-sensitive checks ===
+    // === NullChecker(strict: true): flow-sensitive checks ===
 
     @Test
     void testStrictFlowNullReassignInsideGuard() {

Reply via email to