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

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

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


##########
subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy:
##########
@@ -231,43 +251,158 @@ class NullChecker extends 
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
 
             @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)
+                def condition = ifElse.booleanExpression.expression
+                visitCondition(condition)
+                def facts = analyzeCondition(condition)
+                withGuards(facts.whenTrue) { ifElse.ifBlock.visit(this) }
+                withGuards(facts.whenFalse) { ifElse.elseBlock.visit(this) }
+                // Early exit: if (x == null) return/throw → x is non-null 
after (and vice versa)
+                if (isEarlyExit(ifElse.ifBlock)) {
+                    applyFacts(facts.whenFalse)
+                }
+                if (isEarlyExit(ifElse.elseBlock)) {
+                    applyFacts(facts.whenTrue)
+                }
+            }
+
+            @Override
+            void visitWhileLoop(WhileStatement loop) {
+                def condition = loop.booleanExpression.expression
+                visitCondition(condition)
+                withGuards(analyzeCondition(condition).whenTrue) { 
loop.loopBlock.visit(this) }
+            }
+
+            @Override
+            void visitTernaryExpression(TernaryExpression expression) {
+                def condition = expression.booleanExpression.expression
+                visitCondition(condition)
+                def facts = analyzeCondition(condition)
+                // for elvis, the true expression is the (already visited) 
condition
+                if (!condition.is(expression.trueExpression) && 
!expression.booleanExpression.is(expression.trueExpression)) {
+                    withGuards(facts.whenTrue) { 
expression.trueExpression.visit(this) }
                 }
+                withGuards(facts.whenFalse) { 
expression.falseExpression.visit(this) }
+            }
+
+            @Override
+            void visitAssertStatement(AssertStatement statement) {
+                def condition = statement.booleanExpression.expression
+                visitCondition(condition)
+                statement.messageExpression?.visit(this)
+                // assert x/assert x != null → x is non-null for the rest of 
the block
+                applyFacts(analyzeCondition(condition).whenTrue)
             }
 
             
//------------------------------------------------------------------
 
-            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)
+            /**
+             * Visits a condition applying short-circuit guard semantics: in
+             * {@code x != null && x.foo()} the right operand is protected by 
the
+             * left operand's null check (and similarly after {@code x == null 
||}).
+             */
+            private void visitCondition(Expression condition) {
+                if (condition instanceof BooleanExpression) {
+                    visitCondition(condition.expression)
+                } else if (condition instanceof BinaryExpression && 
condition.operation.type == Types.LOGICAL_AND) {
+                    visitCondition(condition.leftExpression)
+                    
withGuards(analyzeCondition(condition.leftExpression).whenTrue) {
+                        visitCondition(condition.rightExpression)
+                    }
+                } else if (condition instanceof BinaryExpression && 
condition.operation.type == Types.LOGICAL_OR) {
+                    visitCondition(condition.leftExpression)
+                    
withGuards(analyzeCondition(condition.leftExpression).whenFalse) {
+                        visitCondition(condition.rightExpression)
+                    }

Review Comment:
   `analyzeCondition(condition.leftExpression)` is recomputed immediately after 
`visitCondition(condition.leftExpression)` and can be invoked repeatedly for 
nested boolean expressions, creating avoidable allocations and repeated AST 
walks. A tangible improvement would be to compute the left facts once (store in 
a local) and/or cache `GuardFacts` per `Expression` node (e.g., via node 
metadata) during a single visitation pass.



##########
subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy:
##########
@@ -231,43 +251,158 @@ class NullChecker extends 
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
 
             @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)
+                def condition = ifElse.booleanExpression.expression
+                visitCondition(condition)
+                def facts = analyzeCondition(condition)
+                withGuards(facts.whenTrue) { ifElse.ifBlock.visit(this) }
+                withGuards(facts.whenFalse) { ifElse.elseBlock.visit(this) }
+                // Early exit: if (x == null) return/throw → x is non-null 
after (and vice versa)
+                if (isEarlyExit(ifElse.ifBlock)) {
+                    applyFacts(facts.whenFalse)
+                }
+                if (isEarlyExit(ifElse.elseBlock)) {
+                    applyFacts(facts.whenTrue)
+                }
+            }
+
+            @Override
+            void visitWhileLoop(WhileStatement loop) {
+                def condition = loop.booleanExpression.expression
+                visitCondition(condition)
+                withGuards(analyzeCondition(condition).whenTrue) { 
loop.loopBlock.visit(this) }
+            }
+
+            @Override
+            void visitTernaryExpression(TernaryExpression expression) {
+                def condition = expression.booleanExpression.expression
+                visitCondition(condition)
+                def facts = analyzeCondition(condition)
+                // for elvis, the true expression is the (already visited) 
condition
+                if (!condition.is(expression.trueExpression) && 
!expression.booleanExpression.is(expression.trueExpression)) {
+                    withGuards(facts.whenTrue) { 
expression.trueExpression.visit(this) }
                 }
+                withGuards(facts.whenFalse) { 
expression.falseExpression.visit(this) }
+            }
+
+            @Override
+            void visitAssertStatement(AssertStatement statement) {
+                def condition = statement.booleanExpression.expression
+                visitCondition(condition)
+                statement.messageExpression?.visit(this)
+                // assert x/assert x != null → x is non-null for the rest of 
the block
+                applyFacts(analyzeCondition(condition).whenTrue)
             }
 
             
//------------------------------------------------------------------
 
-            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)
+            /**
+             * Visits a condition applying short-circuit guard semantics: in
+             * {@code x != null && x.foo()} the right operand is protected by 
the
+             * left operand's null check (and similarly after {@code x == null 
||}).
+             */
+            private void visitCondition(Expression condition) {
+                if (condition instanceof BooleanExpression) {
+                    visitCondition(condition.expression)
+                } else if (condition instanceof BinaryExpression && 
condition.operation.type == Types.LOGICAL_AND) {
+                    visitCondition(condition.leftExpression)
+                    
withGuards(analyzeCondition(condition.leftExpression).whenTrue) {
+                        visitCondition(condition.rightExpression)
+                    }
+                } else if (condition instanceof BinaryExpression && 
condition.operation.type == Types.LOGICAL_OR) {
+                    visitCondition(condition.leftExpression)
+                    
withGuards(analyzeCondition(condition.leftExpression).whenFalse) {
+                        visitCondition(condition.rightExpression)
+                    }
                 } 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)
+                    condition.visit(this)
+                }
+            }
+
+            /**
+             * Analyzes a condition into the sets of variables known to be 
non-null
+             * when the condition evaluates true and when it evaluates false.
+             */
+            private GuardFacts analyzeCondition(Expression condition) {
+                def facts = new GuardFacts()
+                if (condition instanceof NotExpression) {
+                    def inner = analyzeCondition(condition.expression)
+                    facts.whenTrue = inner.whenFalse
+                    facts.whenFalse = inner.whenTrue
+                } else if (condition instanceof BooleanExpression) {
+                    facts = analyzeCondition(condition.expression)
+                } else if (condition instanceof BinaryExpression) {
+                    int op = condition.operation.type
+                    if (op == Types.LOGICAL_AND) {
+                        def left = analyzeCondition(condition.leftExpression)
+                        def right = analyzeCondition(condition.rightExpression)
+                        facts.whenTrue = left.whenTrue + right.whenTrue
+                        facts.whenFalse = 
left.whenFalse.intersect(right.whenFalse)
+                    } else if (op == Types.LOGICAL_OR) {
+                        def left = analyzeCondition(condition.leftExpression)
+                        def right = analyzeCondition(condition.rightExpression)
+                        facts.whenTrue = 
left.whenTrue.intersect(right.whenTrue)
+                        facts.whenFalse = left.whenFalse + right.whenFalse
+                    } else if (op == Types.KEYWORD_INSTANCEOF) {

Review Comment:
   The code assigns the results of `+` and `intersect(...)` directly into 
fields declared as `Set<Variable>`. In Groovy, these operations can yield a 
generic `Collection` (and the concrete type may vary), which makes the 
`GuardFacts` invariants less explicit and can introduce subtle behavior 
differences (e.g., duplicates/order) and extra allocations. Consider explicitly 
constructing `HashSet`s for union/intersection (e.g., copy + `addAll` / 
`retainAll`) so `whenTrue/whenFalse` are always concrete sets with predictable 
semantics.



##########
subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc:
##########
@@ -707,6 +707,19 @@ 
include::../test/NullCheckerTest.groovy[tags=null_guard,indent=0]
 include::../test/NullCheckerTest.groovy[tags=early_return,indent=0]
 ----
 
+*Broader guard patterns* are also recognized: Groovy-truth guards (`if (x)`),
+`instanceof` checks, `Objects.nonNull(x)`/`Objects.isNull(x)`, assert 
statements
+(`assert x`, `assert x != null`), and boolean conjunctions and disjunctions 
with
+short-circuit semantics (`x != null && x.length() > 0`,
+`if (x == null || x.isEmpty()) return`). Negated forms (`if (!x) return`,
+`x !instanceof Foo`) are understood as well. Guard conditions are honored in
+`if` statements, `while` loops, and ternary expressions:

Review Comment:
   The “short-circuit semantics” examples mix a pure expression with a full 
statement (`if (x == null || x.isEmpty()) return`) inside the same 
parenthetical list, which makes the list harder to scan and slightly ambiguous 
about what is being recognized (the boolean expression vs. the early-exit 
statement form). Consider rephrasing to keep this portion as expressions only 
(e.g., `x == null || x.isEmpty()`) and call out the early-exit `if (...) 
return/throw` form separately.





> NullChecker: broaden null-guard recognition
> -------------------------------------------
>
>                 Key: GROOVY-12208
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12208
>             Project: Groovy
>          Issue Type: Improvement
>          Components: groovy-typecheckers
>            Reporter: Paul King
>            Priority: Major
>
> The incubating {{groovy.typecheckers.NullChecker}} currently recognizes a 
> limited in-body guard vocabulary: single {{x != null}} / {{x == null}} binary 
> comparisons (including identity forms) as {{if}} conditions, and early-exit 
> patterns ({{if (x == null) return/throw}}), plus safe navigation. 
> (Separately, the groovy-contracts bridge already infers non-nullness from 
> {{@Requires}} conditions including conjunctions, via 
> {{StaticTypesMarker.INFERRED_NON_NULL}} — this issue is about guard 
> recognition inside method bodies, where conjunctions and other idiomatic 
> forms are not yet recognized.)
> Idiomatic Groovy uses several other null-guard forms, each currently 
> producing false positives that discourage adoption:
> * Groovy-truth guards: {{if (x) { x.foo() } }} — for reference types, 
> truthiness implies non-null within the guarded block (Groovy truth being 
> false for empty strings/collections does not affect nullness soundness).
> * Boolean conjunctions in conditions: {{if (x != null && x.foo())}}, and 
> short-circuit dereferences {{x != null && x.foo()}} in expression position; 
> Groovy-truth conjuncts {{x && x.foo()}}.
> * {{instanceof}} checks: {{if (x instanceof Foo)}} implies {{x}} is non-null 
> (aligns with the flow typing the static type checker already performs for 
> {{instanceof}}).
> * {{Objects.nonNull(x)}} / {{Objects.isNull(x)}} in the positions the binary 
> forms are recognized today.
> * {{assert x}} and {{assert x != null}} statements.
> * Guard conditions in {{while}} loops and ternary conditions, mirroring the 
> {{if}} handling.
> Each addition removes a class of false positives without changing the 
> checker's architecture. Test coverage should include the negated forms and 
> else-branch behaviour for each new guard shape, following the existing 
> patterns in {{NullCheckerTest}} (cf. {{testNullableWithNotNullGuard}} et al.).



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

Reply via email to