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

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

daniellansun commented on code in PR #2773:
URL: https://github.com/apache/groovy/pull/2773#discussion_r3740607353


##########
src/test/groovy/groovy/InstanceofTest.groovy:
##########
@@ -223,4 +223,279 @@ final class InstanceofTest {
         }
         assert y == 'foobar'
     }
+
+    // GROOVY-12242: Java-aligned flow scoping for negated instanceof (JEP 394)
+    @Test
+    void testVariableScopeNegatedElse() {
+        def f = { Object o ->
+            if (!(o instanceof String s)) {
+                return 'not'
+            } else {
+                return s.toUpperCase()
+            }
+        }
+        assert f('hi') == 'HI'
+        assert f(1) == 'not'
+    }
+
+    // GROOVY-12242: pattern variable remains in scope after abrupt then-branch
+    @Test
+    void testVariableScopeEarlyReturn() {
+        def f = { Object o ->
+            if (!(o instanceof String s)) return 'early'
+            return s.toUpperCase()
+        }
+        assert f('hi') == 'HI'
+        assert f(42) == 'early'
+    }
+
+    // GROOVY-12242: pattern variable remains after else that cannot complete 
normally
+    @Test
+    void testVariableScopeAfterAbruptElse() {
+        def f = { Object o ->
+            if (o instanceof String s) {
+                // matched
+            } else {
+                return 'no'
+            }
+            return s.toUpperCase()
+        }
+        assert f('ab') == 'AB'
+        assert f(9) == 'no'
+    }
+
+    // GROOVY-12242: pattern variable must not leak after a declaration 
statement
+    @Test
+    void testVariableNoLeakAfterDeclaration() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    boolean b = (o instanceof String s)
+                    return s
+                }
+            }
+            new C().m('hi')
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: pattern variable must not leak after an expression 
statement
+    @Test
+    void testVariableNoLeakAfterExpressionStatement() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    o instanceof String s && s.length() > 0
+                    return s
+                }
+            }
+            new C().m('hi')
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: true branch of negated instanceof must not see the 
pattern local
+    // (CompileStack polarity must match VariableScope — no silent null ALOAD)
+    @Test
+    void testVariableNegatedIfBranchNotInScope() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    if (!(o instanceof String s)) {
+                        return s
+                    }
+                    return 'matched'
+                }
+            }
+            new C().m(1)
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: true-path binding of left of || is not in scope on the 
right (Java)
+    @Test
+    void testVariableOrRightHandSideNotInScope() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        def err = shouldFail shell, '''
+            @groovy.transform.TypeChecked
+            class C {
+                static void m(Object o) {
+                    if (o instanceof String s || s.length() > 0) {
+                    }
+                }
+            }
+        '''
+        assert err.message =~ /The variable .s. is undeclared|Apparent 
variable .s./
+    }
+
+    // GROOVY-12242: false-path binding is in scope on the right of || (Java)
+    @Test
+    void testVariableOrRightHandSideFalsePathInScope() {
+        def f = { Object o ->
+            // when o is String, left is false, right sees s
+            return (!(o instanceof String s) || s.isEmpty())
+        }
+        assert f('') == true
+        assert f('x') == false
+        assert f(1) == true // left true → short-circuit, s not needed
+    }
+
+    // GROOVY-12242: ternary false branch must not see true-path pattern 
variable
+    @Test
+    void testVariableTernaryFalseBranchNotInScope() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        def err = shouldFail shell, '''
+            @groovy.transform.TypeChecked
+            class C {
+                static Object m(Object o) {
+                    return o instanceof String s ? 'yes' : s
+                }
+            }
+        '''
+        assert err.message =~ /The variable .s. is undeclared|Apparent 
variable .s./
+    }
+
+    // GROOVY-12242: dynamic ternary false branch must not load a pattern local
+    @Test
+    void testVariableTernaryFalseBranchNotInScopeDynamic() {
+        def err = shouldFail MissingPropertyException, '''
+            class C {
+                Object m(Object o) {
+                    return o instanceof String s ? 'yes' : s
+                }
+            }
+            new C().m(1)
+        '''
+        assert err.message =~ /No such property: s/
+    }
+
+    // GROOVY-12242: ternary true branch sees pattern variable
+    @Test
+    void testVariableTernaryTrueBranch() {
+        def f = { Object o -> o instanceof String s ? s.toUpperCase() : 'no' }
+        assert f('ab') == 'AB'
+        assert f(1) == 'no'
+    }
+
+    // GROOVY-12242: reassignment of pattern variable (not implicitly final, 
JEP 394)
+    @Test
+    void testVariableReassignment() {
+        Object o = 'hi'
+        if (o instanceof String s) {
+            s = s + '!'
+            assert s == 'hi!'
+        } else {
+            assert false
+        }
+    }
+
+    // GROOVY-12242: pattern variable shadows a field only where in scope
+    @Test
+    void testVariableFieldShadowing() {
+        def obj = new Object() {
+            String s = 'field'
+            def test(Object o) {
+                if (o instanceof String s) {
+                    return "pv=$s"
+                }
+                return "field=$s"
+            }
+        }
+        assert obj.test('x') == 'pv=x'
+        assert obj.test(1) == 'field=field'
+    }
+
+    // GROOVY-12242: && chain uses pattern variable on subsequent operands
+    @Test
+    void testVariableAndChain() {
+        Object o = 'hello'
+        assert (o instanceof String s && s.length() > 3 && s.startsWith('h'))
+        assert !(o instanceof String s && s.length() > 99)
+    }
+
+    // GROOVY-12242: while body can use true-path pattern variable
+    @Test
+    void testVariableWhileBody() {
+        Object o = 'ab'
+        def n = 0
+        while (o instanceof String s && s.length() > 0) {
+            n += 1
+            o = s.substring(1)
+        }
+        assert n == 2
+        assert o == ''
+    }
+
+    // GROOVY-12242: reuse the same pattern variable name in successive 
statements
+    @Test
+    void testVariableNameReuse() {
+        Object a = 'x', b = 1
+        def r = []
+        if (a instanceof String s) r << s
+        if (b instanceof Integer s) r << s
+        assert r == ['x', 1]
+    }
+
+    // GROOVY-12242: type-checked flow scoping for early return
+    @Test
+    void testVariableScopeEarlyReturnTypeChecked() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        assert shell.evaluate('''
+            @groovy.transform.TypeChecked
+            class C {
+                static String m(Object o) {
+                    if (!(o instanceof String s)) return 'early'
+                    return s.toUpperCase()
+                }
+            }
+            assert C.m('hi') == 'HI'
+            assert C.m(1) == 'early'
+            true
+        ''')
+    }
+
+    // GROOVY-12242: type-checked — positive instanceof still not in else
+    @Test
+    void testVariableScopePositiveNotInElseTypeChecked() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        def err = shouldFail shell, '''
+            Number n = 12345
+            if (n instanceof Integer i) {
+            } else {
+                i.toString()
+            }
+        '''
+        assert err.message =~ /The variable .i. is undeclared/
+    }
+
+    // GROOVY-12242: type-checked — negated instanceof is in else
+    @Test
+    void testVariableScopeNegatedInElseTypeChecked() {
+        def shell = GroovyShell.withConfig {
+            ast groovy.transform.TypeChecked
+        }
+        assert shell.evaluate('''
+            @groovy.transform.TypeChecked
+            class C {
+                static String m(Object o) {
+                    if (!(o instanceof String s)) {
+                        return 'not'
+                    } else {
+                        return s.toUpperCase()
+                    }
+                }
+            }
+            assert C.m('hi') == 'HI'
+            assert C.m(1) == 'not'
+            true
+        ''')
+    }

Review Comment:
   This is the most actionable review point, and the coverage was indeed 
incomplete.
   
   One important nuance about dynamic-mode Groovy: `evaluateInstanceof` always 
allocates the
   CompileStack slot during condition evaluation (needed for `&&` 
short-circuit). In *dynamic*
   mode this means the slot can be accessed at runtime even for condition 
shapes where flow
   scoping says it is not "definitely bound" (e.g. `instanceof s || true` in 
the if-block).
   `@TypeChecked` / `@CompileStatic` enforces the stricter Java rule at compile 
time. The
   cases below are therefore tested in whichever mode is the appropriate signal 
for that shape.
   
   | # | Condition shape | if-block | else-block | after if-else | Test method |
   |---|-----------------|----------|-----------|---------------|-------------|
   | 1 | `o instanceof String s` | ✅ visible (existing) | ❌ **new** | ❌ **new** 
| `testSimpleInstanceof_notInElse_notAfterIf` |
   | 1b | `o instanceof String s`, else throws | — | — | ✅ visible **new** | 
`testSimpleInstanceof_visibleAfterIf_whenElseAbrupt` |
   | 2 | `!(o instanceof String s)` | ❌ **new** | ✅ visible (existing) | — | 
`testNegatedInstanceof_truePathHides_falsePathBinds` |
   | 3 | `!(o instanceof s)` + return in if | — | — | ✅ **new** (explicit cell) 
| `testNegatedInstanceof_earlyReturnInTrue_visibleAfter` |
   | 4 | `o instanceof String s && cond` | ✅ visible (existing) | ❌ **new** | ❌ 
**new** | `testAndChain_ifBlockVisible_elseNotVisible_afterNotVisible` |
   | 5 | `o instanceof String s \|\| cond` | ❌ (TypeChecked) **new** | ❌ | ❌ | 
`testOrChain_noVisibilityAnywhere` |
   | 6 | `!(o instanceof String s) && cond` | ❌ (TypeChecked) **new** | — | — | 
`testNegatedAndCond_noVisibility` |
   | 7 | `!(o instanceof s) && cond`, else return | — | — | ❌ (TypeChecked) 
**new** | `testNegatedAndCond_withElseReturn_noVisibilityAfter` |
   | 8 | `!(o instanceof String s) \|\| cond` | — | ✅ visible **new** | — | 
`testNegatedOr_elseBlockSees_afterAbruptIfBlockSees` |
   | 9 | `!(o instanceof s) \|\| cond`, else return | — | — | ❌ **new** | 
`testNegatedOr_elseReturn_noVisibilityAfter` |
   
   > *Cases 5, 6, 7 use `@TypeChecked` because in dynamic Groovy the slot is 
physically
   > allocated by `evaluateInstanceof` regardless of flow scoping. TypeChecked 
enforces the
   > Java-compatible rule at compile time, which is the correct enforcement 
layer.*
   
   Additional De Morgan / compound cases added:
   
   - `!(o instanceof String s && cond)` — conservative: no binding anywhere
     (`testDeMorgan_notAndNegation_noBinding`)
   - `!!(o instanceof String s)` — double negation restores positive binding
     (`testDoubleNegation_positiveBinding`)
   
   **Unit-test coverage for the analysis logic** (`InstanceofFlowBindingsTest`) 
has been
   extended with the same systematic matrix at the pure binding-analysis level, 
testing
   `whenTrue()`, `whenFalse()`, and `isEmpty()` for each condition shape. 
Multi-pattern
   `&&`/`||` combinations (two distinct pattern variables `s` and `i`) and 
`allNames()`
   deduplication are also covered.
   





> instanceof pattern variable scope is not aligned with Java flow scoping (JEP 
> 394)
> ---------------------------------------------------------------------------------
>
>                 Key: GROOVY-12242
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12242
>             Project: Groovy
>          Issue Type: Bug
>            Reporter: Daniel Sun
>            Priority: Major
>
> h2. Summary
> After {{instanceof}} type patterns landed in GROOVY-11229, pattern variables 
> were still scoped with a coarse lexical approximation. That diverges from 
> Java’s *flow scoping* (JEP 394): a pattern variable must be visible only 
> where the pattern has *definitely* matched.
> The gaps appear as:
>  # variables missing where Java allows them
>  # variables leaking past the statement that introduced them
>  # name resolution and bytecode disagreeing, so an “out of scope” use can 
> still load a local slot
> h2. Background
>  * GROOVY-11229 added {{e instanceof T t}} (parser, AST, store-on-match).
>  * Java (JEP 394 / JLS): scope follows boolean flow and abrupt completion, 
> not simple block poison.
>  * Groovy initially limited leakage with push/pop around statements, but did 
> not implement true/false-path binding or CompileStack polarity.
> h2. Problems (before the fix)
> ||#||Scenario||Java||Groovy (before)||
> |1|negated {{instanceof}} — use pattern var in else|in scope|missing|
> |2|negated {{instanceof}} + early {{return}} — use pattern var after if|in 
> scope|missing|
> |3|positive {{instanceof}} + abrupt else — use pattern var after if|in 
> scope|missing|
> |4|{{boolean b = (o instanceof String s)}} then use {{s}}|not in 
> scope|CompileStack leak (local still loadable)|
> |5|expression statement with pattern, then use pattern var|not in 
> scope|CompileStack leak|
> |6|type-checked: pattern var used on RHS of logical-or|error on RHS|often 
> accepted|
> |7|type-checked ternary false arm uses pattern var|error|often accepted|
> |8|negated {{instanceof}} — use pattern var in then-branch|not in scope|could 
> ALOAD unassigned local (null)|
> h2. Steps to reproduce
> h3. A. Negated instanceof — else branch (should see {{{}s{}}})
> {code:groovy}
> def f = { Object o ->
>     if (!(o instanceof String s)) {
>         return 'not'
>     } else {
>         return s.toUpperCase()   // expected: OK when o is String
>     }
> }
> assert f('hi') == 'HI'
> {code}
> h3. B. Early return after negation (should see {{s}} after if)
> {code:groovy}
> def f = { Object o ->
>     if (!(o instanceof String s)) return 'early'
>     return s.toUpperCase()       // expected: OK when o is String
> }
> assert f('hi') == 'HI'
> {code}
> h3. C. Leak after declaration (must *not* see {{{}s{}}})
> {code:groovy}
> class C {
>     Object m(Object o) {
>         boolean b = (o instanceof String s)
>         return s                 // expected: MissingPropertyException / 
> undeclared
>     }
> }
> new C().m('hi')
> {code}
> h3. D. Type-checked {{||}} RHS must not see true-path binding
> {code:groovy}
> @groovy.transform.TypeChecked
> class C {
>     static void m(Object o) {
>         if (o instanceof String s || s.length() > 0) {
>             // expected: undeclared / apparent variable s on RHS of ||
>         }
>     }
> }
> {code}
> h2. Expected behaviour
> Align with Java JEP 394 flow scoping for the common shapes:
>  * true-path bindings (e.g. {{{}e instanceof T t{}}}) live in then-blocks, 
> {{&&}} RHS, and ternary true arm
>  * false-path bindings (e.g. {{{}!(e instanceof T t){}}}) live in 
> else-blocks, after abrupt then, and the matching ternary arm
>  * pattern variables do not leak past the introducing statement (declaration 
> RHS, expression statement, …)
>  * VariableScope (names) and CompileStack (locals) agree on which path a 
> pattern local is live
> h2. Actual behaviour (before fix)
>  * Lexical push/pop approximated “no leak past statement” but not true/false 
> path polarity.
>  * CompileStack could keep pattern slots after VariableScope had dropped the 
> name (silent local load vs property miss).
>  * Negation and abrupt-completion cases from Java were not supported.
>  



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

Reply via email to