From 4ed4d21c7c1de8e5a98f17a96f6bb8329ad0335d Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 26 Jul 2026 18:47:57 +0200 Subject: [PATCH 1/9] Reload recipe classes when a classpath entry changed The `RewriteClassLoader` was cached per Gradle daemon and only replaced when the set of classpath locations changed. Recipes built by the project itself keep the same location when rebuilt, so their previous classes kept being used until the daemon was stopped. Compare the size and last modified time of each classpath entry as well, and shut down JGit's per class loader work queue thread when a class loader is replaced, as that thread would otherwise retain it. --- .../gradle/DelegatingProjectParser.java | 85 ++++++++++++- .../gradle/DelegatingProjectParserTest.java | 116 ++++++++++++++++++ .../openrewrite/gradle/RewriteDiscoverTest.kt | 67 ++++++++++ 3 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java diff --git a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java index 1d5c6ea2e..aa24af2f8 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java @@ -19,24 +19,33 @@ import org.gradle.internal.service.ServiceRegistry; import org.jspecify.annotations.Nullable; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; +import java.util.stream.Stream; import static java.util.stream.Collectors.toList; public class DelegatingProjectParser implements GradleProjectParser { + private static final AtomicLong UNREADABLE_CLASSPATH_ENTRIES = new AtomicLong(); + @Nullable - protected static List rewriteClasspath; + protected static List rewriteClasspathFingerprint; @Nullable protected static RewriteClassLoader rewriteClassLoader; protected final GradleProjectParser gpp; @@ -61,15 +70,16 @@ public DelegatingProjectParser(Project project, RewriteExtension extension, Set< classpathUrls.add(currentJar); ClassLoader pluginClassLoader = getPluginClassLoader(project); + List classpathFingerprint = fingerprint(classpathUrls); if (rewriteClassLoader == null || - !classpathUrls.equals(rewriteClasspath) || + !classpathFingerprint.equals(rewriteClasspathFingerprint) || rewriteClassLoader.getPluginClassLoader() != pluginClassLoader) { if (rewriteClassLoader != null) { - rewriteClassLoader.close(); + discard(rewriteClassLoader); } rewriteClassLoader = new RewriteClassLoader(classpathUrls, pluginClassLoader); - rewriteClasspath = classpathUrls; + rewriteClasspathFingerprint = classpathFingerprint; } Class gppClass = Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, rewriteClassLoader); @@ -134,6 +144,73 @@ public void shutdownRewrite() { }); } + private static void discard(RewriteClassLoader classLoader) throws IOException { + shutdownJGitWorkQueue(classLoader); + classLoader.close(); + } + + /** + * JGit keeps a daemon thread around per class loader that initialized it, and that thread references the + * class loader that created it. Without shutting it down every replaced class loader, and all the recipe + * classes it loaded, would be retained in metaspace for as long as the Gradle daemon lives. + */ + private static void shutdownJGitWorkQueue(ClassLoader classLoader) { + try { + Class workQueue = Class.forName("org.openrewrite.jgit.lib.internal.WorkQueue", true, classLoader); + ((ExecutorService) workQueue.getMethod("getExecutor").invoke(null)).shutdownNow(); + } catch (ReflectiveOperationException | ClassCastException | LinkageError ignored) { + // Not all versions of rewrite bundle JGit, in which case there is no work queue to shut down + } + } + + /** + * Recipe jars built by the project itself are replaced in place, keeping the same location on the classpath. + * Comparing locations alone would then reuse a {@link RewriteClassLoader} holding the previous recipe classes + * for as long as the Gradle daemon lives, so compare the contents of each classpath entry as well. + */ + static List fingerprint(Collection classpath) { + return classpath.stream() + .map(DelegatingProjectParser::fingerprint) + .sorted() + .collect(toList()); + } + + private static String fingerprint(URL classpathEntry) { + if (!"file".equals(classpathEntry.getProtocol())) { + return classpathEntry + "|" + UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet(); + } + try { + return classpathEntry + "|" + fingerprint(Paths.get(classpathEntry.toURI())); + } catch (URISyntaxException e) { + return classpathEntry + "|" + UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet(); + } + } + + private static String fingerprint(Path classpathEntry) { + try { + BasicFileAttributes attributes = Files.readAttributes(classpathEntry, BasicFileAttributes.class); + if (!attributes.isDirectory()) { + return attributes.size() + "|" + attributes.lastModifiedTime().toMillis(); + } + try (Stream contents = Files.walk(classpathEntry)) { + return "directory|" + contents.filter(Files::isRegularFile) + .mapToLong(DelegatingProjectParser::fingerprintHash) + .sum(); + } + } catch (IOException e) { + return String.valueOf(UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet()); + } + } + + private static long fingerprintHash(Path file) { + try { + BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class); + return 31L * file.hashCode() + 31L * attributes.size() + attributes.lastModifiedTime().toMillis(); + } catch (IOException e) { + return UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet(); + } + } + protected URL jarContainingResource(String resourcePath) { try { if (resourcePath.startsWith("jar:")) { diff --git a/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java b/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java new file mode 100644 index 000000000..99dd48939 --- /dev/null +++ b/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2025 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.gradle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.openrewrite.Issue; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.util.Arrays; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.gradle.DelegatingProjectParser.fingerprint; + +@Issue("https://github.com/openrewrite/rewrite-gradle-plugin/issues/453") +class DelegatingProjectParserTest { + + @Test + void unchangedClasspathHasEqualFingerprints(@TempDir Path tempDir) throws IOException { + Path jar = write(tempDir.resolve("recipes.jar"), "first"); + + assertThat(fingerprint(urls(jar))).isEqualTo(fingerprint(urls(jar))); + } + + @Test + void orderDoesNotAffectFingerprint(@TempDir Path tempDir) throws IOException { + Path first = write(tempDir.resolve("first.jar"), "first"); + Path second = write(tempDir.resolve("second.jar"), "second"); + + assertThat(fingerprint(urls(first, second))).isEqualTo(fingerprint(urls(second, first))); + } + + @Test + void replacedJarChangesFingerprint(@TempDir Path tempDir) throws IOException { + Path jar = write(tempDir.resolve("recipes.jar"), "first"); + List before = fingerprint(urls(jar)); + + write(jar, "second"); + + assertThat(fingerprint(urls(jar))).isNotEqualTo(before); + } + + @Test + void rebuiltJarOfEqualSizeChangesFingerprint(@TempDir Path tempDir) throws IOException { + Path jar = write(tempDir.resolve("recipes.jar"), "first"); + List before = fingerprint(urls(jar)); + + touch(jar); + + assertThat(fingerprint(urls(jar))).isNotEqualTo(before); + } + + @Test + void changedFileWithinDirectoryChangesFingerprint(@TempDir Path tempDir) throws IOException { + Path classes = Files.createDirectories(tempDir.resolve("classes/org/example")); + write(classes.resolve("Recipe.class"), "first"); + List before = fingerprint(urls(tempDir.resolve("classes"))); + + touch(write(classes.resolve("Recipe.class"), "second")); + + assertThat(fingerprint(urls(tempDir.resolve("classes")))).isNotEqualTo(before); + } + + @Test + void fileAddedToDirectoryChangesFingerprint(@TempDir Path tempDir) throws IOException { + Path classes = Files.createDirectories(tempDir.resolve("classes/org/example")); + write(classes.resolve("Recipe.class"), "first"); + List before = fingerprint(urls(tempDir.resolve("classes"))); + + write(classes.resolve("OtherRecipe.class"), "first"); + + assertThat(fingerprint(urls(tempDir.resolve("classes")))).isNotEqualTo(before); + } + + private static Path write(Path file, String content) throws IOException { + return Files.write(file, content.getBytes(UTF_8)); + } + + private static Path touch(Path file) throws IOException { + FileTime lastModified = Files.getLastModifiedTime(file); + return Files.setLastModifiedTime(file, FileTime.fromMillis(lastModified.toMillis() + 1_000)); + } + + private static List urls(Path... paths) { + return Arrays.stream(paths) + .map(path -> { + try { + return path.toUri().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + }) + .collect(toList()); + } +} diff --git a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt index b128288ea..65717a602 100644 --- a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.DisabledIf import org.junit.jupiter.api.io.TempDir import org.openrewrite.Issue +import org.openrewrite.gradle.fixtures.GradleFixtures import java.io.File class RewriteDiscoverTest : RewritePluginTest { @@ -65,4 +66,70 @@ class RewriteDiscoverTest : RewritePluginTest { assertThat(result.output).contains("Configured with 2 active recipes and 1 active styles.") } + + @Issue("https://github.com/openrewrite/rewrite-gradle-plugin/issues/453") + @Test + fun `rewriteDiscover picks up recipes rebuilt within the same daemon`( + @TempDir projectDir: File + ) { + gradleProject(projectDir) { + buildGradle(""" + plugins { + id("java") + id("org.openrewrite.rewrite") + } + + ${GradleFixtures.REPOSITORIES} + + dependencies { + rewrite(project(":recipes")) + } + """) + + subproject("recipes") { + buildGradle(""" + plugins { + id("java") + } + + ${GradleFixtures.REPOSITORIES} + + dependencies { + implementation("org.openrewrite:rewrite-core:latest.release") + } + """) + + sourceSet("main") { + java(recipe("FirstRecipe")) + } + } + } + + assertThat(runGradle(projectDir, taskName()).output).contains("org.example.FirstRecipe") + + val recipeSources = File(projectDir, "recipes/src/main/java/org/example") + assertThat(File(recipeSources, "FirstRecipe.java").delete()).isTrue() + File(recipeSources, "SecondRecipe.java").writeText(recipe("SecondRecipe")) + + assertThat(runGradle(projectDir, taskName()).output).contains("org.example.SecondRecipe") + } + + //language=java + private fun recipe(className: String) = """ + package org.example; + + import org.openrewrite.Recipe; + + public class $className extends Recipe { + @Override + public String getDisplayName() { + return "$className"; + } + + @Override + public String getDescription() { + return "$className."; + } + } + """.trimIndent() } From 5174c835ba4f817589e42a6a8e7e93453fec8e6d Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Sun, 26 Jul 2026 20:49:16 +0200 Subject: [PATCH 2/9] Simplify classpath fingerprinting and move the JGit shutdown into the isolated parser --- .../gradle/DelegatingProjectParser.java | 96 +++++++++---------- .../gradle/isolated/DefaultProjectParser.java | 14 +++ .../gradle/DelegatingProjectParserTest.java | 45 ++++----- .../openrewrite/gradle/RewriteDiscoverTest.kt | 9 +- 4 files changed, 76 insertions(+), 88 deletions(-) diff --git a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java index aa24af2f8..04a7ebc2b 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java @@ -23,27 +23,25 @@ import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; +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.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; -import java.util.stream.Stream; +import static java.util.Collections.sort; import static java.util.stream.Collectors.toList; public class DelegatingProjectParser implements GradleProjectParser { - private static final AtomicLong UNREADABLE_CLASSPATH_ENTRIES = new AtomicLong(); - @Nullable protected static List rewriteClasspathFingerprint; @Nullable @@ -69,10 +67,14 @@ public DelegatingProjectParser(Project project, RewriteExtension extension, Set< .toString()); classpathUrls.add(currentJar); + List classpathEntries = new ArrayList<>(classpath); + classpathEntries.add(Paths.get(currentJar.toURI())); + ClassLoader pluginClassLoader = getPluginClassLoader(project); - List classpathFingerprint = fingerprint(classpathUrls); + List classpathFingerprint = fingerprint(classpathEntries); if (rewriteClassLoader == null || + classpathFingerprint == null || !classpathFingerprint.equals(rewriteClasspathFingerprint) || rewriteClassLoader.getPluginClassLoader() != pluginClassLoader) { if (rewriteClassLoader != null) { @@ -145,69 +147,57 @@ public void shutdownRewrite() { } private static void discard(RewriteClassLoader classLoader) throws IOException { - shutdownJGitWorkQueue(classLoader); - classLoader.close(); - } - - /** - * JGit keeps a daemon thread around per class loader that initialized it, and that thread references the - * class loader that created it. Without shutting it down every replaced class loader, and all the recipe - * classes it loaded, would be retained in metaspace for as long as the Gradle daemon lives. - */ - private static void shutdownJGitWorkQueue(ClassLoader classLoader) { try { - Class workQueue = Class.forName("org.openrewrite.jgit.lib.internal.WorkQueue", true, classLoader); - ((ExecutorService) workQueue.getMethod("getExecutor").invoke(null)).shutdownNow(); - } catch (ReflectiveOperationException | ClassCastException | LinkageError ignored) { + Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, classLoader) + .getMethod("shutdownJGitWorkQueue") + .invoke(null); + } catch (ReflectiveOperationException | LinkageError ignored) { // Not all versions of rewrite bundle JGit, in which case there is no work queue to shut down } + classLoader.close(); } /** * Recipe jars built by the project itself are replaced in place, keeping the same location on the classpath. * Comparing locations alone would then reuse a {@link RewriteClassLoader} holding the previous recipe classes * for as long as the Gradle daemon lives, so compare the contents of each classpath entry as well. + * + * @return a fingerprint per classpath entry, or {@code null} if any entry could not be read */ - static List fingerprint(Collection classpath) { - return classpath.stream() - .map(DelegatingProjectParser::fingerprint) - .sorted() - .collect(toList()); + static @Nullable List fingerprint(Collection classpath) { + List fingerprints = new ArrayList<>(classpath.size()); + for (Path classpathEntry : classpath) { + try { + fingerprints.add(fingerprint(classpathEntry)); + } catch (IOException e) { + return null; + } + } + sort(fingerprints); + return fingerprints; } - private static String fingerprint(URL classpathEntry) { - if (!"file".equals(classpathEntry.getProtocol())) { - return classpathEntry + "|" + UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet(); - } - try { - return classpathEntry + "|" + fingerprint(Paths.get(classpathEntry.toURI())); - } catch (URISyntaxException e) { - return classpathEntry + "|" + UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet(); + private static String fingerprint(Path classpathEntry) throws IOException { + BasicFileAttributes attributes = Files.readAttributes(classpathEntry, BasicFileAttributes.class); + if (!attributes.isDirectory()) { + return classpathEntry + "|" + stamp(classpathEntry, attributes); } + DirectoryStamp directoryStamp = new DirectoryStamp(); + Files.walkFileTree(classpathEntry, directoryStamp); + return classpathEntry + "|" + directoryStamp.stamp; } - private static String fingerprint(Path classpathEntry) { - try { - BasicFileAttributes attributes = Files.readAttributes(classpathEntry, BasicFileAttributes.class); - if (!attributes.isDirectory()) { - return attributes.size() + "|" + attributes.lastModifiedTime().toMillis(); - } - try (Stream contents = Files.walk(classpathEntry)) { - return "directory|" + contents.filter(Files::isRegularFile) - .mapToLong(DelegatingProjectParser::fingerprintHash) - .sum(); - } - } catch (IOException e) { - return String.valueOf(UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet()); - } + private static long stamp(Path file, BasicFileAttributes attributes) { + return 31L * (31L * file.hashCode() + attributes.size()) + attributes.lastModifiedTime().toMillis(); } - private static long fingerprintHash(Path file) { - try { - BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class); - return 31L * file.hashCode() + 31L * attributes.size() + attributes.lastModifiedTime().toMillis(); - } catch (IOException e) { - return UNREADABLE_CLASSPATH_ENTRIES.incrementAndGet(); + private static class DirectoryStamp extends SimpleFileVisitor { + private long stamp; + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) { + stamp += stamp(file, attributes); + return FileVisitResult.CONTINUE; } } diff --git a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java index dfba291f6..dca2065a2 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java @@ -68,6 +68,7 @@ import org.openrewrite.jgit.dircache.DirCache; import org.openrewrite.jgit.lib.ObjectId; import org.openrewrite.jgit.lib.Repository; +import org.openrewrite.jgit.lib.internal.WorkQueue; import org.openrewrite.jgit.revwalk.RevCommit; import org.openrewrite.jgit.revwalk.RevWalk; import org.openrewrite.jgit.treewalk.TreeWalk; @@ -1501,6 +1502,19 @@ public void shutdownRewrite() { } } + /** + * JGit keeps a daemon thread around per class loader that initialized it, and that thread references the + * class loader that created it. Without shutting it down every replaced class loader, and all the recipe + * classes it loaded, would be retained in metaspace for as long as the Gradle daemon lives. + *

+ * Deliberately not part of {@link #shutdownRewrite()}, as the executor is created once per class loader and + * never recreated; it may only be shut down when the class loader itself is discarded. + */ + @SuppressWarnings("unused") // Called reflectively by DelegatingProjectParser when it discards a class loader + public static void shutdownJGitWorkQueue() { + WorkQueue.getExecutor().shutdownNow(); + } + private static synchronized MavenPomCache getPomCache(@Nullable String pomCacheDirectory) { if (pomCache == null) { pomCache = new MavenPomCacheBuilder(logger).build(pomCacheDirectory); diff --git a/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java b/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java index 99dd48939..213c1f01a 100644 --- a/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java +++ b/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java @@ -20,16 +20,14 @@ import org.openrewrite.Issue; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; -import java.util.Arrays; import java.util.List; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.stream.Collectors.toList; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.gradle.DelegatingProjectParser.fingerprint; @@ -40,7 +38,7 @@ class DelegatingProjectParserTest { void unchangedClasspathHasEqualFingerprints(@TempDir Path tempDir) throws IOException { Path jar = write(tempDir.resolve("recipes.jar"), "first"); - assertThat(fingerprint(urls(jar))).isEqualTo(fingerprint(urls(jar))); + assertThat(fingerprint(singletonList(jar))).isEqualTo(fingerprint(singletonList(jar))); } @Test @@ -48,49 +46,54 @@ void orderDoesNotAffectFingerprint(@TempDir Path tempDir) throws IOException { Path first = write(tempDir.resolve("first.jar"), "first"); Path second = write(tempDir.resolve("second.jar"), "second"); - assertThat(fingerprint(urls(first, second))).isEqualTo(fingerprint(urls(second, first))); + assertThat(fingerprint(asList(first, second))).isEqualTo(fingerprint(asList(second, first))); + } + + @Test + void missingClasspathEntryHasNoFingerprint(@TempDir Path tempDir) { + assertThat(fingerprint(singletonList(tempDir.resolve("missing.jar")))).isNull(); } @Test void replacedJarChangesFingerprint(@TempDir Path tempDir) throws IOException { Path jar = write(tempDir.resolve("recipes.jar"), "first"); - List before = fingerprint(urls(jar)); + List before = fingerprint(singletonList(jar)); write(jar, "second"); - assertThat(fingerprint(urls(jar))).isNotEqualTo(before); + assertThat(fingerprint(singletonList(jar))).isNotEqualTo(before); } @Test void rebuiltJarOfEqualSizeChangesFingerprint(@TempDir Path tempDir) throws IOException { Path jar = write(tempDir.resolve("recipes.jar"), "first"); - List before = fingerprint(urls(jar)); + List before = fingerprint(singletonList(jar)); touch(jar); - assertThat(fingerprint(urls(jar))).isNotEqualTo(before); + assertThat(fingerprint(singletonList(jar))).isNotEqualTo(before); } @Test void changedFileWithinDirectoryChangesFingerprint(@TempDir Path tempDir) throws IOException { Path classes = Files.createDirectories(tempDir.resolve("classes/org/example")); write(classes.resolve("Recipe.class"), "first"); - List before = fingerprint(urls(tempDir.resolve("classes"))); + List before = fingerprint(singletonList(tempDir.resolve("classes"))); - touch(write(classes.resolve("Recipe.class"), "second")); + touch(classes.resolve("Recipe.class")); - assertThat(fingerprint(urls(tempDir.resolve("classes")))).isNotEqualTo(before); + assertThat(fingerprint(singletonList(tempDir.resolve("classes")))).isNotEqualTo(before); } @Test void fileAddedToDirectoryChangesFingerprint(@TempDir Path tempDir) throws IOException { Path classes = Files.createDirectories(tempDir.resolve("classes/org/example")); write(classes.resolve("Recipe.class"), "first"); - List before = fingerprint(urls(tempDir.resolve("classes"))); + List before = fingerprint(singletonList(tempDir.resolve("classes"))); write(classes.resolve("OtherRecipe.class"), "first"); - assertThat(fingerprint(urls(tempDir.resolve("classes")))).isNotEqualTo(before); + assertThat(fingerprint(singletonList(tempDir.resolve("classes")))).isNotEqualTo(before); } private static Path write(Path file, String content) throws IOException { @@ -101,16 +104,4 @@ private static Path touch(Path file) throws IOException { FileTime lastModified = Files.getLastModifiedTime(file); return Files.setLastModifiedTime(file, FileTime.fromMillis(lastModified.toMillis() + 1_000)); } - - private static List urls(Path... paths) { - return Arrays.stream(paths) - .map(path -> { - try { - return path.toUri().toURL(); - } catch (MalformedURLException e) { - throw new IllegalStateException(e); - } - }) - .collect(toList()); - } } diff --git a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt index 65717a602..f6461ece1 100644 --- a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt @@ -73,14 +73,7 @@ class RewriteDiscoverTest : RewritePluginTest { @TempDir projectDir: File ) { gradleProject(projectDir) { - buildGradle(""" - plugins { - id("java") - id("org.openrewrite.rewrite") - } - - ${GradleFixtures.REPOSITORIES} - + buildGradle(GradleFixtures.REWRITE_BUILD_GRADLE + """ dependencies { rewrite(project(":recipes")) } From d2b0a2a32c751701696c68ba990a7be11920a384 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:44:13 +0200 Subject: [PATCH 3/9] Separate known rewrite dependencies from recipes and their dependencies To greatly reduce reloading classes during development by reusing most of rewrite's classes --- .../gradle/AbstractRewriteTask.java | 20 +- .../gradle/DelegatingProjectParser.java | 98 ++++++---- .../org/openrewrite/gradle/RewritePlugin.java | 181 +++++++++++++++--- 3 files changed, 221 insertions(+), 78 deletions(-) diff --git a/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java b/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java index aff5d4087..25cf65261 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java +++ b/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java @@ -24,19 +24,14 @@ import org.gradle.util.GradleVersion; import org.gradle.work.DisableCachingByDefault; import org.jspecify.annotations.Nullable; +import org.openrewrite.gradle.RewritePlugin.ResolvedDependencies; import javax.inject.Inject; -import java.io.File; -import java.nio.file.Path; import java.util.List; -import java.util.Set; - -import static java.util.Collections.emptySet; -import static java.util.stream.Collectors.toSet; @DisableCachingByDefault(because = "Rewrite tasks act on source files in place and are not safe to cache") public abstract class AbstractRewriteTask extends DefaultTask { - protected @Nullable Provider> resolvedDependencies; + protected @Nullable Provider resolvedDependencies; protected boolean dumpGcActivity; protected @Nullable GradleProjectParser gpp; protected @Nullable RewriteExtension extension; @@ -53,7 +48,7 @@ public T setExtension(RewriteExtension extension return (T) this; } - public T setResolvedDependencies(Provider> resolvedDependencies) { + public T setResolvedDependencies(Provider resolvedDependencies) { this.resolvedDependencies = resolvedDependencies; //noinspection unchecked return (T) this; @@ -83,14 +78,11 @@ protected T getProjectParser() { if (resolvedDependencies == null) { throw new IllegalArgumentException("Must configure resolvedDependencies"); } - Set deps = resolvedDependencies.getOrNull(); + ResolvedDependencies deps = resolvedDependencies.getOrNull(); if (deps == null) { - deps = emptySet(); + deps = ResolvedDependencies.EMPTY; } - Set classpath = deps.stream() - .map(File::toPath) - .collect(toSet()); - gpp = new DelegatingProjectParser(getProject(), extension, classpath); + gpp = new DelegatingProjectParser(getProject(), extension, deps); } //noinspection unchecked return (T) gpp; diff --git a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java index 04a7ebc2b..a53541062 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java @@ -18,23 +18,16 @@ import org.gradle.api.Project; import org.gradle.internal.service.ServiceRegistry; import org.jspecify.annotations.Nullable; +import org.openrewrite.gradle.RewritePlugin.ResolvedDependencies; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -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.net.URLClassLoader; +import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.Callable; import java.util.function.Consumer; @@ -45,46 +38,46 @@ public class DelegatingProjectParser implements GradleProjectParser { @Nullable protected static List rewriteClasspathFingerprint; @Nullable + protected static List recipeClasspathFingerprint; + @Nullable protected static RewriteClassLoader rewriteClassLoader; + @Nullable + protected static URLClassLoader recipeClassLoader; protected final GradleProjectParser gpp; - public DelegatingProjectParser(Project project, RewriteExtension extension, Set classpath) { + public DelegatingProjectParser(Project project, RewriteExtension extension, ResolvedDependencies classpath) { try { - List classpathUrls = classpath.stream() - .map(Path::toUri) - .map(uri -> { - try { - return uri.toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - }) + List rewriteClasspathUrls = classpath.getFromRewriteOnly().stream() + .map(RewritePlugin.ProjectDependency::getUrl) + .collect(toList()); + List recipeClasspathUrls = classpath.getFromRecipeOnly().stream() + .map(RewritePlugin.ProjectDependency::getUrl) .collect(toList()); @SuppressWarnings("ConstantConditions") URL currentJar = jarContainingResource(getClass() .getResource("/org/openrewrite/gradle/isolated/DefaultProjectParser.class") .toString()); - classpathUrls.add(currentJar); + rewriteClasspathUrls.add(currentJar); - List classpathEntries = new ArrayList<>(classpath); - classpathEntries.add(Paths.get(currentJar.toURI())); + List rewriteClasspathEntries = classpath.getFromRewriteOnly().stream().map(RewritePlugin.ProjectDependency::getPath).collect(toList()); + List recipeClasspathEntries = classpath.getFromRecipeOnly().stream().map(RewritePlugin.ProjectDependency::getPath).collect(toList()); + rewriteClasspathEntries.add(Paths.get(currentJar.toURI())); ClassLoader pluginClassLoader = getPluginClassLoader(project); - List classpathFingerprint = fingerprint(classpathEntries); - - if (rewriteClassLoader == null || - classpathFingerprint == null || - !classpathFingerprint.equals(rewriteClasspathFingerprint) || - rewriteClassLoader.getPluginClassLoader() != pluginClassLoader) { - if (rewriteClassLoader != null) { - discard(rewriteClassLoader); - } - rewriteClassLoader = new RewriteClassLoader(classpathUrls, pluginClassLoader); - rewriteClasspathFingerprint = classpathFingerprint; + List newRewriteClasspathFingerprint = fingerprint(rewriteClasspathEntries); + List newRecipeClasspathFingerprint = fingerprint(recipeClasspathEntries); + + // Throw recipe CL if rewrite CL is reset + // because recipe classes depend on rewrite's dependencies + if (hasRewriteClasspathChanged(newRewriteClasspathFingerprint, pluginClassLoader)) { + recreateRewriteClassLoader(rewriteClasspathUrls, newRewriteClasspathFingerprint, pluginClassLoader); + recreateRecipeClassLoader(recipeClasspathUrls, newRecipeClasspathFingerprint); + } else if (hasRecipeClasspathChanged(newRecipeClasspathFingerprint)) { + recreateRecipeClassLoader(recipeClasspathUrls, newRecipeClasspathFingerprint); } - Class gppClass = Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, rewriteClassLoader); + Class gppClass = Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, recipeClassLoader); assert (gppClass.getClassLoader() == rewriteClassLoader) : "DefaultProjectParser must be loaded from RewriteClassLoader to be sufficiently isolated from Gradle's classpath"; gpp = (GradleProjectParser) gppClass.getDeclaredConstructor(Project.class, RewriteExtension.class) .newInstance(project, extension); @@ -94,6 +87,37 @@ public DelegatingProjectParser(Project project, RewriteExtension extension, Set< } } + private static boolean hasRewriteClasspathChanged(@Nullable List newRewriteClasspathFingerprint, ClassLoader pluginClassLoader) { + return rewriteClassLoader == null || + rewriteClasspathFingerprint == null || + !rewriteClasspathFingerprint.equals(newRewriteClasspathFingerprint) || + rewriteClassLoader.getPluginClassLoader() != pluginClassLoader; + } + + private static boolean hasRecipeClasspathChanged(@Nullable List newRecipeClasspathFingerprint) { + return recipeClassLoader == null || + recipeClasspathFingerprint == null || + !recipeClasspathFingerprint.equals(newRecipeClasspathFingerprint); + } + + private static void recreateRewriteClassLoader(List rewriteClasspathUrls, @Nullable List newRewriteClasspathFingerprint, ClassLoader pluginClassLoader) throws IOException { + if (rewriteClassLoader != null) { + discard(rewriteClassLoader); + } + + rewriteClassLoader = new RewriteClassLoader(rewriteClasspathUrls, pluginClassLoader); + rewriteClasspathFingerprint = newRewriteClasspathFingerprint; + } + + private static void recreateRecipeClassLoader(List recipeClasspathUrls, @Nullable List newRecipeClasspathFingerprint) throws IOException { + if (recipeClassLoader != null) { + discard(recipeClassLoader); + } + + recipeClassLoader = new URLClassLoader(recipeClasspathUrls.toArray(new URL[0]), Objects.requireNonNull(rewriteClassLoader, "Rewrite CL is missing")); + recipeClasspathFingerprint = newRecipeClasspathFingerprint; + } + @Override public List getActiveRecipes() { return unwrapInvocationException(gpp::getActiveRecipes); @@ -146,7 +170,7 @@ public void shutdownRewrite() { }); } - private static void discard(RewriteClassLoader classLoader) throws IOException { + private static void discard(URLClassLoader classLoader) throws IOException { try { Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, classLoader) .getMethod("shutdownJGitWorkQueue") diff --git a/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java b/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java index 484003b26..c0ebbd75e 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java +++ b/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java @@ -20,7 +20,12 @@ import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleIdentifier; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.gradle.api.attributes.*; import org.gradle.api.attributes.java.TargetJvmEnvironment; import org.gradle.api.model.ObjectFactory; @@ -32,12 +37,18 @@ import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskProvider; +import org.jetbrains.annotations.TestOnly; import org.jspecify.annotations.Nullable; import java.io.File; import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; +import static java.util.Collections.emptyList; import static org.gradle.api.attributes.Bundling.BUNDLING_ATTRIBUTE; import static org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE; @@ -52,7 +63,7 @@ public class RewritePlugin implements Plugin { @Nullable - private Set resolvedDependencies; + private ResolvedDependencies resolvedDependencies; @Override public void apply(Project project) { @@ -70,7 +81,7 @@ public void apply(Project project) { Configuration rewriteConf = project.getConfigurations().maybeCreate("rewrite"); rewriteConf.setCanBeConsumed(false); - Provider> resolvedDependenciesProvider = project.provider(() -> getResolvedDependencies(project, extension, rewriteConf)); + Provider resolvedDependenciesProvider = project.provider(() -> getResolvedDependencies(project, extension, rewriteConf)); TaskProvider rewriteRun = project.getTasks().register("rewriteRun", RewriteRunTask.class, task -> { task.setExtension(extension); @@ -164,40 +175,62 @@ private static void configureProject(Project project, RewriteExtension extension }); } - private Set getResolvedDependencies(Project project, RewriteExtension extension, Configuration rewriteConf) { + public ResolvedDependencies getResolvedDependencies(Project project, RewriteExtension extension, Configuration rewriteConf) { if (resolvedDependencies == null) { // Avoid Stream.concat here pending https://github.com/gradle/gradle/issues/33152 List dependencies = new ArrayList<>(); dependencies.addAll(knownRewriteDependencies(extension, project.getDependencies())); dependencies.addAll(rewriteConf.getDependencies()); - // By using a detached configuration, we separate this dependency resolution from the rest of the project's - // configuration. This also means that Gradle has no criteria with which to select between variants of - // dependencies which expose differing capabilities. So those must be manually configured - Configuration detachedConf = project.getConfigurations().detachedConfiguration(dependencies.toArray(new Dependency[0])); - - try { - ObjectFactory objectFactory = project.getObjects(); - detachedConf.attributes(attributes -> { - // Adapted from org.gradle.api.plugins.jvm.internal.DefaultJvmEcosystemAttributesDetails - attributes.attribute(Category.CATEGORY_ATTRIBUTE, objectFactory.named(Category.class, Category.LIBRARY)); - attributes.attribute(Usage.USAGE_ATTRIBUTE, objectFactory.named(Usage.class, Usage.JAVA_RUNTIME)); - attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objectFactory.named(LibraryElements.class, LibraryElements.JAR)); - attributes.attribute(BUNDLING_ATTRIBUTE, objectFactory.named(Bundling.class, Bundling.EXTERNAL)); + + List rewriteDependencies = resolveConfiguration(project, knownRewriteDependencies(extension, project.getDependencies())); + List recipeDependencies = resolveConfiguration(project, rewriteConf.getDependencies()); + List effectiveDependencies = resolveConfiguration(project, dependencies); + + resolvedDependencies = new ResolvedDependencies(rewriteDependencies, recipeDependencies, effectiveDependencies); + } + return resolvedDependencies; + } + + private static List resolveConfiguration(Project project, Collection dependencies) { + // By using a detached configuration, we separate this dependency resolution from the rest of the project's + // configuration. This also means that Gradle has no criteria with which to select between variants of + // dependencies which expose differing capabilities. So those must be manually configured + Configuration detachedConf = project.getConfigurations().detachedConfiguration(dependencies.toArray(new Dependency[0])); + configureAttributes(project, detachedConf); + + return detachedConf.getIncoming() + .getArtifacts().getArtifacts() + .stream() + .map(artifactResult -> { try { - attributes.attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objectFactory.named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM)); - } catch (NoClassDefFoundError e) { - // Old versions of Gradle don't have the class TargetJvmEnvironment and that's OK, we can always - // try this attribute instead - attributes.attribute(Attribute.of("org.gradle.jvm.environment", String.class), "standard-jvm"); + return new ProjectDependency(artifactResult); + } catch (MalformedURLException e) { + throw new RuntimeException(e); } - }); - } catch (NoClassDefFoundError e) { - // Old versions of Gradle don't have all of these attributes and that's OK - } + }) + .collect(Collectors.toList()); + } - resolvedDependencies = detachedConf.resolve(); + private static void configureAttributes(Project project, Configuration configuration) { + try { + ObjectFactory objectFactory = project.getObjects(); + configuration.attributes(attributes -> { + // Adapted from org.gradle.api.plugins.jvm.internal.DefaultJvmEcosystemAttributesDetails + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objectFactory.named(Category.class, Category.LIBRARY)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objectFactory.named(Usage.class, Usage.JAVA_RUNTIME)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objectFactory.named(LibraryElements.class, LibraryElements.JAR)); + attributes.attribute(BUNDLING_ATTRIBUTE, objectFactory.named(Bundling.class, Bundling.EXTERNAL)); + try { + attributes.attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objectFactory.named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM)); + } catch (NoClassDefFoundError e) { + // Old versions of Gradle don't have the class TargetJvmEnvironment and that's OK, we can always + // try this attribute instead + attributes.attribute(Attribute.of("org.gradle.jvm.environment", String.class), "standard-jvm"); + } + }); + } catch (NoClassDefFoundError e) { + // Old versions of Gradle don't have all of these attributes and that's OK } - return resolvedDependencies; } private static Collection knownRewriteDependencies(RewriteExtension extension, DependencyHandler deps) { @@ -229,4 +262,98 @@ private static Collection knownRewriteDependencies(RewriteExtension deps.create("org.rocksdb:rocksdbjni:" + extension.getRocksdbJniVersion()) ); } + + public static class ResolvedDependencies { + static final ResolvedDependencies EMPTY = new ResolvedDependencies(emptyList(), emptyList(), emptyList()); + + private final List rewriteClasspath; + private final List recipeClasspath; + private final List effectiveClasspath; + + private ResolvedDependencies(List rewriteClasspath, List recipeClasspath, List effectiveClasspath) { + this.rewriteClasspath = rewriteClasspath; + this.recipeClasspath = recipeClasspath; + this.effectiveClasspath = effectiveClasspath; + } + + @TestOnly // Used by RewritePluginTest + @SuppressWarnings("unused") + public List getEffectiveClasspath() { + return effectiveClasspath; + } + + /** Returns dependencies only including OpenRewrite's own required dependencies */ + public List getFromRewriteOnly() { + // Using the effective dependency versions, take the ones that only appear from the (known) rewrite classpath + return effectiveClasspath.stream() + .filter(effectiveDependency -> { + // Keep if from known rewrite deps + ComponentIdentifier expectedIdentifier = effectiveDependency.getIdentifier(); + return rewriteClasspath.stream() + .anyMatch(rewriteDependency -> hasSameDependencyIdentifier(expectedIdentifier, rewriteDependency.getIdentifier())); + }) + .collect(Collectors.toList()); + } + + /** Returns dependencies that are only present in the recipe's classpath, i.e., excluding rewrite classpath */ + public List getFromRecipeOnly() { + // Take recipe classpath, remove rewrite modules + return recipeClasspath.stream() + .filter(recipeDependency -> { + ComponentIdentifier expectedIdentifier = recipeDependency.getIdentifier(); + + // Keep when identifier (independent of version) can't be found in rewrite classpath + return rewriteClasspath.stream().noneMatch(dep -> hasSameDependencyIdentifier(expectedIdentifier, dep.getIdentifier())); + }) + .collect(Collectors.toList()); + } + + private static boolean hasSameDependencyIdentifier(ComponentIdentifier expectedIdentifier, ComponentIdentifier depIdentifier) { + if (!(expectedIdentifier instanceof ModuleComponentIdentifier) && !(expectedIdentifier instanceof ProjectComponentIdentifier)) { + throw new UnsupportedOperationException("Unsupported component identifier type: " + expectedIdentifier.getClass().getName()); + } + + if (expectedIdentifier instanceof ModuleComponentIdentifier) { + if (!(depIdentifier instanceof ModuleComponentIdentifier)) { + return false; + } + + ModuleIdentifier expectedModuleIdentifier = ((ModuleComponentIdentifier) expectedIdentifier).getModuleIdentifier(); + ModuleIdentifier depModuleIdentifier = ((ModuleComponentIdentifier) depIdentifier).getModuleIdentifier(); + return expectedModuleIdentifier.equals(depModuleIdentifier); + } else if (expectedIdentifier instanceof ProjectComponentIdentifier) { + if (!(depIdentifier instanceof ProjectComponentIdentifier)) { + return false; + } + + return depIdentifier.equals(expectedIdentifier); + } else { + throw new UnsupportedOperationException("Unsupported component identifier type: " + depIdentifier.getClass().getName()); + } + } + } + + public static class ProjectDependency { + private final Path path; + private final URL url; + private final ComponentIdentifier identifier; + + private ProjectDependency(ResolvedArtifactResult artifactResult) throws MalformedURLException { + this.path = artifactResult.getFile().toPath(); + this.url = path.toUri().toURL(); + this.identifier = artifactResult.getVariant().getOwner(); + } + + public Path getPath() { + return path; + } + + public URL getUrl() { + return url; + } + + public ComponentIdentifier getIdentifier() { + return identifier; + } + } } From 33f2c5ffdb8a43c70c593d15a38a01493552568a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:44:52 +0200 Subject: [PATCH 4/9] Test `rewrite + recipe classpath = effective classpath` To make sure the merged classpath is the same as before --- .../openrewrite/gradle/IRewritePluginTest.kt | 59 +++++++++++++ .../openrewrite/gradle/RewriteDiscoverTest.kt | 3 +- .../openrewrite/gradle/RewriteDryRunTest.kt | 2 +- .../openrewrite/gradle/RewritePluginTest.kt | 84 ++++++++++++++----- .../org/openrewrite/gradle/RewriteRunTest.kt | 2 +- 5 files changed, 124 insertions(+), 26 deletions(-) create mode 100644 plugin/src/test/kotlin/org/openrewrite/gradle/IRewritePluginTest.kt diff --git a/plugin/src/test/kotlin/org/openrewrite/gradle/IRewritePluginTest.kt b/plugin/src/test/kotlin/org/openrewrite/gradle/IRewritePluginTest.kt new file mode 100644 index 000000000..0d34c18a4 --- /dev/null +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/IRewritePluginTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2025 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.gradle + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.openrewrite.Issue +import org.openrewrite.gradle.condition.EnabledForGradleRange +import java.io.File + +interface IRewritePluginTest: GradleRunnerTest { + + fun taskName(): String + + // The configuration cache works on Gradle 6.6+, but rewrite-gradle-plugin uses notCompatibleWithConfigurationCache, + // which is only available on Gradle 7.4+. + @EnabledForGradleRange(min = "7.4") + @Issue("https://github.com/openrewrite/rewrite-gradle-plugin/issues/227") + @Test + fun `task is compatible with the configuration cache`( + @TempDir projectDir: File + ) { + gradleProject(projectDir) { + buildGradle( + """ + plugins { + id("org.openrewrite.rewrite") + } + + repositories { + mavenLocal() + mavenCentral() + maven { + url = uri("https://central.sonatype.com/repository/maven-snapshots") + } + } + """ + ) + } + val result = runGradle(projectDir, taskName(), "--configuration-cache") + val taskResult = result.task(":${taskName()}")!! + assertThat(taskResult.outcome).isEqualTo(TaskOutcome.SUCCESS) + } +} diff --git a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt index f6461ece1..4d4a2ae4a 100644 --- a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt @@ -18,13 +18,12 @@ package org.openrewrite.gradle import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test -import org.junit.jupiter.api.condition.DisabledIf import org.junit.jupiter.api.io.TempDir import org.openrewrite.Issue import org.openrewrite.gradle.fixtures.GradleFixtures import java.io.File -class RewriteDiscoverTest : RewritePluginTest { +class RewriteDiscoverTest : IRewritePluginTest { override fun taskName(): String = "rewriteDiscover" diff --git a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDryRunTest.kt b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDryRunTest.kt index 9fa9c0c25..0de705e89 100644 --- a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDryRunTest.kt +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDryRunTest.kt @@ -27,7 +27,7 @@ import org.openrewrite.gradle.condition.EnabledForGradleRange import java.io.File @Suppress("GroovyUnusedAssignment") -class RewriteDryRunTest : RewritePluginTest { +class RewriteDryRunTest : IRewritePluginTest { @TempDir lateinit var projectDir: File diff --git a/plugin/src/test/kotlin/org/openrewrite/gradle/RewritePluginTest.kt b/plugin/src/test/kotlin/org/openrewrite/gradle/RewritePluginTest.kt index 22cd7dbc2..65909f12d 100644 --- a/plugin/src/test/kotlin/org/openrewrite/gradle/RewritePluginTest.kt +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/RewritePluginTest.kt @@ -19,41 +19,81 @@ import org.assertj.core.api.Assertions.assertThat import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -import org.openrewrite.Issue -import org.openrewrite.gradle.condition.EnabledForGradleRange +import org.openrewrite.gradle.fixtures.GradleFixtures import java.io.File -interface RewritePluginTest: GradleRunnerTest { +class RewritePluginTest : GradleRunnerTest { - fun taskName(): String - - // The configuration cache works on Gradle 6.6+, but rewrite-gradle-plugin uses notCompatibleWithConfigurationCache, - // which is only available on Gradle 7.4+. - @EnabledForGradleRange(min = "7.4") - @Issue("https://github.com/openrewrite/rewrite-gradle-plugin/issues/227") @Test - fun `task is compatible with the configuration cache`( - @TempDir projectDir: File + fun `effective classpath is equal to rewrite plus recipe classpath`( + @TempDir projectDir: File, ) { gradleProject(projectDir) { buildGradle( - """ - plugins { - id("org.openrewrite.rewrite") + GradleFixtures.REWRITE_BUILD_GRADLE + """ + + dependencies { + rewrite(project(":recipes")) } - repositories { - mavenLocal() - mavenCentral() - maven { - url = uri("https://central.sonatype.com/repository/maven-snapshots") - } + def plugin = plugins.getPlugin("org.openrewrite.rewrite") + def extension = project.extensions["rewrite"] + def configuration = project.configurations["rewrite"] + + def deps = plugin.getResolvedDependencies(project, extension, configuration) + def effective = new HashSet<>(deps.getEffectiveClasspath().collect { it.getPath() }) + def concatenated = new HashSet<>() + concatenated.addAll(deps.getFromRewriteOnly().collect { it.getPath() }) + concatenated.addAll(deps.getFromRecipeOnly().collect { it.getPath() }) + + if (effective != concatenated) { + throw new AssertionError("Effective classpath isn't rewrite + recipe") } """ ) + + subproject("recipes") { + buildGradle(""" + plugins { + id("java") + } + + ${GradleFixtures.REPOSITORIES} + + dependencies { + implementation("org.openrewrite:rewrite-core:latest.release") + } + """) + + sourceSet("main") { + java(recipe("FirstRecipe")) + } + } } - val result = runGradle(projectDir, taskName(), "--configuration-cache") - val taskResult = result.task(":${taskName()}")!! + val buildResult = runGradle(projectDir, "rewriteDiscover") + // Make sure our recipe was detected + assertThat(buildResult.output).contains("org.example.FirstRecipe") + + val taskResult = buildResult.task(":rewriteDiscover")!! assertThat(taskResult.outcome).isEqualTo(TaskOutcome.SUCCESS) } + + //language=java + private fun recipe(className: String) = """ + package org.example; + + import org.openrewrite.Recipe; + + public class $className extends Recipe { + @Override + public String getDisplayName() { + return "$className"; + } + + @Override + public String getDescription() { + return "$className."; + } + } + """.trimIndent() } diff --git a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteRunTest.kt b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteRunTest.kt index 8efce636a..ed1468d37 100644 --- a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteRunTest.kt +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteRunTest.kt @@ -33,7 +33,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Path @Suppress("GroovyUnusedAssignment") -class RewriteRunTest : RewritePluginTest { +class RewriteRunTest : IRewritePluginTest { override fun taskName(): String = "rewriteRun" From 13d57c9359a42f67cc69853bb9eacd9fc38c983b Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:20:53 +0200 Subject: [PATCH 5/9] Pass composite `ClassLoader` to `Environment` So recipes from both the RewriteClassLoader and the recipe's CL can be discovered --- .../gradle/CompositeURLClassLoader.java | 53 +++++++++++++++++++ .../gradle/DelegatingProjectParser.java | 4 +- .../gradle/isolated/DefaultProjectParser.java | 8 +-- 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100755 plugin/src/main/java/org/openrewrite/gradle/CompositeURLClassLoader.java diff --git a/plugin/src/main/java/org/openrewrite/gradle/CompositeURLClassLoader.java b/plugin/src/main/java/org/openrewrite/gradle/CompositeURLClassLoader.java new file mode 100755 index 000000000..62f4516a9 --- /dev/null +++ b/plugin/src/main/java/org/openrewrite/gradle/CompositeURLClassLoader.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025 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.gradle; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +/** + * Class loader passed to {@code rewrite-core}'s {@link org.openrewrite.config.Environment Environment} + * so it can discover recipes from multiple class loaders. + * In our case, they are from {@code RewritePlugin.knownRewriteDependencies} and recipes in the {@code rewrite} configuration. + */ +public class CompositeURLClassLoader extends URLClassLoader { + + private final Collection loaders; + + private CompositeURLClassLoader(Collection loaders) { + super(loaders.stream().flatMap(cl -> Arrays.stream(cl.getURLs())).toArray(URL[]::new)); + this.loaders = loaders; + } + + public CompositeURLClassLoader(URLClassLoader... loaders) { + this(new ArrayList<>(Arrays.asList(loaders))); + } + + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + for (URLClassLoader loader : loaders) { + try { + return loader.loadClass(name); + } catch (ClassNotFoundException e) { + // Ignore + } + } + + throw new ClassNotFoundException(name); + } +} diff --git a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java index a53541062..16547c1dd 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java @@ -79,8 +79,8 @@ public DelegatingProjectParser(Project project, RewriteExtension extension, Reso Class gppClass = Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, recipeClassLoader); assert (gppClass.getClassLoader() == rewriteClassLoader) : "DefaultProjectParser must be loaded from RewriteClassLoader to be sufficiently isolated from Gradle's classpath"; - gpp = (GradleProjectParser) gppClass.getDeclaredConstructor(Project.class, RewriteExtension.class) - .newInstance(project, extension); + gpp = (GradleProjectParser) gppClass.getDeclaredConstructor(Project.class, RewriteExtension.class, ClassLoader.class) + .newInstance(project, extension, new CompositeURLClassLoader(rewriteClassLoader, recipeClassLoader)); } catch (Exception e) { throw new RuntimeException(e); diff --git a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java index dca2065a2..f6e356c9b 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java @@ -122,6 +122,7 @@ public class DefaultProjectParser implements GradleProjectParser { protected final Path baseDir; protected final RewriteExtension extension; protected final Project project; + protected final ClassLoader classLoader; private final List sharedProvenance; @Nullable @@ -140,11 +141,12 @@ public class DefaultProjectParser implements GradleProjectParser { @Nullable private AndroidProjectParser androidProjectParser; - public DefaultProjectParser(Project project, RewriteExtension extension) { + public DefaultProjectParser(Project project, RewriteExtension extension, ClassLoader classLoader) { this.baseDir = repositoryRoot(project); this.repository = getRepository(baseDir); this.extension = extension; this.project = project; + this.classLoader = classLoader; BuildEnvironment buildEnvironment = BuildEnvironment.build(System::getenv); sharedProvenance = Stream.of( @@ -646,12 +648,12 @@ protected Environment environment() { properties.putAll(gradleProps); Environment.Builder env = Environment.builder(); - env.scanClassLoader(getClass().getClassLoader()); + env.scanClassLoader(classLoader); File rewriteConfig = extension.getConfigFile(); if (rewriteConfig.exists()) { try (FileInputStream is = new FileInputStream(rewriteConfig)) { - YamlResourceLoader resourceLoader = new YamlResourceLoader(is, rewriteConfig.toURI(), properties, getClass().getClassLoader()); + YamlResourceLoader resourceLoader = new YamlResourceLoader(is, rewriteConfig.toURI(), properties, classLoader); env.load(resourceLoader); } catch (IOException e) { throw new RuntimeException("Unable to load rewrite configuration", e); From fc3d385201238869b8011c1c6e7f39835afd3731 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:04:47 +0200 Subject: [PATCH 6/9] Clear Jackson's type cache as it caches types from the recipe's class loader --- .../openrewrite/gradle/isolated/DefaultProjectParser.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java index f6e356c9b..8296046e7 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java @@ -15,6 +15,7 @@ */ package org.openrewrite.gradle.isolated; +import com.fasterxml.jackson.databind.type.TypeFactory; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics; import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; @@ -1515,6 +1516,10 @@ public void shutdownRewrite() { @SuppressWarnings("unused") // Called reflectively by DelegatingProjectParser when it discards a class loader public static void shutdownJGitWorkQueue() { WorkQueue.getExecutor().shutdownNow(); + // Jackson (from the RewriteClassLoader) caches references of classes coming from the recipe's ClassLoader, + // which retains the recipe's CL until the RewriteClassLoader gets dropped too. (which is almost never) + // By clearing the cache, references to the recipe's CL are gone. + TypeFactory.defaultInstance().clearCache(); } private static synchronized MavenPomCache getPomCache(@Nullable String pomCacheDirectory) { From d97c10b84d319294d73f120cc5304fcd8e359625 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:08:37 +0200 Subject: [PATCH 7/9] Update naming and docs of class loader cleanup function --- .../openrewrite/gradle/DelegatingProjectParser.java | 2 +- .../gradle/isolated/DefaultProjectParser.java | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java index 16547c1dd..31107e6fd 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java @@ -173,7 +173,7 @@ public void shutdownRewrite() { private static void discard(URLClassLoader classLoader) throws IOException { try { Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, classLoader) - .getMethod("shutdownJGitWorkQueue") + .getMethod("cleanCurrentClassLoader") .invoke(null); } catch (ReflectiveOperationException | LinkageError ignored) { // Not all versions of rewrite bundle JGit, in which case there is no work queue to shut down diff --git a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java index 8296046e7..7213333d7 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/isolated/DefaultProjectParser.java @@ -1506,16 +1506,19 @@ public void shutdownRewrite() { } /** - * JGit keeps a daemon thread around per class loader that initialized it, and that thread references the - * class loader that created it. Without shutting it down every replaced class loader, and all the recipe - * classes it loaded, would be retained in metaspace for as long as the Gradle daemon lives. + * When a ClassLoader is ready to be discarded, + * cleans up some things needed to not leak the class loader of the currently executing class. *

* Deliberately not part of {@link #shutdownRewrite()}, as the executor is created once per class loader and * never recreated; it may only be shut down when the class loader itself is discarded. */ @SuppressWarnings("unused") // Called reflectively by DelegatingProjectParser when it discards a class loader - public static void shutdownJGitWorkQueue() { + public static void cleanCurrentClassLoader() { + // JGit keeps a daemon thread around per class loader that initialized it, and that thread references the + // class loader that created it. Without shutting it down every replaced class loader, and all the recipe + // classes it loaded, would be retained in metaspace for as long as the Gradle daemon lives. WorkQueue.getExecutor().shutdownNow(); + // Jackson (from the RewriteClassLoader) caches references of classes coming from the recipe's ClassLoader, // which retains the recipe's CL until the RewriteClassLoader gets dropped too. (which is almost never) // By clearing the cache, references to the recipe's CL are gone. From ce4233a315636cbf41486fb66e07912859afa401 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:23:38 +0200 Subject: [PATCH 8/9] Move out the logic getting rewrite/recipe classpaths --- .../gradle/AbstractRewriteTask.java | 5 +- .../gradle/DelegatingProjectParser.java | 11 +- .../org/openrewrite/gradle/RewritePlugin.java | 202 +----------------- .../dependencies/ProjectDependency.java | 32 +++ .../dependencies/ResolvedDependencies.java | 82 +++++++ .../ResolvedDependenciesProvider.java | 111 ++++++++++ 6 files changed, 240 insertions(+), 203 deletions(-) create mode 100644 plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java create mode 100644 plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java create mode 100644 plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java diff --git a/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java b/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java index 25cf65261..7f75c35ba 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java +++ b/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java @@ -24,7 +24,8 @@ import org.gradle.util.GradleVersion; import org.gradle.work.DisableCachingByDefault; import org.jspecify.annotations.Nullable; -import org.openrewrite.gradle.RewritePlugin.ResolvedDependencies; +import org.openrewrite.gradle.dependencies.ResolvedDependencies; +import org.openrewrite.gradle.dependencies.ResolvedDependenciesProvider; import javax.inject.Inject; import java.util.List; @@ -80,7 +81,7 @@ protected T getProjectParser() { } ResolvedDependencies deps = resolvedDependencies.getOrNull(); if (deps == null) { - deps = ResolvedDependencies.EMPTY; + deps = ResolvedDependenciesProvider.empty(); } gpp = new DelegatingProjectParser(getProject(), extension, deps); } diff --git a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java index 31107e6fd..b638c71d2 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java @@ -18,7 +18,8 @@ import org.gradle.api.Project; import org.gradle.internal.service.ServiceRegistry; import org.jspecify.annotations.Nullable; -import org.openrewrite.gradle.RewritePlugin.ResolvedDependencies; +import org.openrewrite.gradle.dependencies.ProjectDependency; +import org.openrewrite.gradle.dependencies.ResolvedDependencies; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -48,10 +49,10 @@ public class DelegatingProjectParser implements GradleProjectParser { public DelegatingProjectParser(Project project, RewriteExtension extension, ResolvedDependencies classpath) { try { List rewriteClasspathUrls = classpath.getFromRewriteOnly().stream() - .map(RewritePlugin.ProjectDependency::getUrl) + .map(ProjectDependency::getUrl) .collect(toList()); List recipeClasspathUrls = classpath.getFromRecipeOnly().stream() - .map(RewritePlugin.ProjectDependency::getUrl) + .map(ProjectDependency::getUrl) .collect(toList()); @SuppressWarnings("ConstantConditions") @@ -60,8 +61,8 @@ public DelegatingProjectParser(Project project, RewriteExtension extension, Reso .toString()); rewriteClasspathUrls.add(currentJar); - List rewriteClasspathEntries = classpath.getFromRewriteOnly().stream().map(RewritePlugin.ProjectDependency::getPath).collect(toList()); - List recipeClasspathEntries = classpath.getFromRecipeOnly().stream().map(RewritePlugin.ProjectDependency::getPath).collect(toList()); + List rewriteClasspathEntries = classpath.getFromRewriteOnly().stream().map(ProjectDependency::getPath).collect(toList()); + List recipeClasspathEntries = classpath.getFromRecipeOnly().stream().map(ProjectDependency::getPath).collect(toList()); rewriteClasspathEntries.add(Paths.get(currentJar.toURI())); ClassLoader pluginClassLoader = getPluginClassLoader(project); diff --git a/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java b/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java index c0ebbd75e..520b22ca7 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java +++ b/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java @@ -19,16 +19,6 @@ import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.artifacts.ModuleIdentifier; -import org.gradle.api.artifacts.component.ComponentIdentifier; -import org.gradle.api.artifacts.component.ModuleComponentIdentifier; -import org.gradle.api.artifacts.component.ProjectComponentIdentifier; -import org.gradle.api.artifacts.dsl.DependencyHandler; -import org.gradle.api.artifacts.result.ResolvedArtifactResult; -import org.gradle.api.attributes.*; -import org.gradle.api.attributes.java.TargetJvmEnvironment; -import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.JvmEcosystemPlugin; @@ -37,20 +27,15 @@ import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskProvider; -import org.jetbrains.annotations.TestOnly; import org.jspecify.annotations.Nullable; +import org.openrewrite.gradle.dependencies.ResolvedDependencies; +import org.openrewrite.gradle.dependencies.ResolvedDependenciesProvider; import java.io.File; import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Collectors; - -import static java.util.Collections.emptyList; -import static org.gradle.api.attributes.Bundling.BUNDLING_ATTRIBUTE; -import static org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; /** * When applied to the root project of a multi-project build, applies to all subprojects. @@ -177,183 +162,8 @@ private static void configureProject(Project project, RewriteExtension extension public ResolvedDependencies getResolvedDependencies(Project project, RewriteExtension extension, Configuration rewriteConf) { if (resolvedDependencies == null) { - // Avoid Stream.concat here pending https://github.com/gradle/gradle/issues/33152 - List dependencies = new ArrayList<>(); - dependencies.addAll(knownRewriteDependencies(extension, project.getDependencies())); - dependencies.addAll(rewriteConf.getDependencies()); - - List rewriteDependencies = resolveConfiguration(project, knownRewriteDependencies(extension, project.getDependencies())); - List recipeDependencies = resolveConfiguration(project, rewriteConf.getDependencies()); - List effectiveDependencies = resolveConfiguration(project, dependencies); - - resolvedDependencies = new ResolvedDependencies(rewriteDependencies, recipeDependencies, effectiveDependencies); + resolvedDependencies = ResolvedDependenciesProvider.get(project, extension, rewriteConf); } return resolvedDependencies; } - - private static List resolveConfiguration(Project project, Collection dependencies) { - // By using a detached configuration, we separate this dependency resolution from the rest of the project's - // configuration. This also means that Gradle has no criteria with which to select between variants of - // dependencies which expose differing capabilities. So those must be manually configured - Configuration detachedConf = project.getConfigurations().detachedConfiguration(dependencies.toArray(new Dependency[0])); - configureAttributes(project, detachedConf); - - return detachedConf.getIncoming() - .getArtifacts().getArtifacts() - .stream() - .map(artifactResult -> { - try { - return new ProjectDependency(artifactResult); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toList()); - } - - private static void configureAttributes(Project project, Configuration configuration) { - try { - ObjectFactory objectFactory = project.getObjects(); - configuration.attributes(attributes -> { - // Adapted from org.gradle.api.plugins.jvm.internal.DefaultJvmEcosystemAttributesDetails - attributes.attribute(Category.CATEGORY_ATTRIBUTE, objectFactory.named(Category.class, Category.LIBRARY)); - attributes.attribute(Usage.USAGE_ATTRIBUTE, objectFactory.named(Usage.class, Usage.JAVA_RUNTIME)); - attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objectFactory.named(LibraryElements.class, LibraryElements.JAR)); - attributes.attribute(BUNDLING_ATTRIBUTE, objectFactory.named(Bundling.class, Bundling.EXTERNAL)); - try { - attributes.attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objectFactory.named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM)); - } catch (NoClassDefFoundError e) { - // Old versions of Gradle don't have the class TargetJvmEnvironment and that's OK, we can always - // try this attribute instead - attributes.attribute(Attribute.of("org.gradle.jvm.environment", String.class), "standard-jvm"); - } - }); - } catch (NoClassDefFoundError e) { - // Old versions of Gradle don't have all of these attributes and that's OK - } - } - - private static Collection knownRewriteDependencies(RewriteExtension extension, DependencyHandler deps) { - String rewriteVersion = extension.getRewriteVersion(); - return Arrays.asList( - deps.create("org.openrewrite:rewrite-core:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-docker:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-groovy:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-gradle:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-hcl:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-json:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-kotlin:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-java:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-java-25:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-java-21:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-java-17:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-java-11:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-java-8:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-maven:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-properties:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-protobuf:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-toml:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-xml:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-yaml:" + rewriteVersion), - deps.create("org.openrewrite:rewrite-polyglot:" + extension.getRewritePolyglotVersion()), - deps.create("org.openrewrite.gradle.tooling:model:" + extension.getRewriteGradleModelVersion()), - deps.create("com.fasterxml.jackson.module:jackson-module-kotlin:" + extension.getJacksonModuleKotlinVersion()), - deps.create("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:" + extension.getJacksonModuleKotlinVersion()), - deps.create("org.rocksdb:rocksdbjni:" + extension.getRocksdbJniVersion()) - ); - } - - public static class ResolvedDependencies { - static final ResolvedDependencies EMPTY = new ResolvedDependencies(emptyList(), emptyList(), emptyList()); - - private final List rewriteClasspath; - private final List recipeClasspath; - private final List effectiveClasspath; - - private ResolvedDependencies(List rewriteClasspath, List recipeClasspath, List effectiveClasspath) { - this.rewriteClasspath = rewriteClasspath; - this.recipeClasspath = recipeClasspath; - this.effectiveClasspath = effectiveClasspath; - } - - @TestOnly // Used by RewritePluginTest - @SuppressWarnings("unused") - public List getEffectiveClasspath() { - return effectiveClasspath; - } - - /** Returns dependencies only including OpenRewrite's own required dependencies */ - public List getFromRewriteOnly() { - // Using the effective dependency versions, take the ones that only appear from the (known) rewrite classpath - return effectiveClasspath.stream() - .filter(effectiveDependency -> { - // Keep if from known rewrite deps - ComponentIdentifier expectedIdentifier = effectiveDependency.getIdentifier(); - return rewriteClasspath.stream() - .anyMatch(rewriteDependency -> hasSameDependencyIdentifier(expectedIdentifier, rewriteDependency.getIdentifier())); - }) - .collect(Collectors.toList()); - } - - /** Returns dependencies that are only present in the recipe's classpath, i.e., excluding rewrite classpath */ - public List getFromRecipeOnly() { - // Take recipe classpath, remove rewrite modules - return recipeClasspath.stream() - .filter(recipeDependency -> { - ComponentIdentifier expectedIdentifier = recipeDependency.getIdentifier(); - - // Keep when identifier (independent of version) can't be found in rewrite classpath - return rewriteClasspath.stream().noneMatch(dep -> hasSameDependencyIdentifier(expectedIdentifier, dep.getIdentifier())); - }) - .collect(Collectors.toList()); - } - - private static boolean hasSameDependencyIdentifier(ComponentIdentifier expectedIdentifier, ComponentIdentifier depIdentifier) { - if (!(expectedIdentifier instanceof ModuleComponentIdentifier) && !(expectedIdentifier instanceof ProjectComponentIdentifier)) { - throw new UnsupportedOperationException("Unsupported component identifier type: " + expectedIdentifier.getClass().getName()); - } - - if (expectedIdentifier instanceof ModuleComponentIdentifier) { - if (!(depIdentifier instanceof ModuleComponentIdentifier)) { - return false; - } - - ModuleIdentifier expectedModuleIdentifier = ((ModuleComponentIdentifier) expectedIdentifier).getModuleIdentifier(); - ModuleIdentifier depModuleIdentifier = ((ModuleComponentIdentifier) depIdentifier).getModuleIdentifier(); - return expectedModuleIdentifier.equals(depModuleIdentifier); - } else if (expectedIdentifier instanceof ProjectComponentIdentifier) { - if (!(depIdentifier instanceof ProjectComponentIdentifier)) { - return false; - } - - return depIdentifier.equals(expectedIdentifier); - } else { - throw new UnsupportedOperationException("Unsupported component identifier type: " + depIdentifier.getClass().getName()); - } - } - } - - public static class ProjectDependency { - private final Path path; - private final URL url; - private final ComponentIdentifier identifier; - - private ProjectDependency(ResolvedArtifactResult artifactResult) throws MalformedURLException { - this.path = artifactResult.getFile().toPath(); - this.url = path.toUri().toURL(); - this.identifier = artifactResult.getVariant().getOwner(); - } - - public Path getPath() { - return path; - } - - public URL getUrl() { - return url; - } - - public ComponentIdentifier getIdentifier() { - return identifier; - } - } } diff --git a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java new file mode 100644 index 000000000..c9d10be47 --- /dev/null +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java @@ -0,0 +1,32 @@ +package org.openrewrite.gradle.dependencies; + +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; + +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Path; + +public class ProjectDependency { + private final Path path; + private final URL url; + private final ComponentIdentifier identifier; + + public ProjectDependency(ResolvedArtifactResult artifactResult) throws MalformedURLException { + this.path = artifactResult.getFile().toPath(); + this.url = path.toUri().toURL(); + this.identifier = artifactResult.getVariant().getOwner(); + } + + public Path getPath() { + return path; + } + + public URL getUrl() { + return url; + } + + public ComponentIdentifier getIdentifier() { + return identifier; + } +} diff --git a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java new file mode 100644 index 000000000..8d44f1689 --- /dev/null +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java @@ -0,0 +1,82 @@ +package org.openrewrite.gradle.dependencies; + +import org.gradle.api.artifacts.ModuleIdentifier; +import org.gradle.api.artifacts.component.ComponentIdentifier; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.component.ProjectComponentIdentifier; +import org.jetbrains.annotations.TestOnly; + +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +public class ResolvedDependencies { + static final ResolvedDependencies EMPTY = new ResolvedDependencies(emptyList(), emptyList(), emptyList()); + + private final List rewriteClasspath; + private final List recipeClasspath; + private final List effectiveClasspath; + + ResolvedDependencies(List rewriteClasspath, List recipeClasspath, List effectiveClasspath) { + this.rewriteClasspath = rewriteClasspath; + this.recipeClasspath = recipeClasspath; + this.effectiveClasspath = effectiveClasspath; + } + + @TestOnly // Used by RewritePluginTest + @SuppressWarnings("unused") + public List getEffectiveClasspath() { + return effectiveClasspath; + } + + /** Returns dependencies only including OpenRewrite's own required dependencies */ + public List getFromRewriteOnly() { + // Using the effective dependency versions, take the ones that only appear from the (known) rewrite classpath + return effectiveClasspath.stream() + .filter(effectiveDependency -> { + // Keep if from known rewrite deps + ComponentIdentifier expectedIdentifier = effectiveDependency.getIdentifier(); + return rewriteClasspath.stream() + .anyMatch(rewriteDependency -> hasSameDependencyIdentifier(expectedIdentifier, rewriteDependency.getIdentifier())); + }) + .collect(toList()); + } + + /** Returns dependencies that are only present in the recipe's classpath, i.e., excluding rewrite classpath */ + public List getFromRecipeOnly() { + // Take recipe classpath, remove rewrite modules + return recipeClasspath.stream() + .filter(recipeDependency -> { + ComponentIdentifier expectedIdentifier = recipeDependency.getIdentifier(); + + // Keep when identifier (independent of version) can't be found in rewrite classpath + return rewriteClasspath.stream().noneMatch(dep -> hasSameDependencyIdentifier(expectedIdentifier, dep.getIdentifier())); + }) + .collect(toList()); + } + + private static boolean hasSameDependencyIdentifier(ComponentIdentifier expectedIdentifier, ComponentIdentifier depIdentifier) { + if (!(expectedIdentifier instanceof ModuleComponentIdentifier) && !(expectedIdentifier instanceof ProjectComponentIdentifier)) { + throw new UnsupportedOperationException("Unsupported component identifier type: " + expectedIdentifier.getClass().getName()); + } + + if (expectedIdentifier instanceof ModuleComponentIdentifier) { + if (!(depIdentifier instanceof ModuleComponentIdentifier)) { + return false; + } + + ModuleIdentifier expectedModuleIdentifier = ((ModuleComponentIdentifier) expectedIdentifier).getModuleIdentifier(); + ModuleIdentifier depModuleIdentifier = ((ModuleComponentIdentifier) depIdentifier).getModuleIdentifier(); + return expectedModuleIdentifier.equals(depModuleIdentifier); + } else if (expectedIdentifier instanceof ProjectComponentIdentifier) { + if (!(depIdentifier instanceof ProjectComponentIdentifier)) { + return false; + } + + return depIdentifier.equals(expectedIdentifier); + } else { + throw new UnsupportedOperationException("Unsupported component identifier type: " + depIdentifier.getClass().getName()); + } + } +} diff --git a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java new file mode 100644 index 000000000..5b4b28af8 --- /dev/null +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java @@ -0,0 +1,111 @@ +package org.openrewrite.gradle.dependencies; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.attributes.*; +import org.gradle.api.attributes.java.TargetJvmEnvironment; +import org.gradle.api.model.ObjectFactory; +import org.openrewrite.gradle.RewriteExtension; + +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static java.util.stream.Collectors.toList; +import static org.gradle.api.attributes.Bundling.BUNDLING_ATTRIBUTE; +import static org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE; + +public class ResolvedDependenciesProvider { + public static ResolvedDependencies empty() { + return ResolvedDependencies.EMPTY; + } + + public static ResolvedDependencies get(Project project, RewriteExtension extension, Configuration rewriteConf) { + // Avoid Stream.concat here pending https://github.com/gradle/gradle/issues/33152 + List dependencies = new ArrayList<>(); + dependencies.addAll(knownRewriteDependencies(extension, project.getDependencies())); + dependencies.addAll(rewriteConf.getDependencies()); + + List rewriteDependencies = resolveConfiguration(project, knownRewriteDependencies(extension, project.getDependencies())); + List recipeDependencies = resolveConfiguration(project, rewriteConf.getDependencies()); + List effectiveDependencies = resolveConfiguration(project, dependencies); + + return new ResolvedDependencies(rewriteDependencies, recipeDependencies, effectiveDependencies); + } + + private static List resolveConfiguration(Project project, Collection dependencies) { + // By using a detached configuration, we separate this dependency resolution from the rest of the project's + // configuration. This also means that Gradle has no criteria with which to select between variants of + // dependencies which expose differing capabilities. So those must be manually configured + Configuration detachedConf = project.getConfigurations().detachedConfiguration(dependencies.toArray(new Dependency[0])); + configureAttributes(project, detachedConf); + + return detachedConf.getIncoming() + .getArtifacts().getArtifacts() + .stream() + .map(artifactResult -> { + try { + return new ProjectDependency(artifactResult); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }) + .collect(toList()); + } + + private static void configureAttributes(Project project, Configuration configuration) { + try { + ObjectFactory objectFactory = project.getObjects(); + configuration.attributes(attributes -> { + // Adapted from org.gradle.api.plugins.jvm.internal.DefaultJvmEcosystemAttributesDetails + attributes.attribute(Category.CATEGORY_ATTRIBUTE, objectFactory.named(Category.class, Category.LIBRARY)); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objectFactory.named(Usage.class, Usage.JAVA_RUNTIME)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objectFactory.named(LibraryElements.class, LibraryElements.JAR)); + attributes.attribute(BUNDLING_ATTRIBUTE, objectFactory.named(Bundling.class, Bundling.EXTERNAL)); + try { + attributes.attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objectFactory.named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM)); + } catch (NoClassDefFoundError e) { + // Old versions of Gradle don't have the class TargetJvmEnvironment and that's OK, we can always + // try this attribute instead + attributes.attribute(Attribute.of("org.gradle.jvm.environment", String.class), "standard-jvm"); + } + }); + } catch (NoClassDefFoundError e) { + // Old versions of Gradle don't have all of these attributes and that's OK + } + } + + private static Collection knownRewriteDependencies(RewriteExtension extension, DependencyHandler deps) { + String rewriteVersion = extension.getRewriteVersion(); + return Arrays.asList( + deps.create("org.openrewrite:rewrite-core:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-docker:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-groovy:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-gradle:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-hcl:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-json:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-kotlin:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-java:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-java-25:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-java-21:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-java-17:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-java-11:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-java-8:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-maven:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-properties:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-protobuf:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-toml:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-xml:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-yaml:" + rewriteVersion), + deps.create("org.openrewrite:rewrite-polyglot:" + extension.getRewritePolyglotVersion()), + deps.create("org.openrewrite.gradle.tooling:model:" + extension.getRewriteGradleModelVersion()), + deps.create("com.fasterxml.jackson.module:jackson-module-kotlin:" + extension.getJacksonModuleKotlinVersion()), + deps.create("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:" + extension.getJacksonModuleKotlinVersion()), + deps.create("org.rocksdb:rocksdbjni:" + extension.getRocksdbJniVersion()) + ); + } +} From 7c919ff41ecc92e5fa81f0ef00e6919cc946b440 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:34:11 +0200 Subject: [PATCH 9/9] Add missing copyright headers --- .../gradle/dependencies/ProjectDependency.java | 15 +++++++++++++++ .../gradle/dependencies/ResolvedDependencies.java | 15 +++++++++++++++ .../ResolvedDependenciesProvider.java | 15 +++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java index c9d10be47..acef6efa3 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java @@ -1,3 +1,18 @@ +/* + * 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.gradle.dependencies; import org.gradle.api.artifacts.component.ComponentIdentifier; diff --git a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java index 8d44f1689..a0df8db0e 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java @@ -1,3 +1,18 @@ +/* + * 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.gradle.dependencies; import org.gradle.api.artifacts.ModuleIdentifier; diff --git a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java index 5b4b28af8..528903c42 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java @@ -1,3 +1,18 @@ +/* + * 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.gradle.dependencies; import org.gradle.api.Project;