You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2020/02/21 23:54:23 UTC

[groovy] branch master updated: GROOVY-9408: Avoid unnecessary looking up default import classes when… (#1169)

This is an automated email from the ASF dual-hosted git repository.

sunlan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 636fb81  GROOVY-9408: Avoid unnecessary looking up default import classes when… (#1169)
636fb81 is described below

commit 636fb8171f51f9f755eb80d5293ec1ece67fd91d
Author: Daniel.Sun <su...@apache.org>
AuthorDate: Sat Feb 22 07:54:10 2020 +0800

    GROOVY-9408: Avoid unnecessary looking up default import classes when… (#1169)
    
    * GROOVY-9408: Avoid unnecessary looking up default import classes when resolving types
    
    * Improve the robustness and tweak doco
---
 .../codehaus/groovy/control/ResolveVisitor.java    |   5 +
 .../org/codehaus/groovy/vmplugin/VMPlugin.java     |  14 ++
 .../codehaus/groovy/vmplugin/v9/ClassFinder.java   | 165 +++++++++++++++++++++
 .../org/codehaus/groovy/vmplugin/v9/Java9.java     |  50 +++++++
 .../groovy/vmplugin/v9/ClassFinderTest.groovy      | 145 ++++++++++++++++++
 5 files changed, 379 insertions(+)

diff --git a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
index 07c3c88..4395dc8 100644
--- a/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
+++ b/src/main/java/org/codehaus/groovy/control/ResolveVisitor.java
@@ -67,6 +67,7 @@ import org.codehaus.groovy.runtime.memoize.EvictableCache;
 import org.codehaus.groovy.runtime.memoize.UnlimitedConcurrentCache;
 import org.codehaus.groovy.syntax.Types;
 import org.codehaus.groovy.transform.trait.Traits;
+import org.codehaus.groovy.vmplugin.VMPluginFactory;
 import org.objectweb.asm.Opcodes;
 
 import java.lang.annotation.Annotation;
@@ -636,6 +637,10 @@ public class ResolveVisitor extends ClassCodeExpressionTransformer {
     }
 
     private static final EvictableCache<String, Set<String>> DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE = new UnlimitedConcurrentCache<>();
+    static {
+        Map<String, Set<String>> defaultImportClasses = VMPluginFactory.getPlugin().getDefaultImportClasses(DEFAULT_IMPORTS);
+        DEFAULT_IMPORT_CLASS_AND_PACKAGES_CACHE.putAll(defaultImportClasses);
+    }
 
     protected boolean resolveFromDefaultImports(final ClassNode type, final String[] packagePrefixes) {
         String typeName = type.getName();
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java b/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java
index 40ba4fb..7ef1065 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/VMPlugin.java
@@ -26,6 +26,9 @@ import org.codehaus.groovy.ast.CompileUnit;
 
 import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Interface to access VM version based actions.
@@ -111,4 +114,15 @@ public interface VMPlugin {
      * @return the transformed meta method
      */
     MetaMethod transformMetaMethod(MetaClass metaClass, MetaMethod metaMethod);
+
+    /**
+     * Returns the default import classes: class name -> the relevant package names
+     *
+     * @param packageNames the default import package names, e.g. java.lang.
+     * @return the default import classes
+     * @since 3.0.2
+     */
+    default Map<String, Set<String>> getDefaultImportClasses(String[] packageNames) {
+        return Collections.emptyMap();
+    }
 }
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v9/ClassFinder.java b/src/main/java/org/codehaus/groovy/vmplugin/v9/ClassFinder.java
new file mode 100644
index 0000000..4bfe659
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v9/ClassFinder.java
@@ -0,0 +1,165 @@
+/*
+ *  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.codehaus.groovy.vmplugin.v9;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Find classes under the specified package via some classpath entry
+ *
+ * Usage:
+ * <pre><code>
+ *   // find classes under `me.sunlan` package via classpath entry(directory) `D:/_APPS/git_apps/java8-labs/out/production/classes/`
+ *   ClassFinder.find(URI.create("file:/D:/_APPS/git_apps/java8-labs/out/production/classes/"), "me/sunlan")
+ *
+ *   // find classes under `groovy.lang` package via classpath entry(jar file) `D:/_DEV/Groovy/groovy-3.0.1/lib/groovy-3.0.1.jar`
+ *   ClassFinder.find(URI.create("file:/D:/_DEV/Groovy/groovy-3.0.1/lib/groovy-3.0.1.jar"), "groovy/lang")
+ *
+ *   // find classes under `java.lang` package via classpath entry(jrt)
+ *   ClassFinder.find(URI.create("jrt:/modules/java.base/"), "java/lang")
+ *
+ *   // find classes under the sub-packages too, e.g. we can get {@link groovy.lang.groovydoc.GroovydocHolder} via the following code
+ *   ClassFinder.find(URI.create("file:/D:/_DEV/Groovy/groovy-3.0.1/lib/groovy-3.0.1.jar"), "groovy/lang", true)
+ * </code></pre>
+ *
+ * @since 3.0.2
+ */
+public class ClassFinder {
+    /**
+     * Returns the found classes
+     *
+     * @param classpathEntryURI the classpath entry
+     * @param packageName the package under which we find classes
+     * @return the found classes
+     * @since 3.0.2
+     */
+    public static Map<String, Set<String>> find(URI classpathEntryURI, String packageName) {
+        return find(classpathEntryURI, packageName, false);
+    }
+
+    /**
+     * Returns the found classes
+     *
+     * @param classpathEntryURI the classpath entry
+     * @param packageName the package under which we find classes
+     * @param recursive whether to find sub-packages
+     * @return the found classes
+     * @since 3.0.2
+     */
+    public static Map<String, Set<String>> find(URI classpathEntryURI, String packageName, boolean recursive) {
+        URI uri;
+        String prefix;
+        String scheme = classpathEntryURI.getScheme();
+        if ("jrt".equals(scheme)) {
+            uri = URI.create("jrt:/");
+            prefix = classpathEntryURI.getPath().substring(1) + "/";
+        } else if ("file".equals(scheme)) {
+            Path path = Paths.get(classpathEntryURI);
+            if (!Files.exists(path)) {
+                throw new IllegalArgumentException(path + " does not exist");
+            }
+
+            if (Files.isDirectory(path)) {
+                uri = URI.create("file:/");
+                prefix = path.toString();
+            } else {
+                uri = URI.create("jar:" + classpathEntryURI.toString());
+                prefix = "";
+            }
+        } else {
+            throw new UnsupportedOperationException(classpathEntryURI + " is not supported");
+        }
+
+        return find(uri, prefix, packageName, recursive);
+    }
+
+    private static Map<String, Set<String>> find(URI uri, String prefix, String packageName, boolean recursive) {
+        boolean wfs = "file".equals(uri.getScheme());
+        if (wfs) prefix = prefix.replace("/", File.separator);
+
+        final String sepPatten = Pattern.quote(wfs ? File.separator : "/");
+        final int prefixElemCnt = prefix.trim().isEmpty() ? 0 : prefix.split(sepPatten).length;
+
+        Map<String, Set<String>> result = new LinkedHashMap<>();
+        try (FileSystem fs = newFileSystem(uri)) {
+            Files.walkFileTree(fs.getPath(prefix + "/" + packageName), new SimpleFileVisitor<Path>() {
+                @Override
+                public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) {
+                    return FileVisitResult.CONTINUE;
+                }
+
+                @Override
+                public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
+                    String[] pathElems = path.toString().split(sepPatten);
+                    int nameCount = pathElems.length;
+                    if (nameCount <= prefixElemCnt) {
+                        return FileVisitResult.CONTINUE;
+                    }
+
+                    String filename = pathElems[nameCount - 1];
+                    if (!filename.endsWith(".class") || "module-info.class".equals(filename) || "package-info.class".equals(filename)) {
+                        return FileVisitResult.CONTINUE;
+                    }
+
+                    String packagePathStr = String.join("/", Arrays.copyOfRange(pathElems, prefixElemCnt + (!wfs && pathElems[0].isEmpty() ? 1 : 0), nameCount - 1));
+
+                    if (recursive || packageName.equals(packagePathStr)) {
+                        Set<String> packageNameSet = result.computeIfAbsent(filename.substring(0, filename.lastIndexOf(".")), f -> new HashSet<>(2));
+                        packageNameSet.add(packagePathStr);
+                    }
+
+                    return FileVisitResult.CONTINUE;
+                }
+            });
+        } catch (UnsupportedOperationException ignored) {
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return result;
+    }
+
+    private static FileSystem newFileSystem(URI uri) throws IOException {
+        try {
+            return FileSystems.newFileSystem(uri, Collections.emptyMap());
+        } catch (FileSystemAlreadyExistsException e) {
+            return FileSystems.getFileSystem(uri);
+        }
+    }
+
+    private ClassFinder() {}
+}
diff --git a/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java b/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java
index cfcb9d8..60b354d 100644
--- a/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java
+++ b/src/main/java/org/codehaus/groovy/vmplugin/v9/Java9.java
@@ -19,6 +19,7 @@
 package org.codehaus.groovy.vmplugin.v9;
 
 import groovy.lang.GroovyRuntimeException;
+import groovy.lang.GroovySystem;
 import groovy.lang.MetaClass;
 import groovy.lang.MetaMethod;
 import groovy.lang.Tuple;
@@ -27,6 +28,7 @@ import org.codehaus.groovy.GroovyBugError;
 import org.codehaus.groovy.reflection.CachedClass;
 import org.codehaus.groovy.reflection.CachedMethod;
 import org.codehaus.groovy.reflection.ReflectionUtils;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 import org.codehaus.groovy.vmplugin.v8.Java8;
 
 import java.lang.invoke.MethodHandle;
@@ -41,9 +43,12 @@ import java.lang.reflect.Member;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.math.BigInteger;
+import java.net.URI;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -55,6 +60,51 @@ import java.util.stream.Collectors;
  * Additional Java 9 based functions will be added here as needed.
  */
 public class Java9 extends Java8 {
+    @Override
+    public Map<String, Set<String>> getDefaultImportClasses(String[] packageNames) {
+        Map<String, Set<String>> result = new LinkedHashMap<>();
+
+        List<String> javaPns = new ArrayList<>(4);
+        List<String> groovyPns = new ArrayList<>(4);
+        for (String prefix : packageNames) {
+            String pn = prefix.substring(0, prefix.length() - 1).replace('.', '/');
+
+            if (pn.startsWith("java/")) {
+                javaPns.add(pn);
+            } else if (pn.startsWith("groovy/")) {
+                groovyPns.add(pn);
+            } else {
+                throw new GroovyBugError("unexpected package: " + pn);
+            }
+        }
+
+        result.putAll(doFindClasses(URI.create("jrt:/modules/java.base/"), "java", javaPns));
+
+        try {
+            URI gsLocation = DefaultGroovyMethods.getLocation(GroovySystem.class).toURI();
+            result.putAll(doFindClasses(gsLocation, "groovy", groovyPns));
+        } catch (Exception e) {
+            System.err.println("[WARNING] Failed to get default imported groovy classes: " + e.getMessage());
+        }
+
+        return result;
+    }
+
+    private static Map<String, Set<String>> doFindClasses(URI uri, String packageName, List<String> defaultPackageNames) {
+        Map<String, Set<String>> result = ClassFinder.find(uri, packageName, true)
+                .entrySet().stream()
+                .filter(e -> e.getValue().stream().anyMatch(defaultPackageNames::contains))
+                .collect(
+                        Collectors.toMap(
+                                Map.Entry::getKey,
+                                entry -> entry.getValue().stream()
+                                        .filter(e -> defaultPackageNames.contains(e))
+                                        .map(e -> e.replace('/', '.') + ".")
+                                        .collect(Collectors.toSet())
+                        )
+                );
+        return result;
+    }
 
     private static class LookupHolder {
         private static final Method PRIVATE_LOOKUP;
diff --git a/src/test/org/codehaus/groovy/vmplugin/v9/ClassFinderTest.groovy b/src/test/org/codehaus/groovy/vmplugin/v9/ClassFinderTest.groovy
new file mode 100644
index 0000000..37ee4b0
--- /dev/null
+++ b/src/test/org/codehaus/groovy/vmplugin/v9/ClassFinderTest.groovy
@@ -0,0 +1,145 @@
+/*
+ *  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.codehaus.groovy.vmplugin.v9
+
+import org.codehaus.groovy.control.ResolveVisitor
+import org.codehaus.groovy.runtime.DefaultGroovyMethods
+import org.codehaus.groovy.vmplugin.VMPluginFactory
+import org.junit.Test
+
+import java.util.stream.Collectors
+
+class ClassFinderTest {
+    @Test
+    void findGroovyClass() {
+        Map<String, Set<String>> result = ClassFinder.find(GroovySystem.location.toURI(), "groovy/lang")
+        assert ["groovy/lang"] == result.get("GString")?.toList()
+        assert null == result.get("GroovydocHolder")
+    }
+
+    @Test
+    void findGroovyClass2() {
+        Map<String, Set<String>> result = ClassFinder.find(GroovySystem.location.toURI(), "groovy/util")
+        assert ["groovy/util"] == result.get("NodeBuilder")?.toList()
+    }
+
+    @Test
+    void findGroovyClass3() {
+        Map<String, Set<String>> result = ClassFinder.find(org.codehaus.groovy.control.ResolveVisitor.location.toURI(), "org/codehaus/groovy/control")
+        assert ["org/codehaus/groovy/control"] == result.get("ResolveVisitor")?.toList()
+    }
+
+    @Test
+    void findGroovyClassRecursive() {
+        Map<String, Set<String>> result = ClassFinder.find(GroovySystem.location.toURI(), "groovy/lang", true)
+        assert ["groovy/lang"] == result.get("GString")?.toList()
+        assert ["groovy/lang/groovydoc"] == result.get("GroovydocHolder")?.toList()
+    }
+
+    @Test
+    void findJavaClass() {
+        Map<String, Set<String>> result = ClassFinder.find(URI.create("jrt:/modules/java.base/"), "java/lang")
+        assert ["java/lang"] == result.get("String")?.toList()
+        assert null == result.get("MethodHandle")
+    }
+
+    @Test
+    void findJavaClass2() {
+        Map<String, Set<String>> result = ClassFinder.find(URI.create("jrt:/modules/java.base/"), "java/util")
+        assert ["java/util"] == result.get("Map")?.toList()
+    }
+
+    @Test
+    void findJavaClass3() {
+        Map<String, Set<String>> result = ClassFinder.find(URI.create("jrt:/modules/java.base/"), "java/io")
+        assert ["java/io"] == result.get("InputStream")?.toList()
+    }
+
+    @Test
+    void findJavaClass4() {
+        Map<String, Set<String>> result = ClassFinder.find(URI.create("jrt:/modules/java.base/"), "java/net")
+        assert ["java/net"] == result.get("Inet4Address")?.toList()
+    }
+
+    @Test
+    void findJavaClassRecursive() {
+        Map<String, Set<String>> result = ClassFinder.find(URI.create("jrt:/modules/java.base/"), "java/lang", true)
+        assert ["java/lang/invoke"] == result.get("MethodHandle")?.toList()
+    }
+
+    @Test
+    void findJarClass() {
+        Map<String, Set<String>> result = ClassFinder.find(org.antlr.v4.runtime.tree.ParseTree.location.toURI(), "org/antlr/v4/runtime/tree")
+        assert ["org/antlr/v4/runtime/tree"] == result.get("ParseTree")?.toList()
+        assert null == result.get("ParseTreePattern")
+    }
+
+    @Test
+    void findJarClassRecursive() {
+        Map<String, Set<String>> result = ClassFinder.find(org.antlr.v4.runtime.tree.ParseTree.location.toURI(), "org/antlr/v4/runtime/tree", true)
+        assert ["org/antlr/v4/runtime/tree"] == result.get("ParseTree")?.toList()
+        assert ["org/antlr/v4/runtime/tree/pattern"] == result.get("ParseTreePattern")?.toList()
+    }
+
+    @Test
+    void defaultImportClasses() {
+        Map<String, Set<String>> r1 = VMPluginFactory.getPlugin().getDefaultImportClasses(ResolveVisitor.DEFAULT_IMPORTS) as TreeMap<String, Set<String>>
+
+        assert (ResolveVisitor.DEFAULT_IMPORTS as List).sort() == r1.values().stream().flatMap(e -> e.stream()).collect(Collectors.toSet()).sort()
+
+        def r2 = [:] as TreeMap
+        URI gsLocation = null;
+        try {
+            gsLocation = DefaultGroovyMethods.getLocation(GroovySystem.class).toURI();
+        } catch (URISyntaxException e) {
+            e.printStackTrace();
+        }
+
+        for (String prefix : ResolveVisitor.DEFAULT_IMPORTS) {
+            String pn = prefix.substring(0, prefix.length() - 1).replace('.', '/');
+
+            Map<String, Set<String>> map = Collections.emptyMap();
+            if (pn.startsWith("java/")) {
+                map = ClassFinder.find(URI.create("jrt:/modules/java.base/"), pn);
+            } else if (pn.startsWith("groovy/")) {
+                if (null != gsLocation) {
+                    map = ClassFinder.find(gsLocation, pn);
+                }
+            }
+
+            map = map.entrySet().stream()
+                    .collect(
+                            Collectors.toMap(
+                                    (Map.Entry entry) -> entry.getKey(),
+                                    (Map.Entry entry) -> entry.getValue().stream()
+                                            .map(e -> e.replace('/', '.') + ".")
+                                            .collect(Collectors.toSet())
+                            )
+                    );
+
+            r2.putAll(map);
+        }
+
+        assert r1.size() == r2.size()
+        assert r1.keySet() == r2.keySet()
+        r1.keySet().each { c1 ->
+            assert (r1.get(c1) as TreeSet) == (r2.get(c1) as TreeSet)
+        }
+    }
+}