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

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

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


##########
src/test/groovy/bugs/Groovy11916.groovy:
##########
@@ -0,0 +1,727 @@
+/*
+ *  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 bugs
+
+import groovy.lang.GroovyClassLoader
+import org.codehaus.groovy.control.CompilationUnit
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.Phases
+import org.codehaus.groovy.control.customizers.ImportCustomizer
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.Test
+
+import java.lang.reflect.Modifier
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Star imports, module imports, and default imports resolve only accessible
+ * types. A source import shadows those defaults, including {@code java.lang}.
+ * Two source star imports bind the first accessible type in source order.
+ * An explicit single-type import, a fully qualified name, and
+ * {@code Class.forName} still name a type that is not public.
+ */
+final class Groovy11916 {
+
+    private static GroovyClassLoader types
+
+    @BeforeAll
+    static void compilePackagePrivateTypes() {
+        types = compile(Groovy11916.classLoader, [
+            'p/Types': '''
+                package p
+
+                @groovy.transform.PackageScope
+                class Hidden {}
+
+                class Visible {}
+
+                @groovy.transform.PackageScope
+                class Outer {
+                    public static class Inner {}
+                }
+
+                class Protector {
+                    protected static class Secret {}
+                }
+
+                class Priv {
+                    private static class Secret {}
+                    public static class Shown {}
+                }
+
+                @groovy.transform.PackageScope
+                class Collision {}
+            '''
+        ])
+        assert !Modifier.isPublic(types.loadClass('p.Hidden').modifiers)
+        assert !Modifier.isPublic(types.loadClass('p.Collision').modifiers)
+        assert Modifier.isPublic(types.loadClass('p.Visible').modifiers)
+        assert !Modifier.isPublic(types.loadClass('p.Outer').modifiers)
+        assert Modifier.isPublic(types.loadClass('p.Outer$Inner').modifiers)
+        assert 
Modifier.isProtected(types.loadClass('p.Protector$Secret').modifiers)
+        assert Modifier.isPrivate(types.loadClass('p.Priv$Secret').modifiers)
+    }
+
+    private static GroovyClassLoader compile(ClassLoader parent, Map<String, 
String> sources, CompilerConfiguration config = null) {
+        def loader = new GroovyClassLoader(parent, config)
+        def unit = new CompilationUnit(config, null, loader)
+        sources.each { name, text -> unit.addSource(name + '.groovy', text) }
+        unit.compile(Phases.CLASS_GENERATION)
+        unit.classes.sort { it.name }.each { groovyClass ->
+            loader.defineClass(groovyClass.name, groovyClass.bytes)
+        }
+        loader
+    }
+
+    private static void putType(Map<String, String> sources, String pkg, 
String simpleName, String state) {
+        sources[pkg + '/Marker'] = "package ${pkg}\nclass Marker {}"
+        if (state == 'p') {
+            sources[pkg + '/' + simpleName] = "package ${pkg}\nclass 
${simpleName} {}"
+        } else if (state == 'k') {
+            sources[pkg + '/' + simpleName] = "package 
${pkg}\[email protected]\nclass ${simpleName} {}"
+        }
+    }
+
+    private static String useSource(String pkg, String imports, String 
simpleName) {
+        """\
+package ${pkg}
+${imports}
+class Use {
+    ${simpleName} field
+    static Class which() { ${simpleName} }
+}
+"""
+    }
+
+    /** First public type in source order. {@code p} public, {@code k} 
package-private, {@code m} missing. */
+    private static String firstStar(String a, String b, String c) {
+        if (a == 'p') return 'pa.X'
+        if (b == 'p') return 'pb.X'
+        if (c == 'p') return 'pc.X'
+        return 'unresolved'
+    }
+
+    private static String classifyUse(Map<String, String> sources, String 
className, CompilerConfiguration config = null) {
+        // Dependency types are compiled first so package-private modifiers are
+        // already on the bytecode when the importing compilation unit is 
resolved.
+        def deps = sources.findAll { name, text -> !name.endsWith('/Use') }
+        def use = sources.findAll { name, text -> name.endsWith('/Use') }
+        try {
+            def parent = deps ? compile(Groovy11916.classLoader, deps) : 
Groovy11916.classLoader
+            def loader = compile(parent, use, config)
+            return 
loader.loadClass(className).getDeclaredMethod('which').invoke(null).name
+        } catch (Exception e) {
+            String text = ''
+            for (Throwable t = e; t != null; t = t.cause) {
+                if (t.message != null) text += t.message + '\n'
+            }
+            if (text.contains('ambiguous')) return 'ambiguous'
+            if (text.contains('unable to resolve')) return 'unresolved'
+            if (text.contains('not public')) return 'not-public'
+            def snippet = text.readLines().collect { it.trim() }.findAll { it 
}.take(4).join(' | ')
+            return 'other:' + (snippet ?: e.getClass().simpleName)
+        }
+    }
+
+    @Test
+    void testStarImportDoesNotResolvePackagePrivateJdkType() {
+        assert !Modifier.isPublic(Class.forName('java.util.TimSort').modifiers)
+
+        def err = shouldFail '''
+            import java.util.*
+            def seen = null instanceof TimSort
+        '''
+        assert err.message.contains('unable to resolve class TimSort')
+
+        // A bare name in a script is a dynamic property when it is not a type.
+        // It must not bind the inaccessible class (that binding is what throws
+        // IllegalAccessError for a later instanceof / checkcast).
+        err = shouldFail '''
+            import java.util.*
+            assert TimSort.name == 'java.util.TimSort'
+        '''
+        assert err instanceof MissingPropertyException
+    }
+
+    @Test
+    void testDefaultImportDoesNotResolvePackagePrivateJdkType() {
+        def err = shouldFail '''
+            def seen = null instanceof TimSort
+        '''
+        assert err.message.contains('unable to resolve class TimSort')
+    }
+
+    @Test
+    void testModuleImportDoesNotResolvePackagePrivateJdkType() {
+        def err = shouldFail '''
+            import module java.base
+            def seen = null instanceof TimSort
+        '''
+        assert err.message.contains('unable to resolve class TimSort')
+    }
+
+    @Test
+    void testStarImportStillResolvesPublicTypes() {
+        assertScript '''
+            import java.util.*
+            assert ArrayList.name == 'java.util.ArrayList'
+            assert List.name == 'java.util.List'
+            assert new ArrayList() instanceof List
+        '''
+        assertScript '''
+            import java.util.Map.*
+            Entry e = [a:1].entrySet().first()
+            assert e.key == 'a'
+        '''
+        assertScript '''
+            import static java.util.Map.*
+            Entry e = [a:1].entrySet().first()
+            assert e instanceof Entry
+        '''
+        assertScript '''
+            import module java.base
+            assert ArrayList.name == 'java.util.ArrayList'
+            assert LocalDate.name == 'java.time.LocalDate'
+        '''
+    }
+
+    @Test
+    void testExplicitReferenceToPackagePrivateTypeStillResolves() {
+        assertScript '''
+            import java.util.TimSort
+            assert TimSort.name == 'java.util.TimSort'
+        '''
+        assertScript '''
+            assert java.util.TimSort.name == 'java.util.TimSort'
+        '''
+        assertScript '''
+            assert Class.forName('java.util.TimSort').name == 
'java.util.TimSort'
+        '''
+        compile(types, [
+            'q/Direct': '''
+                package q
+                import p.Hidden
+                class Direct {
+                    Hidden h
+                }
+            '''
+        ])
+    }
+
+    @Test
+    void testSamePackageStarImportSeesPackagePrivateType() {
+        compile(types, [
+            'p/Peer': '''
+                package p
+                import p.*
+                class Peer {
+                    Hidden h
+                    Visible v
+                }
+            '''
+        ])
+    }
+
+    @Test
+    void testOtherPackageStarImportDoesNotSeePackagePrivateType() {
+        def err = shouldFail {
+            compile(types, [
+                'q/User': '''
+                    package q
+                    import p.*
+                    class User {
+                        Hidden h
+                    }
+                '''
+            ])
+        }
+        assert err.message.contains('unable to resolve class Hidden')
+
+        compile(types, [
+            'q/PublicUser': '''
+                package q
+                import p.*
+                class PublicUser {
+                    Visible v
+                }
+            '''
+        ])
+    }
+
+    @Test
+    void testStarImportOfPublicMemberOfInaccessibleType() {
+        def err = shouldFail {
+            compile(types, [
+                'q/InnerUser': '''
+                    package q
+                    import p.Outer.*
+                    class InnerUser {
+                        Inner i
+                    }
+                '''
+            ])
+        }
+        assert err.message.contains('unable to resolve class Inner')
+
+        compile(types, [
+            'p/InnerPeer': '''
+                package p
+                import p.Outer.*
+                class InnerPeer {
+                    Inner i
+                }
+            '''
+        ])
+    }
+
+    @Test
+    void testProtectedMemberTypeFromSubclass() {
+        compile(types, [
+            'q/Child': '''
+                package q
+                import p.Protector
+                import p.Protector.*
+                class Child extends Protector {
+                    Secret s
+                }
+            '''
+        ])
+        def err = shouldFail {
+            compile(types, [
+                'q/Stranger': '''
+                    package q
+                    import p.Protector.*
+                    class Stranger {
+                        Secret s
+                    }
+                '''
+            ])
+        }
+        assert err.message.contains('unable to resolve class Secret')
+    }
+
+    @Test
+    void testPrivateMemberTypeStarImport() {
+        compile(types, [
+            'q/ShownUser': '''
+                package q
+                import p.Priv.*
+                class ShownUser {
+                    Shown s
+                }
+            '''
+        ])
+        compile(types, [
+            'q/ShownStatic': '''
+                package q
+                import static p.Priv.*
+                class ShownStatic {
+                    Shown s
+                }
+            '''
+        ])
+        def err = shouldFail {
+            compile(types, [
+                'q/SecretUser': '''
+                    package q
+                    import p.Priv.*
+                    class SecretUser {
+                        Secret s
+                    }
+                '''
+            ])
+        }
+        assert err.message.contains('unable to resolve class Secret')
+    }
+
+    @Test
+    void testInaccessibleStarImportDoesNotHideLaterPublicType() {
+        def loader = compile(types, [
+            'q/Collision': '''
+                package q
+                class Collision {}
+            ''',
+            'r/User': '''
+                package r
+                import p.*
+                import q.*
+                class User {
+                    static Class which() { Collision }
+                }
+            '''
+        ])
+        assert 
loader.loadClass('r.User').getDeclaredMethod('which').invoke(null).name == 
'q.Collision'
+    }
+
+    // GROOVY-11916
+    @Test
+    void testStarImportsBindFirstAccessibleType() {

Review Comment:
   Each method now says what it checks. 
`testStarImportsBindFirstAccessibleType`: `pa.X`, `pb.X`, and `pc.X` are 
missing, public, or package-private, and the use class imports the three stars 
in that order. The first public type wins. An inaccessible type does not hide a 
later public one. With no public type the name stays unresolved. A repeated 
star and the reversed list follow the same rule. 
`testSamePackageTypeShadowsOnDemandImports`: the use class is `pa.Use`. When 
`pa.X` exists it wins whether it is public or package-private. When it is 
missing, the first accessible star is used. 
`testDeclaredStarShadowsLanguageDefaults`: the use class names `String`. The 
first public `pb.String` or `pc.String` shadows `java.lang.String`. A 
package-private one is skipped. An explicit `import java.lang.*` is a source 
star too, so its position relative to `pb.*` decides the winner.





> Star imports should not resolve package-private types
> -----------------------------------------------------
>
>                 Key: GROOVY-11916
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11916
>             Project: Groovy
>          Issue Type: Bug
>            Reporter: Paul King
>            Priority: Major
>
> Groovy's star import resolution (e.g., import java.util.*) currently includes 
> package-private classes in name resolution. On modern JDKs (9+) with the 
> module system, these classes cannot actually be used — attempts result in 
> IllegalAccessError at runtime. This can lead to confusing errors and 
> inconsistency with Java's behaviour where star imports only resolve public 
> types.
> For example, import java.util.* allows def cls = TimSort to compile 
> (resolving the package-private java.util.TimSort), but any actual usage 
> (instanceof, construction, method calls) fails at runtime with 
> IllegalAccessError.
> {code:groovy}
> import java.util.*
> assert String.modifiers == 17
> def cls = TimSort     // fine
> assert cls.modifiers == 0
> println cls           // => class java.util.TimSort
> println Class.forName('java.util.TimSort') // => class java.util.TimSort
> println new Object() instanceof TimSort // IllegalAccessError
> {code}
> Having said that, as a counter example, is it really any different than being 
> able to get the class via Class.forName?
> If the Java behavior is deemed correct, the fix would be to filter non-public 
> classes during star import resolution in ResolveVisitor, producing a clean 
> compile-time error instead of a runtime error. This is technically a breaking 
> change but only affects code that was already failing at runtime on modular 
> JDKs.
> This also benefits the import module feature (GROOVY-11896), which expands to 
> star imports — filtering at the resolution level means module imports 
> correctly include only public types, matching Java's import module semantics.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to