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)
+ }
+ }
+}