diff --git a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/BuilderFactoryBenchmark.java b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/BuilderFactoryBenchmark.java new file mode 100644 index 00000000000..13417d6da2b --- /dev/null +++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/BuilderFactoryBenchmark.java @@ -0,0 +1,57 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed 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 + *

+ * https://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.openrewrite.benchmarks.java; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openrewrite.java.JavaParser; + +import java.util.concurrent.TimeUnit; + +/** + * Microbenchmark for JavaParser.fromJavaVersion() builder factory. + * Measures the overhead of reflection vs MethodHandle invocation. + */ +@Fork(value = 2, warmups = 1) +@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 3, time = 1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +public class BuilderFactoryBenchmark { + + /** + * Measures the cost of repeatedly calling fromJavaVersion() to obtain + * a new builder instance. This exercises the cached supplier's invoke path. + */ + @Benchmark + public void builderFactory(Blackhole bh) { + JavaParser.Builder builder = JavaParser.fromJavaVersion(); + bh.consume(builder); + } + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(BuilderFactoryBenchmark.class.getSimpleName()) + .shouldFailOnError(true) + .build(); + new Runner(opt).run(); + } +} diff --git a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaSourceSetBenchmark.java b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaSourceSetBenchmark.java index e21aab4dd12..75ffeb5074b 100644 --- a/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaSourceSetBenchmark.java +++ b/rewrite-benchmarks/src/jmh/java/org/openrewrite/benchmarks/java/JavaSourceSetBenchmark.java @@ -29,10 +29,4 @@ public void setup() { public void jarIOBenchmark() { JavaSourceSet.build("main", classpath); } - - @Benchmark - public void classgraphBenchmark() { - //noinspection deprecation - JavaSourceSet.build("main", classpath, new JavaTypeCache(), false); - } } diff --git a/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11Parser.java b/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11Parser.java index 0fe766a526c..e194ca9169a 100644 --- a/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11Parser.java +++ b/rewrite-java-11/src/main/java/org/openrewrite/java/isolated/ReloadableJava11Parser.java @@ -380,6 +380,8 @@ public ReloadableJava11Parser build() { private static class ByteArrayCapableJavacFileManager extends JavacFileManager { private final List classByteClasspath; + private final IdentityHashMap inferBinaryNameCache = new IdentityHashMap<>(); + private final HashMap> listCache = new HashMap<>(); public ByteArrayCapableJavacFileManager(Context context, boolean register, @@ -396,19 +398,52 @@ public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof PackageAwareJavaFileObject) { return ((PackageAwareJavaFileObject) file).getClassName(); } - return super.inferBinaryName(location, file); + String cached = inferBinaryNameCache.get(file); + if (cached != null) { + return cached; + } + String result = super.inferBinaryName(location, file); + if (result != null) { + inferBinaryNameCache.put(file, result); + } + return result; + } + + @Override + public void flush() { + super.flush(); + inferBinaryNameCache.clear(); + listCache.clear(); + } + + @Override + public void setLocationFromPaths(Location location, Collection paths) throws IOException { + super.setLocationFromPaths(location, paths); + inferBinaryNameCache.clear(); + listCache.clear(); } @Override public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { + String key = location.getName() + ':' + packageName + ':' + kinds + ':' + recurse; + List cached = listCache.get(key); + if (cached != null) { + return cached; + } + List result; if (StandardLocation.CLASS_PATH.equals(location)) { Iterable listed = super.list(location, packageName, kinds, recurse); - return Stream.concat(classByteClasspath.stream() + result = Stream.concat(classByteClasspath.stream() .filter(jfo -> jfo.getPackage().equals(packageName)), StreamSupport.stream(listed.spliterator(), false) ).collect(toList()); + } else { + Iterable listed = super.list(location, packageName, kinds, recurse); + result = listed instanceof List ? (List) listed : + StreamSupport.stream(listed.spliterator(), false).collect(toList()); } - return super.list(location, packageName, kinds, recurse); + listCache.put(key, result); + return result; } } diff --git a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17Parser.java b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17Parser.java index 73f23946e1e..bdbe7cc5ad4 100644 --- a/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17Parser.java +++ b/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17Parser.java @@ -351,6 +351,10 @@ public ReloadableJava17Parser build() { private static class ByteArrayCapableJavacFileManager extends JavacFileManager { private final List classByteClasspath; + private final IdentityHashMap inferBinaryNameCache = new IdentityHashMap<>(); + private final HashMap> listCache = new HashMap<>(); + + private record ListCacheKey(Location location, String packageName, Set kinds, boolean recurse) {} public ByteArrayCapableJavacFileManager(Context context, boolean register, @@ -367,20 +371,52 @@ public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof PackageAwareJavaFileObject) { return ((PackageAwareJavaFileObject) file).getClassName(); } - return super.inferBinaryName(location, file); + String cached = inferBinaryNameCache.get(file); + if (cached != null) { + return cached; + } + String result = super.inferBinaryName(location, file); + if (result != null) { + inferBinaryNameCache.put(file, result); + } + return result; + } + + @Override + public void flush() { + super.flush(); + inferBinaryNameCache.clear(); + listCache.clear(); + } + + @Override + public void setLocationFromPaths(Location location, Collection paths) throws IOException { + super.setLocationFromPaths(location, paths); + inferBinaryNameCache.clear(); + listCache.clear(); } @Override public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { - if (StandardLocation.CLASS_PATH.equals(location)) { + ListCacheKey key = new ListCacheKey(location, packageName, kinds, recurse); + List cached = listCache.get(key); + if (cached != null) { + return cached; + } + List result; + if (StandardLocation.CLASS_PATH.equals(location) && !classByteClasspath.isEmpty()) { Iterable listed = super.list(location, packageName, kinds, recurse); - return classByteClasspath.isEmpty() ? listed : - Stream.concat(classByteClasspath.stream() + result = Stream.concat(classByteClasspath.stream() .filter(jfo -> jfo.getPackage().equals(packageName)), StreamSupport.stream(listed.spliterator(), false) ).collect(toList()); + } else { + Iterable listed = super.list(location, packageName, kinds, recurse); + result = listed instanceof List ? (List) listed : + StreamSupport.stream(listed.spliterator(), false).collect(toList()); } - return super.list(location, packageName, kinds, recurse); + listCache.put(key, result); + return result; } } diff --git a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21Parser.java b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21Parser.java index fb4e271213c..6ac1fc3d4f6 100644 --- a/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21Parser.java +++ b/rewrite-java-21/src/main/java/org/openrewrite/java/isolated/ReloadableJava21Parser.java @@ -351,6 +351,10 @@ public ReloadableJava21Parser build() { private static class ByteArrayCapableJavacFileManager extends JavacFileManager { private final List classByteClasspath; + private final IdentityHashMap inferBinaryNameCache = new IdentityHashMap<>(); + private final HashMap> listCache = new HashMap<>(); + + private record ListCacheKey(Location location, String packageName, Set kinds, boolean recurse) {} public ByteArrayCapableJavacFileManager(Context context, boolean register, @@ -367,20 +371,52 @@ public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof PackageAwareJavaFileObject) { return ((PackageAwareJavaFileObject) file).getClassName(); } - return super.inferBinaryName(location, file); + String cached = inferBinaryNameCache.get(file); + if (cached != null) { + return cached; + } + String result = super.inferBinaryName(location, file); + if (result != null) { + inferBinaryNameCache.put(file, result); + } + return result; + } + + @Override + public void flush() { + super.flush(); + inferBinaryNameCache.clear(); + listCache.clear(); + } + + @Override + public void setLocationFromPaths(Location location, Collection paths) throws IOException { + super.setLocationFromPaths(location, paths); + inferBinaryNameCache.clear(); + listCache.clear(); } @Override public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { - if (StandardLocation.CLASS_PATH.equals(location)) { + ListCacheKey key = new ListCacheKey(location, packageName, kinds, recurse); + List cached = listCache.get(key); + if (cached != null) { + return cached; + } + List result; + if (StandardLocation.CLASS_PATH.equals(location) && !classByteClasspath.isEmpty()) { Iterable listed = super.list(location, packageName, kinds, recurse); - return classByteClasspath.isEmpty() ? listed : - Stream.concat(classByteClasspath.stream() + result = Stream.concat(classByteClasspath.stream() .filter(jfo -> jfo.getPackage().equals(packageName)), StreamSupport.stream(listed.spliterator(), false) ).collect(toList()); + } else { + Iterable listed = super.list(location, packageName, kinds, recurse); + result = listed instanceof List ? (List) listed : + StreamSupport.stream(listed.spliterator(), false).collect(toList()); } - return super.list(location, packageName, kinds, recurse); + listCache.put(key, result); + return result; } } diff --git a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25Parser.java b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25Parser.java index 745ffcc72c2..0f4f31bed34 100644 --- a/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25Parser.java +++ b/rewrite-java-25/src/main/java/org/openrewrite/java/isolated/ReloadableJava25Parser.java @@ -351,6 +351,10 @@ public ReloadableJava25Parser build() { private static class ByteArrayCapableJavacFileManager extends JavacFileManager { private final List classByteClasspath; + private final IdentityHashMap inferBinaryNameCache = new IdentityHashMap<>(); + private final HashMap> listCache = new HashMap<>(); + + private record ListCacheKey(Location location, String packageName, Set kinds, boolean recurse) {} public ByteArrayCapableJavacFileManager(Context context, boolean register, @@ -367,20 +371,52 @@ public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof PackageAwareJavaFileObject) { return ((PackageAwareJavaFileObject) file).getClassName(); } - return super.inferBinaryName(location, file); + String cached = inferBinaryNameCache.get(file); + if (cached != null) { + return cached; + } + String result = super.inferBinaryName(location, file); + if (result != null) { + inferBinaryNameCache.put(file, result); + } + return result; + } + + @Override + public void flush() { + super.flush(); + inferBinaryNameCache.clear(); + listCache.clear(); + } + + @Override + public void setLocationFromPaths(Location location, Collection paths) throws IOException { + super.setLocationFromPaths(location, paths); + inferBinaryNameCache.clear(); + listCache.clear(); } @Override public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { - if (StandardLocation.CLASS_PATH.equals(location)) { + ListCacheKey key = new ListCacheKey(location, packageName, kinds, recurse); + List cached = listCache.get(key); + if (cached != null) { + return cached; + } + List result; + if (StandardLocation.CLASS_PATH.equals(location) && !classByteClasspath.isEmpty()) { Iterable listed = super.list(location, packageName, kinds, recurse); - return classByteClasspath.isEmpty() ? listed : - Stream.concat(classByteClasspath.stream() + result = Stream.concat(classByteClasspath.stream() .filter(jfo -> jfo.getPackage().equals(packageName)), StreamSupport.stream(listed.spliterator(), false) ).collect(toList()); + } else { + Iterable listed = super.list(location, packageName, kinds, recurse); + result = listed instanceof List ? (List) listed : + StreamSupport.stream(listed.spliterator(), false).collect(toList()); } - return super.list(location, packageName, kinds, recurse); + listCache.put(key, result); + return result; } } diff --git a/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8Parser.java b/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8Parser.java index 15f05bb8edd..0202b878a1e 100644 --- a/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8Parser.java +++ b/rewrite-java-8/src/main/java/org/openrewrite/java/ReloadableJava8Parser.java @@ -310,6 +310,8 @@ public void reset(Collection uris) { private static class ByteArrayCapableJavacFileManager extends JavacFileManager { private final List classByteClasspath; + private final IdentityHashMap inferBinaryNameCache = new IdentityHashMap<>(); + private final HashMap> listCache = new HashMap<>(); public ByteArrayCapableJavacFileManager(Context context, boolean register, @@ -331,20 +333,53 @@ public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof PackageAwareJavaFileObject) { return ((PackageAwareJavaFileObject) file).getClassName(); } - return super.inferBinaryName(location, file); + String cached = inferBinaryNameCache.get(file); + if (cached != null) { + return cached; + } + String result = super.inferBinaryName(location, file); + if (result != null) { + inferBinaryNameCache.put(file, result); + } + return result; + } + + @Override + public void flush() { + super.flush(); + inferBinaryNameCache.clear(); + listCache.clear(); + } + + @Override + public void setLocation(Location location, Iterable files) throws IOException { + super.setLocation(location, files); + inferBinaryNameCache.clear(); + listCache.clear(); } @Override public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException { + String cacheKey = location.getName() + ':' + packageName + ':' + kinds + ':' + recurse; + List cached = listCache.get(cacheKey); + if (cached != null) { + return cached; + } + List result; if (StandardLocation.CLASS_PATH == location) { Iterable listed = super.list(location, packageName, kinds, recurse); - return Stream.concat( + result = Stream.concat( classByteClasspath.stream() .filter(jfo -> jfo.getPackage().equals(packageName)), StreamSupport.stream(listed.spliterator(), false) ).collect(toList()); + } else { + Iterable listed = super.list(location, packageName, kinds, recurse); + result = listed instanceof List ? (List) listed : + StreamSupport.stream(listed.spliterator(), false).collect(toList()); } - return super.list(location, packageName, kinds, recurse); + listCache.put(cacheKey, result); + return result; } } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/Assertions.java b/rewrite-java/src/main/java/org/openrewrite/java/Assertions.java index 1e4ba58c09a..dfd0eeff068 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/Assertions.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/Assertions.java @@ -243,7 +243,7 @@ public static SourceSpec sourceSet(SourceSpec sourceSpec, String sourceSet public static UncheckedConsumer> addTypesToSourceSet(String sourceSetName, List extendsFrom, List classpath) { return sourceFiles -> { - JavaSourceSet sourceSet = JavaSourceSet.build(sourceSetName, classpath, new JavaTypeCache(), false); + JavaSourceSet sourceSet = JavaSourceSet.build(sourceSetName, classpath); for (int i = 0; i < sourceFiles.size(); i++) { SourceFile sourceFile = sourceFiles.get(i); diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java index 92fc9ec6c36..5c1394b6615 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java @@ -29,6 +29,9 @@ import org.openrewrite.style.NamedStyles; import java.io.ByteArrayInputStream; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.net.URI; import java.nio.charset.Charset; @@ -366,21 +369,22 @@ default Path sourcePathFromSourceText(Path prefix, String sourceCode) { return resolveSourcePathFromSourceText(prefix, sourceCode); } + Pattern SOURCE_PATH_PACKAGE_PATTERN = Pattern.compile("^package\\s+([^;]+);"); + Pattern SOURCE_PATH_CLASS_PATTERN = Pattern.compile("(class|interface|enum|record)\\s*(<[^>]*>)?\\s+(\\w+)"); + Pattern SOURCE_PATH_PUBLIC_CLASS_PATTERN = Pattern.compile("public\\s+" + SOURCE_PATH_CLASS_PATTERN.pattern()); + static Path resolveSourcePathFromSourceText(Path prefix, String sourceCode) { - Pattern packagePattern = Pattern.compile("^package\\s+([^;]+);"); - Pattern classPattern = Pattern.compile("(class|interface|enum|record)\\s*(<[^>]*>)?\\s+(\\w+)"); - Pattern publicClassPattern = Pattern.compile("public\\s+" + classPattern.pattern()); Function simpleName = sourceStr -> { - Matcher classMatcher = publicClassPattern.matcher(sourceStr); + Matcher classMatcher = SOURCE_PATH_PUBLIC_CLASS_PATTERN.matcher(sourceStr); if (classMatcher.find()) { return classMatcher.group(3); } - classMatcher = classPattern.matcher(sourceStr); + classMatcher = SOURCE_PATH_CLASS_PATTERN.matcher(sourceStr); return classMatcher.find() ? classMatcher.group(3) : null; }; - Matcher packageMatcher = packagePattern.matcher(sourceCode); + Matcher packageMatcher = SOURCE_PATH_PACKAGE_PATTERN.matcher(sourceCode); String pkg = packageMatcher.find() ? packageMatcher.group(1).replace('.', '/') + "/" : ""; String className = Optional.ofNullable(simpleName.apply(sourceCode)) @@ -479,16 +483,21 @@ class JdkParserBuilderCache { private static @Nullable Supplier> tryCreateBuilderSupplier(String className) { try { Class clazz = Class.forName(className); - Method builderMethod = clazz.getDeclaredMethod("builder"); + // Use MethodHandle instead of reflection for faster invocation + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + // Get the actual return type from the method (e.g., Java21Parser.Builder) + Method method = clazz.getMethod("builder"); + MethodHandle builderHandle = lookup.findStatic(clazz, "builder", + MethodType.methodType(method.getReturnType())); return () -> { try { //noinspection rawtypes,unchecked - return (JavaParser.Builder) builderMethod.invoke(null); + return (JavaParser.Builder) builderHandle.invoke(); } catch (Throwable e) { throw new RuntimeException("Failed to invoke builder() on " + className, e); } }; - } catch (ClassNotFoundException | NoSuchMethodException e) { + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { return null; // This parser version isn't available } } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/parser/TypeTable.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/parser/TypeTable.java index ffa56a2f2e6..7b3ca4a623b 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/parser/TypeTable.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/parser/TypeTable.java @@ -98,6 +98,8 @@ public class TypeTable implements JavaParserClasspathLoader { public static final String DEFAULT_RESOURCE_PATH = "META-INF/rewrite/classpath.tsv.gz"; private static final Map> classesDirByArtifact = new ConcurrentHashMap<>(); + private static final Map artifactPatternCache = new ConcurrentHashMap<>(); + private static final Pattern PIPE = Pattern.compile("\\|"); public static @Nullable TypeTable fromClasspath(ExecutionContext ctx, Collection artifactNames) { try { @@ -160,7 +162,8 @@ private static void read(URL url, Collection artifactNames, ExecutionCon private static Collection artifactsNotYetWritten(Collection artifactNames) { Collection notWritten = new ArrayList<>(artifactNames); for (String artifactName : artifactNames) { - Pattern artifactPattern = Pattern.compile(artifactName + ".*"); + Pattern artifactPattern = artifactPatternCache.computeIfAbsent(artifactName, + name -> Pattern.compile(name + ".*")); for (GroupArtifactVersion groupArtifactVersion : classesDirByArtifact.keySet()) { if (artifactPattern .matcher(groupArtifactVersion.getArtifactId() + "-" + groupArtifactVersion.getVersion()) @@ -287,7 +290,7 @@ public void parseTsvAndProcess(InputStream is, Options options, name, fields[5].isEmpty() ? null : fields[5], fields[6].isEmpty() ? null : fields[6], - fields[7].isEmpty() ? null : fields[7].split("\\|"), + fields[7].isEmpty() ? null : PIPE.split(fields[7]), fields.length > 14 && !fields[14].isEmpty() ? fields[14] : null, // elementAnnotations - raw string (may have | delimiters) fields.length > 17 && !fields[17].isEmpty() ? fields[17] : null // constantValue moved to column 17 )); @@ -305,8 +308,8 @@ public void parseTsvAndProcess(InputStream is, Options options, fields[9], fields[10], fields[11].isEmpty() ? null : fields[11], - fields[12].isEmpty() ? null : fields[12].split("\\|"), - fields[13].isEmpty() ? null : fields[13].split("\\|"), + fields[12].isEmpty() ? null : PIPE.split(fields[12]), + fields[13].isEmpty() ? null : PIPE.split(fields[13]), fields.length > 14 && !fields[14].isEmpty() ? fields[14] : null, // elementAnnotations - raw string fields.length > 15 && !fields[15].isEmpty() ? fields[15] : null, fields.length > 16 && !fields[16].isEmpty() ? TsvEscapeUtils.splitAnnotationList(fields[16], '|') : null, // typeAnnotations - keep `|` delimiter between different type contexts @@ -601,9 +604,11 @@ public static Writer newWriter(OutputStream out) { @Override public @Nullable Path load(String artifactName) { + Pattern artifactPattern = artifactPatternCache.computeIfAbsent(artifactName, + name -> Pattern.compile(name + ".*")); for (Map.Entry> gavAndClassesDir : classesDirByArtifact.entrySet()) { GroupArtifactVersion gav = gavAndClassesDir.getKey(); - if (Pattern.compile(artifactName + ".*") + if (artifactPattern .matcher(gav.getArtifactId() + "-" + gav.getVersion()) .matches()) { return gavAndClassesDir.getValue().join(); diff --git a/rewrite-java/src/main/java/org/openrewrite/java/marker/JavaSourceSet.java b/rewrite-java/src/main/java/org/openrewrite/java/marker/JavaSourceSet.java index 97c82154eac..51f53921c2a 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/marker/JavaSourceSet.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/marker/JavaSourceSet.java @@ -55,51 +55,6 @@ public class JavaSourceSet implements SourceSet { */ Map> gavToTypes; - /** - * Extract type information from the provided classpath. - * Uses ClassGraph to compute the classpath. - *

- * Does not support gavToTypes or typeToGav mapping - * - * @param fullTypeInformation Not used, does not do anything, to be deleted - * @param ignore Not used, does not do anything, to be deleted - */ - @Deprecated - public static JavaSourceSet build(String sourceSetName, Collection classpath, - JavaTypeCache ignore, boolean fullTypeInformation) { - if (fullTypeInformation) { - throw new UnsupportedOperationException(); - } - - List typeNames; - if (!classpath.iterator().hasNext()) { - // Only load JRE-provided types - try (ScanResult scanResult = new ClassGraph() - .enableClassInfo() - .enableSystemJarsAndModules() - .acceptPackages("java") - .ignoreClassVisibility() - .scan()) { - typeNames = packagesToTypeDeclarations(scanResult); - } - } else { - // Load types from the classpath - try (ScanResult scanResult = new ClassGraph() - .overrideClasspath(classpath) - .enableSystemJarsAndModules() - .enableClassInfo() - .ignoreClassVisibility() - .scan()) { - typeNames = packagesToTypeDeclarations(scanResult); - } - } - - // Peculiarly, Classgraph will not return a ClassInfo for java.lang.Object, although it does for all other java.lang types - typeNames.add("java.lang.Object"); - return new JavaSourceSet(randomId(), sourceSetName, typesFrom(typeNames), emptyMap()); - } - - /* * Create a map of package names to types contained within that package. Type names are not fully qualified, except for type parameter bounds. * e.g.: "java.util" -> [List, Date] @@ -194,6 +149,16 @@ private static List typesFrom(List typeNames) { // Purely IO-based classpath scanning below this point + /** + * @deprecated Use {@link #build(String, Collection)} instead. The {@code JavaTypeCache} and + * {@code fullTypeInformation} parameters are no longer used. + */ + @Deprecated + public static JavaSourceSet build(String sourceSetName, Collection classpath, + JavaTypeCache ignore, boolean fullTypeInformation) { + return build(sourceSetName, classpath); + } + /** * Extract type information from the provided classpath. * Uses file I/O to compute the classpath. diff --git a/rewrite-java/src/main/java/org/openrewrite/java/style/Autodetect.java b/rewrite-java/src/main/java/org/openrewrite/java/style/Autodetect.java index 9752d878f1e..65ea96e26e6 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/style/Autodetect.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/style/Autodetect.java @@ -538,7 +538,7 @@ private void countIndents(String space, boolean isContinuation, IndentStatistics } private static class ImportLayoutStatistics { - List> blocksPerSourceFile = new ArrayList<>(); + List> blocksPerSourceFile = new ArrayList<>(20); Map pkgToBlockPattern = new LinkedHashMap<>(); int staticAtTopCount = 0; int staticAtBotCount = 0; @@ -588,10 +588,11 @@ public ImportLayoutStyle getImportLayoutStyle() { int nonStaticPos = 0; int staticPos = 0; - List nonStaticBlocks = new ArrayList<>(); // Isolate static imports to add at top or bottom of layout. - List staticBlocks = new ArrayList<>(); // Isolate static imports to add at top or bottom of layout. - List countOfBlocksInNonStaticGroups = new ArrayList<>(); - List countOfBlocksInStaticGroups = new ArrayList<>(); + int blockCount = longestBlocks.size(); + List nonStaticBlocks = new ArrayList<>(blockCount); // Isolate static imports to add at top or bottom of layout. + List staticBlocks = new ArrayList<>(blockCount); // Isolate static imports to add at top or bottom of layout. + List countOfBlocksInNonStaticGroups = new ArrayList<>(blockCount); + List countOfBlocksInStaticGroups = new ArrayList<>(blockCount); for (Block block : longestBlocks) { if (BlockType.ImportStatic == block.type) { diff --git a/rewrite-java/src/main/java/org/openrewrite/java/style/ImportLayoutStyle.java b/rewrite-java/src/main/java/org/openrewrite/java/style/ImportLayoutStyle.java index 374d7d40449..d67e164de49 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/style/ImportLayoutStyle.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/style/ImportLayoutStyle.java @@ -43,6 +43,7 @@ import java.io.IOException; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; @@ -295,15 +296,19 @@ public List> addImport(List> origi JRightPadded finalToAdd = paddedToAdd; JRightPadded finalAfter = after; + // Cache atomic values to avoid repeated volatile reads in lambda + boolean shouldStarFold = starFold.get(); + int foldFrom = starFoldFrom.get(); + int foldTo = starFoldTo.get(); return ListUtils.flatMap(originalImports, (i, anImport) -> { - if (starFold.get() && i >= starFoldFrom.get() && i < starFoldTo.get()) { - return i == starFoldFrom.get() ? + if (shouldStarFold && i >= foldFrom && i < foldTo) { + return i == foldFrom ? finalToAdd /* only add the star import once */ : null; } else if (finalAfter != null && anImport.getElement().isScope(finalAfter.getElement())) { - if (starFold.get()) { + if (shouldStarFold) { // The added import is always folded, and is the first package occurrence in the imports. - if (starFoldFrom.get() == starFoldTo.get()) { + if (foldFrom == foldTo) { return Arrays.asList(finalToAdd, finalAfter); } else { return finalAfter; @@ -585,8 +590,9 @@ private void setJVMClassNames() { } private Map> mapNamesInPackageToPackages() { - Map> nameToPackages = new HashMap<>(); - Set checkPackageForClasses = new HashSet<>(); + int importCount = originalImports.size(); + Map> nameToPackages = new HashMap<>(importCount * 4 / 3 + 1); + Set checkPackageForClasses = new HashSet<>(importCount * 4 / 3 + 1); for (JRightPadded anImport : originalImports) { checkPackageForClasses.add(packageOrOuterClassName(anImport)); @@ -669,22 +675,44 @@ class ImportPackage implements Block { // VisibleForTesting final static Comparator> IMPORT_SORTING = (i1, i2) -> { - String[] import1 = i1.getElement().getQualid().printTrimmed().split("\\."); - String[] import2 = i2.getElement().getQualid().printTrimmed().split("\\."); + String s1 = i1.getElement().getQualid().printTrimmed(); + String s2 = i2.getElement().getQualid().printTrimmed(); + return compareImportStrings(s1, s2); + }; - for (int i = 0; i < Math.min(import1.length, import2.length); i++) { - int diff = import1[i].compareTo(import2[i]); - if (diff != 0) { - return diff; + static int compareImportStrings(String s1, String s2) { + int len1 = s1.length(); + int len2 = s2.length(); + int pos1 = 0, pos2 = 0; + while (pos1 < len1 && pos2 < len2) { + int dot1 = s1.indexOf('.', pos1); + int dot2 = s2.indexOf('.', pos2); + int end1 = dot1 == -1 ? len1 : dot1; + int end2 = dot2 == -1 ? len2 : dot2; + int segLen1 = end1 - pos1; + int segLen2 = end2 - pos2; + int segLen = Math.min(segLen1, segLen2); + for (int i = 0; i < segLen; i++) { + int diff = s1.charAt(pos1 + i) - s2.charAt(pos2 + i); + if (diff != 0) { + return diff; + } + } + if (segLen1 != segLen2) { + return segLen1 - segLen2; } + pos1 = end1 + 1; + pos2 = end2 + 1; } - - if (import1.length == import2.length) { + boolean has1 = pos1 < len1; + boolean has2 = pos2 < len2; + if (has1 == has2) { return 0; } + return has1 ? 1 : -1; + } - return import1.length > import2.length ? 1 : -1; - }; + private static final ConcurrentHashMap PATTERN_CACHE = new ConcurrentHashMap<>(); private final Boolean statik; @Getter @@ -692,9 +720,10 @@ class ImportPackage implements Block { public ImportPackage(Boolean statik, String packageWildcard, boolean withSubpackages) { this.statik = statik; - this.packageWildcard = Pattern.compile(packageWildcard + String regex = packageWildcard .replace(".", "\\.") - .replace("*", withSubpackages ? ".+" : "[^.]+")); + .replace("*", withSubpackages ? ".+" : "[^.]+"); + this.packageWildcard = PATTERN_CACHE.computeIfAbsent(regex, Pattern::compile); } public boolean isStatic() { @@ -711,22 +740,27 @@ public boolean accept(JRightPadded anImport) { public List> orderedImports(LayoutState layoutState, int classCountToUseStarImport, int nameCountToUseStarImport, ImportLayoutConflictDetection importLayoutConflictDetection, List packagesToFold) { List> imports = layoutState.getImports(this); - Map>> groupedImports = imports - .stream() - .sorted(IMPORT_SORTING) - .collect(groupingBy( - ImportLayoutStyle::packageOrOuterClassName, - LinkedHashMap::new, // Use an ordered map to preserve sorting - toList() - )); + // Sort a copy and group into a LinkedHashMap to preserve sorted order + List> sorted = new ArrayList<>(imports); + sorted.sort(IMPORT_SORTING); + + Map>> groupedImports = new LinkedHashMap<>(); + for (JRightPadded imp : sorted) { + groupedImports.computeIfAbsent(packageOrOuterClassName(imp), k -> new ArrayList<>()).add(imp); + } List> ordered = new ArrayList<>(imports.size()); for (List> importGroup : groupedImports.values()) { JRightPadded toStar = importGroup.get(0); int threshold = toStar.getElement().isStatic() ? nameCountToUseStarImport : classCountToUseStarImport; - boolean starImportExists = importGroup.stream() - .anyMatch(it -> "*".equals(it.getElement().getQualid().getSimpleName())); + boolean starImportExists = false; + for (JRightPadded it : importGroup) { + if ("*".equals(it.getElement().getQualid().getSimpleName())) { + starImportExists = true; + break; + } + } if (importLayoutConflictDetection.isPackageFoldable(packageOrOuterClassName(toStar)) && (isPackageAlwaysFolded(packagesToFold, toStar.getElement()) || importGroup.size() >= threshold || (starImportExists && importGroup.size() > 1))) { @@ -734,18 +768,28 @@ public List> orderedImports(LayoutState layoutState, int J.FieldAccess qualid = toStar.getElement().getQualid(); J.Identifier name = qualid.getName(); - Set typeNamesInThisGroup = importGroup.stream() - .map(im -> im.getElement().getClassName()) - .collect(toSet()); + Set typeNamesInThisGroup = new HashSet<>(importGroup.size()); + for (JRightPadded im : importGroup) { + typeNamesInThisGroup.add(im.getElement().getClassName()); + } - Optional oneOfTheTypesIsInAnotherGroupToo = groupedImports.values().stream() - .filter(group -> group != importGroup) - .flatMap(group -> group.stream() - .filter(im -> typeNamesInThisGroup.contains(im.getElement().getClassName()))) - .map(im -> im.getElement().getTypeName()) - .findAny(); + String conflictTypeName = null; + for (List> group : groupedImports.values()) { + if (group == importGroup) { + continue; + } + for (JRightPadded im : group) { + if (typeNamesInThisGroup.contains(im.getElement().getClassName())) { + conflictTypeName = im.getElement().getTypeName(); + break; + } + } + if (conflictTypeName != null) { + break; + } + } - if (starImportExists || !oneOfTheTypesIsInAnotherGroupToo.isPresent()) { + if (starImportExists || conflictTypeName == null) { ordered.add(toStar.withElement(toStar.getElement().withQualid(qualid.withName(name.withSimpleName("*"))))); continue; } diff --git a/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/KotlinParser.java b/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/KotlinParser.java index b3c32512617..f854e066604 100644 --- a/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/KotlinParser.java +++ b/rewrite-kotlin/src/main/java/org/openrewrite/kotlin/KotlinParser.java @@ -255,8 +255,7 @@ public JavaSourceSet getSourceSet(ExecutionContext ctx) { if (ctx.getMessage(SKIP_SOURCE_SET_TYPE_GENERATION, false)) { sourceSetProvenance = new JavaSourceSet(Tree.randomId(), sourceSet, emptyList(), emptyMap()); } else { - sourceSetProvenance = JavaSourceSet.build(sourceSet, classpath == null ? emptyList() : classpath, - typeCache, false); + sourceSetProvenance = JavaSourceSet.build(sourceSet, classpath == null ? emptyList() : classpath); } } return sourceSetProvenance;