[ 
https://issues.apache.org/jira/browse/GROOVY-11894?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18070318#comment-18070318
 ] 

ASF GitHub Bot commented on GROOVY-11894:
-----------------------------------------

Copilot commented on code in PR #2426:
URL: https://github.com/apache/groovy/pull/2426#discussion_r3021709367


##########
subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy:
##########
@@ -0,0 +1,350 @@
+/*
+ *  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.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.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')
+
+    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)) {
+            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)
+                }
+            }

Review Comment:
   The flow-sensitive assignment logic has several edge cases that aren't 
currently exercised by tests: (1) casted-null assignments (e.g. `def x = 
(String) null`) should be treated as nullable in `StrictNullChecker`, (2) 
propagation of nullability through variable-to-variable assignment (`def y = 
x`) when `x` is nullable, and (3) `@MonotonicNonNull` should not be considered 
initialized when assigned from a value that may be null. Adding focused tests 
for these cases would prevent regressions and validate the intended 
flow-sensitive behavior.



##########
subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy:
##########
@@ -0,0 +1,350 @@
+/*
+ *  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.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.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')
+
+    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)) {
+            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)
+            } else {
+                nullableVars.remove(target)
+                if (flowSensitive) {
+                    trackNullableReturn(target, expression.rightExpression)

Review Comment:
   In flow-sensitive mode, nullability does not propagate through assignments 
like `y = x` when `x` is known nullable (either annotated `@Nullable` or 
tracked in `nullableVars`). Because the `else` branch unconditionally removes 
the target from `nullableVars` unless RHS is a literal `null` (or a 
nullable-returning method via `trackNullableReturn`), `StrictNullChecker` can 
miss `y` being nullable. Consider treating 
`isKnownNullable(expression.rightExpression)` as nullable and keeping/adding 
the target in `nullableVars` (and similarly avoid marking `@MonotonicNonNull` 
as initialized when the assigned value may be null).
   ```suggestion
               Expression rhs = expression.rightExpression
               boolean rhsIsNull = isNullExpr(rhs)
               boolean rhsKnownNullable = isKnownNullable(rhs)
               if (fieldNonNull && rhsIsNull) {
                   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 (!rhsIsNull && !rhsKnownNullable) {
                       monotonicInitialized.add(target)
                   } else if (rhsIsNull && 
monotonicInitialized.contains(target)) {
                       reportError("Cannot assign null to @MonotonicNonNull 
variable '${expression.leftExpression.name}' after non-null assignment", 
expression)
                   }
               }
               if (rhsIsNull || rhsKnownNullable) {
                   nullableVars.add(target)
               } else {
                   nullableVars.remove(target)
                   if (flowSensitive) {
                       trackNullableReturn(target, rhs)
   ```



##########
subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy:
##########
@@ -0,0 +1,350 @@
+/*
+ *  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.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.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')
+
+    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)) {
+            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)
+            } 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) {
+        node.annotations?.any { it.classNode?.nameWithoutPackage in 
NONNULL_BY_DEFAULT_ANNOS } ?: false
+    }
+
+    static boolean isNullExpr(Expression expr) {
+        expr instanceof ConstantExpression && ((ConstantExpression) 
expr).isNullExpression()
+    }

Review Comment:
   `isNullExpr` only recognizes a bare `ConstantExpression(null)`. Common 
patterns like `def x = (String) null` (casted null) or `x = null as String` 
won't be treated as null, which breaks flow-sensitive tracking and `@NonNull` 
assignment checks in those cases. Consider unwrapping wrapper expressions 
(e.g., `CastExpression`) before checking for a null constant.



##########
subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy:
##########
@@ -0,0 +1,350 @@
+/*
+ *  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.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.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')
+
+    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)) {
+            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)
+            } else {
+                nullableVars.remove(target)
+                if (flowSensitive) {
+                    trackNullableReturn(target, expression.rightExpression)
+                }
+            }

Review Comment:
   `guardedVars` can be made to contain a variable after an early-exit guard 
(`if (x == null) return`) and is then consulted to suppress dereference 
warnings. However, assignments never clear `guardedVars`, so `x` can be 
re-assigned to `null` later and still be treated as guarded, causing false 
negatives in `StrictNullChecker`. A concrete fix is to remove the assignment 
target from `guardedVars` on any write (and only re-add it within the 
appropriate guard scope / post-early-exit state).





> Provide a NullChecker for groovy-typecheckers
> ---------------------------------------------
>
>                 Key: GROOVY-11894
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11894
>             Project: Groovy
>          Issue Type: New Feature
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to