blackdrag commented on code in PR #2645:
URL: https://github.com/apache/groovy/pull/2645#discussion_r3653205645


##########
src/test/groovy/groovy/transform/stc/ClassTagStaticTest.groovy:
##########
@@ -0,0 +1,364 @@
+/*
+ *  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.transform.stc
+
+import groovy.transform.CompileStatic
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Tests for {@code @ClassTag} (GROOVY-12115): under static type checking, a 
call may omit the
+ * trailing compiler-supplied {@code Class<X>} token(s) and the type checker 
synthesises them from
+ * the receiver's type argument(s). The first consumers are the {@code 
asChecked} extension methods.
+ */
+final class ClassTagStaticTest extends StaticTypeCheckingTestCase {
+
+    @Test
+    void testListTokenInjectedFromElementType() {
+        assertScript '''
+            List<String> base = []
+            List<String> checked = base.asChecked()       // compiler injects 
String.class
+            checked.add('ok')
+            assert base == ['ok']                          // checked view 
writes through to base
+            boolean threw = false
+            try {
+                ((List) checked).add(42)                   // wrong element 
type via raw view
+            } catch (ClassCastException e) {
+                threw = true
+            }
+            assert threw
+        '''
+    }
+
+    @Test
+    void testMapTokensInjectedForKeyAndValue() {
+        assertScript '''
+            Map<Number,String> base = [:]
+            Map<Number,String> checked = base.asChecked()  // compiler injects 
Number.class, String.class
+            checked.put(1, 'one')
+            assert base == [1: 'one']
+
+            boolean badKey = false
+            try { ((Map) checked).put('x', 'y') } catch (ClassCastException e) 
{ badKey = true }
+            assert badKey
+
+            boolean badValue = false
+            try { ((Map) checked).put(2, 99) } catch (ClassCastException e) { 
badValue = true }
+            assert badValue
+        '''
+    }
+
+    @Test
+    void testInjectionWorksUnderCompileStaticDirectCall() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            class C {
+                static List<String> make() {
+                    List<String> base = []
+                    List<String> checked = base.asChecked()
+                    checked.add('ok')
+                    base
+                }
+            }
+            assert C.make() == ['ok']
+        '''
+    }
+
+    @Test
+    void testExplicitTokenStillResolves() {
+        assertScript '''
+            List<String> base = []
+            List<String> checked = base.asChecked(String)  // nothing 
injected; existing overload
+            checked.add('ok')
+            assert base == ['ok']
+        '''
+    }
+
+    @Test
+    void testSortedMapSubtypeReceiver() {
+        assertScript '''
+            TreeMap<Number,String> base = new TreeMap<>()
+            Map<Number,String> checked = base.asChecked()  // ConcreteMap 
subtype still resolves K,V
+            checked.put(1, 'one')
+            assert base == [1: 'one']
+        '''
+    }
+
+    @Test
+    void testRawReceiverDoesNotResolve() {
+        // a raw receiver has no statically-known type argument, so no token 
is synthesised
+        shouldFailWithMessages '''
+            void useRaw(Map base) {
+                base.asChecked()
+            }
+        ''', 'Cannot find matching method', 'asChecked()'
+    }
+
+    @Test
+    void testWithDefaultPreemptedToKeyAndValueChecked() {
+        assertScript '''
+            Map<Number,String> base = [:]
+            Map<Number,String> m = base.withDefault{ 'n/a' }   // preempted: 
key+value checked
+            assert m[1] == 'n/a'                                // compatible 
key auto-grows with String default
+
+            boolean badKey = false
+            try { ((Map) m).put('x', 'y') } catch (ClassCastException e) { 
badKey = true }
+            assert badKey
+
+            boolean badValue = false
+            try { ((Map) m).put(2, 99) } catch (ClassCastException e) { 
badValue = true }
+            assert badValue
+        '''
+    }
+
+    @Test
+    void testWithDefaultPreemptedKeyCheckedWhenValueUnconstrained() {
+        assertScript '''
+            Map<Number,?> base = [:]
+            Map<Number,?> m = base.withDefault{ null }          // preempted: 
at least key is checked
+            assert m[1] == null
+
+            boolean badKey = false
+            try { ((Map) m).put('x', 1) } catch (ClassCastException e) { 
badKey = true }
+            assert badKey
+        '''
+    }
+
+    @Test
+    void testWithDefaultStaysLenientWhenNothingToCheck() {
+        // an untyped map erases both tokens to Object, so there is nothing to 
gain and the lenient
+        // withDefault is kept rather than silently becoming a checked view
+        assertScript '''
+            def base = [:]
+            def m = base.withDefault{ 'x' }
+            ((Map) m).put('any', 1)                             // no 
ClassCastException
+            assert m['any'] == 1
+        '''
+    }
+
+    @Test
+    void testExplicitWithDefaultTokensNotReinjected() {
+        assertScript '''
+            Map<Number,String> base = [:]
+            Map<Number,String> m = base.withDefault(Number, String){ 'n/a' }
+            assert m[1] == 'n/a'
+        '''
+    }
+
+    @Test
+    void testTokensReorderedByTypeVariableNotPosition() {
+        // a method declaring the value token BEFORE the key token still 
receives each token in the
+        // slot its Class<X> names - resolution is by type-variable name, not 
by position
+        assertScript '''
+            import groovy.transform.stc.ClassTag
+
+            class Box<K,V> {
+                List<Class> captured = []
+                Map<K,V> record(@ClassTag Class<V> valueType, @ClassTag 
Class<K> keyType, Closure init) {
+                    captured = [valueType, keyType]
+                    [:]
+                }
+            }
+
+            Box<Number,String> b = new Box<>()
+            b.record{ }
+            assert b.captured == [String, Number]   // valueType <- V=String, 
keyType <- K=Number

Review Comment:
   I think I am missing some tests here:
   (1) What happens if there is a record(Closure) method additionally to the 
one declared in Box?
   (2) There should be a test where Class<V> and Class<K> are switched in 
positions.
   (3) What about generic parameters added by the method? 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to