Skip to content

Commit 6d95c2a

Browse files
authored
Merge branch 'main' into type-information-top-level-method-invocations
2 parents 4bbdc61 + bdcdb79 commit 6d95c2a

File tree

37 files changed

+2749
-348
lines changed

37 files changed

+2749
-348
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ IDE.properties
1515
.tool-versions
1616
.aider*
1717
/worktrees
18+
rewrite-gradle-tooling-model/model/src/main/resources/test-manifest.txt

rewrite-core/src/main/java/org/openrewrite/config/ClasspathScanningLoader.java

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,19 @@
2323
import org.jspecify.annotations.Nullable;
2424
import org.openrewrite.Contributor;
2525
import org.openrewrite.Recipe;
26-
import org.openrewrite.ScanningRecipe;
2726
import org.openrewrite.internal.MetricsHelper;
2827
import org.openrewrite.internal.RecipeIntrospectionUtils;
2928
import org.openrewrite.internal.RecipeLoader;
3029
import org.openrewrite.style.NamedStyles;
3130

31+
import java.io.IOException;
32+
import java.io.UncheckedIOException;
3233
import java.lang.reflect.Constructor;
3334
import java.lang.reflect.Modifier;
35+
import java.net.URI;
36+
import java.nio.file.Files;
3437
import java.nio.file.Path;
38+
import java.nio.file.Paths;
3539
import java.util.*;
3640

3741
import static java.util.Collections.emptyList;
@@ -97,16 +101,15 @@ public ClasspathScanningLoader(Properties properties, ClassLoader classLoader) {
97101
public ClasspathScanningLoader(Path jar, Properties properties, Collection<? extends ResourceLoader> dependencyResourceLoaders, ClassLoader classLoader) {
98102
this.classLoader = classLoader;
99103
this.recipeLoader = new RecipeLoader(classLoader);
100-
String jarName = jar.toFile().getName();
101104

102105
this.performScan = () -> {
106+
// Scan entire classpath to get full inheritance hierarchy, but filter results to target jar
103107
scanClasses(new ClassGraph()
104-
.acceptJars(jarName)
105108
.ignoreParentClassLoaders()
106-
.overrideClassLoaders(classLoader), classLoader);
109+
.overrideClassLoaders(classLoader), classLoader, jar);
107110

108111
scanYaml(new ClassGraph()
109-
.acceptJars(jarName)
112+
.acceptJars(jar.toFile().getName())
110113
.ignoreParentClassLoaders()
111114
.overrideClassLoaders(classLoader)
112115
.acceptPaths("META-INF/rewrite"), properties, dependencyResourceLoaders, classLoader);
@@ -155,37 +158,56 @@ private void scanYaml(ClassGraph classGraph, Properties properties, Collection<?
155158
}
156159

157160
private void scanClasses(ClassGraph classGraph, ClassLoader classLoader) {
161+
scanClasses(classGraph, classLoader, null);
162+
}
163+
164+
private void scanClasses(ClassGraph classGraph, ClassLoader classLoader, @Nullable Path targetJarName) {
158165
try (ScanResult result = classGraph
159166
.ignoreClassVisibility()
160167
.overrideClassLoaders(classLoader)
161168
.scan()) {
162169

163-
configureRecipes(result, Recipe.class.getName());
164-
configureRecipes(result, ScanningRecipe.class.getName());
170+
configureRecipes(result, Recipe.class.getName(), targetJarName);
165171

166172
for (ClassInfo classInfo : result.getSubclasses(NamedStyles.class.getName())) {
173+
// Only process styles from the target jar if specified
174+
if (targetJarName != null && !isFromJar(classInfo.getClasspathElementURI(), targetJarName)) {
175+
continue;
176+
}
167177
Class<?> styleClass = classInfo.loadClass();
168-
Constructor<?> constructor = RecipeIntrospectionUtils.getZeroArgsConstructor(styleClass);
169-
if (constructor != null) {
170-
constructor.setAccessible(true);
171-
try {
172-
styles.add((NamedStyles) constructor.newInstance());
173-
} catch (Exception e) {
174-
throw new RuntimeException(e);
175-
}
178+
Constructor<?> constructor = RecipeIntrospectionUtils.getZeroArgsConstructor(styleClass);
179+
if (constructor != null) {
180+
constructor.setAccessible(true);
181+
try {
182+
styles.add((NamedStyles) constructor.newInstance());
183+
} catch (Exception e) {
184+
throw new RuntimeException(e);
176185
}
177-
186+
}
178187
}
179188
}
180189
}
181190

182-
private void configureRecipes(ScanResult result, String className) {
191+
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
192+
private boolean isFromJar(@Nullable URI classpathElementURI, Path jar) {
193+
try {
194+
return classpathElementURI != null && Files.isSameFile(Paths.get(classpathElementURI), jar);
195+
} catch (IOException e) {
196+
throw new UncheckedIOException(e);
197+
}
198+
}
199+
200+
private void configureRecipes(ScanResult result, String className, @Nullable Path targetJar) {
183201
for (ClassInfo classInfo : result.getSubclasses(className)) {
202+
// Only process recipes from the target jar if specified
203+
if (targetJar != null && !isFromJar(classInfo.getClasspathElementURI(), targetJar)) {
204+
continue;
205+
}
184206
Class<?> recipeClass = classInfo.loadClass();
185207
if (recipeClass.getName().equals(DeclarativeRecipe.class.getName()) ||
186-
(recipeClass.getModifiers() & Modifier.PUBLIC) == 0 ||
187-
// `ScanningRecipe` is an example of an abstract `Recipe` subtype
188-
(recipeClass.getModifiers() & Modifier.ABSTRACT) != 0) {
208+
(recipeClass.getModifiers() & Modifier.PUBLIC) == 0 ||
209+
// `ScanningRecipe` is an example of an abstract `Recipe` subtype
210+
(recipeClass.getModifiers() & Modifier.ABSTRACT) != 0) {
189211
continue;
190212
}
191213
Timer.Builder builder = Timer.builder("rewrite.scan.configure.recipe");
@@ -224,8 +246,9 @@ public Collection<Recipe> listRecipes() {
224246

225247
private void ensureScanned() {
226248
if (performScan != null) {
227-
performScan.run();
249+
Runnable scan = performScan;
228250
performScan = null;
251+
scan.run();
229252
}
230253
}
231254

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.config;
17+
18+
import org.intellij.lang.annotations.Language;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.io.TempDir;
21+
22+
import javax.tools.JavaCompiler;
23+
import javax.tools.StandardJavaFileManager;
24+
import javax.tools.StandardLocation;
25+
import javax.tools.ToolProvider;
26+
import java.io.File;
27+
import java.io.IOException;
28+
import java.net.URL;
29+
import java.net.URLClassLoader;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
import java.util.ArrayList;
33+
import java.util.Arrays;
34+
import java.util.List;
35+
import java.util.jar.JarEntry;
36+
import java.util.jar.JarOutputStream;
37+
38+
import static java.util.Collections.emptyList;
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
41+
class ClasspathScanningLoaderTest {
42+
43+
@TempDir
44+
Path tempDir;
45+
46+
@Test
47+
void testInheritanceAcrossJars() throws IOException {
48+
// Create base jar with abstract Recipe subclass
49+
Path baseJar = createBaseRecipeJar();
50+
51+
// Create concrete jar with implementation
52+
Path concreteJar = createConcreteRecipeJar();
53+
54+
// Get rewrite-core jar path (contains Recipe base class)
55+
Path coreJar = findRewriteCoreJar();
56+
57+
// Test 1: URLClassLoader with all jars - should find concrete recipe
58+
URLClassLoader fullClassLoader = new URLClassLoader(
59+
new URL[]{baseJar.toUri().toURL(), concreteJar.toUri().toURL(), coreJar.toUri().toURL()},
60+
ClasspathScanningLoaderTest.class.getClassLoader() // Use parent for Recipe class identity
61+
);
62+
63+
Environment fullEnv = Environment.builder()
64+
.scanJar(concreteJar, emptyList(), fullClassLoader)
65+
.build();
66+
67+
assertThat(fullEnv.listRecipes()).hasSize(1);
68+
assertThat(fullEnv.listRecipes().getFirst().getName()).isEqualTo("test.ConcreteTestRecipe");
69+
70+
// Test 2: URLClassLoader missing base jar - should fail to find recipe due to inheritance
71+
URLClassLoader partialClassLoader = new URLClassLoader(
72+
new URL[]{concreteJar.toUri().toURL(), coreJar.toUri().toURL()},
73+
null // No parent to ensure isolation
74+
);
75+
76+
Environment partialEnv = Environment.builder()
77+
.scanJar(concreteJar, emptyList(), partialClassLoader)
78+
.build();
79+
80+
// Should find 0 recipes because inheritance chain is broken
81+
assertThat(partialEnv.listRecipes()).isEmpty();
82+
}
83+
84+
private Path createBaseRecipeJar() throws IOException {
85+
Path baseJar = tempDir.resolve("base-recipe.jar");
86+
87+
// Create source directory structure
88+
Path srcDir = tempDir.resolve("base-src");
89+
Path packageDir = srcDir.resolve("test");
90+
Files.createDirectories(packageDir);
91+
92+
// Create abstract Recipe subclass
93+
@Language("java")
94+
String abstractRecipeSource = """
95+
package test;
96+
import org.openrewrite.Recipe;
97+
import org.openrewrite.TreeVisitor;
98+
import org.openrewrite.ExecutionContext;
99+
import org.openrewrite.SourceFile;
100+
101+
public abstract class AbstractTestRecipe extends Recipe {
102+
@Override
103+
public String getDisplayName() {
104+
return "Abstract test recipe";
105+
}
106+
107+
@Override
108+
public String getDescription() {
109+
return "Base class for test recipes.";
110+
}
111+
112+
@Override
113+
public abstract TreeVisitor<?, ExecutionContext> getVisitor();
114+
}
115+
""";
116+
117+
Files.write(packageDir.resolve("AbstractTestRecipe.java"), abstractRecipeSource.getBytes());
118+
119+
// Compile the source using current classpath (includes rewrite-core)
120+
Path classesDir = tempDir.resolve("base-classes");
121+
compileJavaWithClasspath(srcDir, classesDir, emptyList());
122+
123+
// Create jar
124+
createJar(classesDir, baseJar);
125+
126+
return baseJar;
127+
}
128+
129+
private Path createConcreteRecipeJar() throws IOException {
130+
Path concreteJar = tempDir.resolve("concrete-recipe.jar");
131+
132+
// Create source directory structure
133+
Path srcDir = tempDir.resolve("concrete-src");
134+
Path packageDir = srcDir.resolve("test");
135+
Files.createDirectories(packageDir);
136+
137+
// Create concrete Recipe implementation
138+
@Language("java")
139+
String concreteRecipeSource = """
140+
package test;
141+
import org.openrewrite.TreeVisitor;
142+
import org.openrewrite.ExecutionContext;
143+
import org.openrewrite.SourceFile;
144+
145+
public class ConcreteTestRecipe extends AbstractTestRecipe {
146+
@Override
147+
public String getName() {
148+
return "test.ConcreteTestRecipe";
149+
}
150+
151+
@Override
152+
public String getDisplayName() {
153+
return "Concrete test recipe";
154+
}
155+
156+
@Override
157+
public String getDescription() {
158+
return "Concrete implementation of test recipe.";
159+
}
160+
161+
@Override
162+
public TreeVisitor<?, ExecutionContext> getVisitor() {
163+
return new TreeVisitor<SourceFile, ExecutionContext>() {};
164+
}
165+
}
166+
""";
167+
168+
Files.write(packageDir.resolve("ConcreteTestRecipe.java"), concreteRecipeSource.getBytes());
169+
170+
// Compile the source (need base jar in classpath, current classpath has rewrite-core)
171+
Path classesDir = tempDir.resolve("concrete-classes");
172+
Path baseJar = tempDir.resolve("base-recipe.jar");
173+
compileJavaWithClasspath(srcDir, classesDir, List.of(baseJar));
174+
175+
// Create jar
176+
createJar(classesDir, concreteJar);
177+
178+
return concreteJar;
179+
}
180+
181+
private void compileJavaWithClasspath(Path sourceDir, Path outputDir, List<Path> classpathJars) throws IOException {
182+
Files.createDirectories(outputDir);
183+
184+
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
185+
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
186+
187+
// Set output directory
188+
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(outputDir.toFile()));
189+
190+
// Use current test classpath plus any additional jars
191+
var classpath = new ArrayList<Path>();
192+
193+
// Add current test classpath
194+
String currentClassPath = System.getProperty("java.class.path");
195+
Arrays.stream(currentClassPath.split(File.pathSeparator))
196+
.map(Path::of)
197+
.forEach(classpath::add);
198+
199+
// Add additional jars
200+
classpath.addAll(classpathJars);
201+
202+
fileManager.setLocation(StandardLocation.CLASS_PATH,
203+
classpath.stream().map(Path::toFile).toList());
204+
205+
// Find all Java files
206+
var javaFiles = fileManager.getJavaFileObjectsFromPaths(
207+
Files.walk(sourceDir)
208+
.filter(path -> path.toString().endsWith(".java"))
209+
.toList()
210+
);
211+
212+
// Compile
213+
var task = compiler.getTask(null, fileManager, null, null, null, javaFiles);
214+
if (!task.call()) {
215+
throw new RuntimeException("Compilation failed");
216+
}
217+
218+
fileManager.close();
219+
}
220+
221+
private void createJar(Path classesDir, Path jarPath) throws IOException {
222+
try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jarPath))) {
223+
Files.walk(classesDir)
224+
.filter(Files::isRegularFile)
225+
.forEach(file -> {
226+
try {
227+
String entryName = classesDir.relativize(file).toString().replace('\\', '/');
228+
jos.putNextEntry(new JarEntry(entryName));
229+
Files.copy(file, jos);
230+
jos.closeEntry();
231+
} catch (IOException e) {
232+
throw new RuntimeException(e);
233+
}
234+
});
235+
}
236+
}
237+
238+
private Path findRewriteCoreJar() {
239+
// Try to find rewrite-core jar from current classpath
240+
String classPath = System.getProperty("java.class.path");
241+
return Arrays.stream(classPath.split(File.pathSeparator))
242+
.filter(path -> path.contains("rewrite-core"))
243+
.filter(path -> path.endsWith(".jar") || path.endsWith("classes"))
244+
.findFirst()
245+
.map(Path::of)
246+
.orElseThrow(() -> new IllegalStateException("Could not find rewrite-core jar or classes in classpath: " + classPath));
247+
}
248+
}

0 commit comments

Comments
 (0)