diff --git a/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java b/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java index aff5d4087..7f75c35ba 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java +++ b/plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.java @@ -24,19 +24,15 @@ import org.gradle.util.GradleVersion; import org.gradle.work.DisableCachingByDefault; import org.jspecify.annotations.Nullable; +import org.openrewrite.gradle.dependencies.ResolvedDependencies; +import org.openrewrite.gradle.dependencies.ResolvedDependenciesProvider; 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 +49,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 +79,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 = ResolvedDependenciesProvider.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/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 1d5c6ea2e..b638c71d2 100755 --- a/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java +++ b/plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.java @@ -18,70 +18,107 @@ import org.gradle.api.Project; import org.gradle.internal.service.ServiceRegistry; import org.jspecify.annotations.Nullable; +import org.openrewrite.gradle.dependencies.ProjectDependency; +import org.openrewrite.gradle.dependencies.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.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import java.net.URLClassLoader; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; import java.util.concurrent.Callable; import java.util.function.Consumer; +import static java.util.Collections.sort; import static java.util.stream.Collectors.toList; public class DelegatingProjectParser implements GradleProjectParser { @Nullable - protected static List rewriteClasspath; + 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(ProjectDependency::getUrl) + .collect(toList()); + List recipeClasspathUrls = classpath.getFromRecipeOnly().stream() + .map(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 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); + List newRewriteClasspathFingerprint = fingerprint(rewriteClasspathEntries); + List newRecipeClasspathFingerprint = fingerprint(recipeClasspathEntries); - if (rewriteClassLoader == null || - !classpathUrls.equals(rewriteClasspath) || - rewriteClassLoader.getPluginClassLoader() != pluginClassLoader) { - if (rewriteClassLoader != null) { - rewriteClassLoader.close(); - } - rewriteClassLoader = new RewriteClassLoader(classpathUrls, pluginClassLoader); - rewriteClasspath = classpathUrls; + // 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); + gpp = (GradleProjectParser) gppClass.getDeclaredConstructor(Project.class, RewriteExtension.class, ClassLoader.class) + .newInstance(project, extension, new CompositeURLClassLoader(rewriteClassLoader, recipeClassLoader)); } catch (Exception e) { throw new RuntimeException(e); } } + 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); @@ -134,6 +171,61 @@ public void shutdownRewrite() { }); } + private static void discard(URLClassLoader classLoader) throws IOException { + try { + Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, classLoader) + .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 + } + 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 @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(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 long stamp(Path file, BasicFileAttributes attributes) { + return 31L * (31L * file.hashCode() + attributes.size()) + attributes.lastModifiedTime().toMillis(); + } + + private static class DirectoryStamp extends SimpleFileVisitor { + private long stamp; + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) { + stamp += stamp(file, attributes); + return FileVisitResult.CONTINUE; + } + } + protected URL jarContainingResource(String resourcePath) { try { if (resourcePath.startsWith("jar:")) { diff --git a/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java b/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java index 484003b26..520b22ca7 100644 --- a/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java +++ b/plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.java @@ -19,11 +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.dsl.DependencyHandler; -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; @@ -33,13 +28,14 @@ import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskProvider; 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.util.*; - -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. @@ -52,7 +48,7 @@ public class RewritePlugin implements Plugin { @Nullable - private Set resolvedDependencies; + private ResolvedDependencies resolvedDependencies; @Override public void apply(Project project) { @@ -70,7 +66,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,69 +160,10 @@ 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)); - 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 - } - - resolvedDependencies = detachedConf.resolve(); + resolvedDependencies = ResolvedDependenciesProvider.get(project, extension, rewriteConf); } return resolvedDependencies; } - - 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()) - ); - } } 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..acef6efa3 --- /dev/null +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ProjectDependency.java @@ -0,0 +1,47 @@ +/* + * 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; +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..a0df8db0e --- /dev/null +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependencies.java @@ -0,0 +1,97 @@ +/* + * 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; +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..528903c42 --- /dev/null +++ b/plugin/src/main/java/org/openrewrite/gradle/dependencies/ResolvedDependenciesProvider.java @@ -0,0 +1,126 @@ +/* + * 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; +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()) + ); + } +} 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 34d96ec78..db3a887b5 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; @@ -68,6 +69,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; @@ -121,6 +123,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 @@ -139,11 +142,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( @@ -645,12 +649,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); @@ -1504,6 +1508,26 @@ public void shutdownRewrite() { } } + /** + * 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 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. + TypeFactory.defaultInstance().clearCache(); + } + 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 new file mode 100644 index 000000000..213c1f01a --- /dev/null +++ b/plugin/src/test/java/org/openrewrite/gradle/DelegatingProjectParserTest.java @@ -0,0 +1,107 @@ +/* + * 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.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.util.List; + +import static java.nio.charset.StandardCharsets.UTF_8; +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; + +@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(singletonList(jar))).isEqualTo(fingerprint(singletonList(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(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(singletonList(jar)); + + write(jar, "second"); + + 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(singletonList(jar)); + + touch(jar); + + 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(singletonList(tempDir.resolve("classes"))); + + touch(classes.resolve("Recipe.class")); + + 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(singletonList(tempDir.resolve("classes"))); + + write(classes.resolve("OtherRecipe.class"), "first"); + + assertThat(fingerprint(singletonList(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)); + } +} 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 b128288ea..4d4a2ae4a 100644 --- a/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt +++ b/plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt @@ -18,12 +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" @@ -65,4 +65,63 @@ 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(GradleFixtures.REWRITE_BUILD_GRADLE + """ + 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() } 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"