Skip to content

Commit eec546e

Browse files
authored
Merge branch 'main' into fix/change-type-source-path
2 parents f73e157 + 806843d commit eec546e

259 files changed

Lines changed: 15841 additions & 4316 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

rewrite-core/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,5 @@ tasks.withType<Javadoc> {
4646
// symbol: method onConstructor_()
4747
// location: @interface AllArgsConstructor
4848
// 1 error
49-
exclude("**/RpcObjectData.java")
49+
exclude("**/RpcObjectData.java", "**/RecipeDescriptor.java")
5050
}

rewrite-core/src/main/java/org/openrewrite/Checksum.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import java.io.IOException;
2727
import java.io.InputStream;
28+
import java.io.UncheckedIOException;
2829
import java.net.URI;
2930
import java.nio.charset.StandardCharsets;
3031
import java.nio.file.Files;
@@ -83,6 +84,10 @@ public static Checksum fromUri(HttpSender httpSender, URI uri, String algorithm)
8384
.withMethod(HttpSender.Method.GET)
8485
.build();
8586
try (HttpSender.Response response = httpSender.send(request)) {
87+
if (!response.isSuccessful()) {
88+
throw new UncheckedIOException(new IOException(
89+
"Failed to download checksum from " + uri + ": HTTP " + response.getCode()));
90+
}
8691
String hexString = new String(response.getBodyAsBytes(), StandardCharsets.UTF_8);
8792
return Checksum.fromHex(algorithm, hexString);
8893
}

rewrite-core/src/main/java/org/openrewrite/LargeSourceSet.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,17 @@ default void afterCycle(boolean lastCycle) {
9999
*/
100100
@Nullable
101101
SourceFile getBefore(Path sourcePath);
102+
103+
/**
104+
* Called when a recipe's {@code generate()} produces a file whose path collides with
105+
* an existing source file or with another generated file. The generated file is silently
106+
* dropped. Implementations may override this to detect the collision (e.g. the test
107+
* framework raises an assertion failure).
108+
*
109+
* @param sourcePath The colliding source path.
110+
* @param existingFile {@code true} if the collision is with an existing source file,
111+
* {@code false} if it's with another generated file.
112+
*/
113+
default void onGenerateCollision(Path sourcePath, boolean existingFile) {
114+
}
102115
}

rewrite-core/src/main/java/org/openrewrite/Recipe.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,6 @@ public String getInstanceNameSuffix() {
203203
* A set of strings used for categorizing related recipes. For example
204204
* "testing", "junit", "spring". Any individual tag should consist of a
205205
* single word, all lowercase.
206-
*
207-
* @return The tags.
208206
*/
209207
@Getter
210208
final Set<String> tags = emptySet();
@@ -225,11 +223,21 @@ public final RecipeDescriptor getDescriptor() {
225223

226224
protected RecipeDescriptor createRecipeDescriptor() {
227225
List<OptionDescriptor> options = getOptionDescriptors();
228-
ArrayList<RecipeDescriptor> recipeList1 = new ArrayList<>();
229-
for (Recipe next : getRecipeList()) {
230-
recipeList1.add(next.getDescriptor());
226+
List<RecipeDescriptor> preconditionDescriptors = emptyList();
227+
if (this instanceof RecipePreconditions) {
228+
RecipePreconditions recipeWithPreconditions = (RecipePreconditions) this;
229+
List<Recipe> preconditions = recipeWithPreconditions.getPreconditions();
230+
preconditionDescriptors = new ArrayList<>(preconditions.size());
231+
for (Recipe precondition : preconditions) {
232+
preconditionDescriptors.add(precondition.getDescriptor());
233+
}
234+
}
235+
236+
List<Recipe> recipeList = getRecipeList();
237+
List<RecipeDescriptor> recipeDescriptors = new ArrayList<>(recipeList.size());
238+
for (Recipe next : recipeList) {
239+
recipeDescriptors.add(next.getDescriptor());
231240
}
232-
recipeList1.trimToSize();
233241

234242
URI recipeSource;
235243
try {
@@ -239,7 +247,7 @@ protected RecipeDescriptor createRecipeDescriptor() {
239247
}
240248

241249
return new RecipeDescriptor(getName(), getDisplayName(), getInstanceName(), getDescription(), getTags(),
242-
getEstimatedEffortPerOccurrence(), options, recipeList1, getDataTableDescriptors(),
250+
getEstimatedEffortPerOccurrence(), options, preconditionDescriptors, recipeDescriptors, getDataTableDescriptors(),
243251
getMaintainers(), getContributors(), getExamples(), recipeSource);
244252
}
245253

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

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@
2929
import org.openrewrite.internal.RecipeLoader;
3030
import org.openrewrite.style.NamedStyles;
3131

32+
import java.io.IOException;
33+
import java.io.UncheckedIOException;
3234
import java.lang.reflect.Constructor;
3335
import java.lang.reflect.Modifier;
34-
import java.nio.file.Path;
36+
import java.nio.file.*;
37+
import java.nio.file.attribute.BasicFileAttributes;
3538
import java.util.*;
39+
import java.util.jar.JarEntry;
40+
import java.util.jar.JarOutputStream;
3641

42+
import static java.nio.file.Files.*;
3743
import static java.util.Collections.emptyList;
3844
import static java.util.Collections.emptyMap;
3945

@@ -97,26 +103,73 @@ public ClasspathScanningLoader(Properties properties, ClassLoader classLoader) {
97103
/**
98104
* Construct a ClasspathScanningLoader as used from `Environment.scanJar` for
99105
* `MavenRecipeBundleReader.marketplaceFromClasspathScan`.
106+
* Supports both jar files and directories containing class files.
100107
*/
101108
public ClasspathScanningLoader(Path jar, Properties properties, Collection<? extends ResourceLoader> dependencyResourceLoaders, ClassLoader classLoader) {
102109
this.classLoader = classLoader;
103110
this.recipeLoader = new RecipeLoader(classLoader);
104-
String jarName = jar.toFile().getName();
105111

106112
this.performScan = () -> {
107-
scanClasses(new ClassGraph()
113+
Path jarPath;
114+
if (isDirectory(jar)) {
115+
try {
116+
jarPath = createTempJarFromDirectory(jar);
117+
} catch (IOException e) {
118+
throw new UncheckedIOException("Failed to create temporary jar from directory: " + jar, e);
119+
}
120+
} else {
121+
jarPath = jar;
122+
}
123+
124+
String jarName = jarPath.toFile().getName();
125+
ClassGraph classGraph = new ClassGraph()
126+
.overrideClasspath(jarPath.toString())
108127
.acceptJars(jarName)
109-
.ignoreParentClassLoaders()
110-
.overrideClassLoaders(classLoader), classLoader);
128+
.overrideClassLoaders(classLoader);
111129

112-
scanYaml(new ClassGraph()
130+
ClassGraph yamlGraph = new ClassGraph()
131+
.overrideClasspath(jarPath.toString())
113132
.acceptJars(jarName)
114-
.ignoreParentClassLoaders()
115133
.overrideClassLoaders(classLoader)
116-
.acceptPaths("META-INF/rewrite"), properties, dependencyResourceLoaders, classLoader);
134+
.acceptPaths("META-INF/rewrite");
135+
136+
scanClasses(classGraph, classLoader);
137+
scanYaml(yamlGraph, properties, dependencyResourceLoaders, classLoader);
117138
};
118139
}
119140

141+
/**
142+
* Creates a temporary jar file containing all files from the given directory.
143+
*/
144+
private static Path createTempJarFromDirectory(Path directory) throws IOException {
145+
Path tempJar = createTempFile("recipe-scan-", ".jar");
146+
tempJar.toFile().deleteOnExit();
147+
148+
try (JarOutputStream jos = new JarOutputStream(newOutputStream(tempJar))) {
149+
walkFileTree(directory, new SimpleFileVisitor<Path>() {
150+
@Override
151+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
152+
String entryName = directory.relativize(file).toString().replace('\\', '/');
153+
jos.putNextEntry(new JarEntry(entryName));
154+
copy(file, jos);
155+
jos.closeEntry();
156+
return FileVisitResult.CONTINUE;
157+
}
158+
159+
@Override
160+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
161+
if (!dir.equals(directory)) {
162+
String entryName = directory.relativize(dir).toString().replace('\\', '/') + "/";
163+
jos.putNextEntry(new JarEntry(entryName));
164+
jos.closeEntry();
165+
}
166+
return FileVisitResult.CONTINUE;
167+
}
168+
});
169+
}
170+
return tempJar;
171+
}
172+
120173
/**
121174
* Construct a ClasspathScanningLoader to load Yaml categories and recipes from the runtime classpath, as part of
122175
* running tests or inferring local recipe categories.

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import static org.openrewrite.Validated.invalid;
3333

3434
@RequiredArgsConstructor
35-
public class DeclarativeRecipe extends ScanningRecipe<DeclarativeRecipe.Accumulator> {
35+
public class DeclarativeRecipe extends ScanningRecipe<DeclarativeRecipe.Accumulator> implements RecipePreconditions {
3636
@Getter
3737
private final String name;
3838

@@ -187,7 +187,6 @@ private void registerNestedScanningRecipes(Recipe recipe, Accumulator acc, Execu
187187
@Override
188188
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
189189
return new TreeVisitor<Tree, ExecutionContext>() {
190-
@SuppressWarnings({"rawtypes", "unchecked"})
191190
@Override
192191
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
193192
for (Recipe precondition : preconditions) {
@@ -251,7 +250,7 @@ public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) {
251250

252251
@EqualsAndHashCode(callSuper = false)
253252
@Value
254-
static class BellwetherDecoratedRecipe extends Recipe implements DelegatingRecipe {
253+
static class BellwetherDecoratedRecipe extends Recipe implements DelegatingRecipe, RecipePreconditions {
255254

256255
DeclarativeRecipe.PreconditionBellwether bellwether;
257256
Recipe delegate;
@@ -350,11 +349,19 @@ public Validated<Object> validate(ExecutionContext ctx) {
350349
public Collection<Validated<Object>> validateAll(ExecutionContext ctx, Collection<Validated<Object>> acc) {
351350
return delegate.validateAll(ctx, acc);
352351
}
352+
353+
@Override
354+
public List<Recipe> getPreconditions() {
355+
if (delegate instanceof RecipePreconditions) {
356+
return ((RecipePreconditions) delegate).getPreconditions();
357+
}
358+
return emptyList();
359+
}
353360
}
354361

355362
@Value
356363
@EqualsAndHashCode(callSuper = false)
357-
static class BellwetherDecoratedScanningRecipe<T> extends ScanningRecipe<T> implements DelegatingRecipe {
364+
static class BellwetherDecoratedScanningRecipe<T> extends ScanningRecipe<T> implements DelegatingRecipe, RecipePreconditions {
358365

359366
DeclarativeRecipe.PreconditionBellwether bellwether;
360367
ScanningRecipe<T> delegate;
@@ -468,6 +475,14 @@ public Validated<Object> validate(ExecutionContext ctx) {
468475
public Collection<Validated<Object>> validateAll(ExecutionContext ctx, Collection<Validated<Object>> acc) {
469476
return delegate.validateAll(ctx, acc);
470477
}
478+
479+
@Override
480+
public List<Recipe> getPreconditions() {
481+
if (delegate instanceof RecipePreconditions) {
482+
return ((RecipePreconditions) delegate).getPreconditions();
483+
}
484+
return emptyList();
485+
}
471486
}
472487

473488
@Override
@@ -589,13 +604,17 @@ private static class LazyLoadedRecipe extends Recipe {
589604

590605
@Override
591606
protected RecipeDescriptor createRecipeDescriptor() {
592-
List<RecipeDescriptor> recipeList = new ArrayList<>();
593-
for (Recipe childRecipe : getRecipeList()) {
594-
recipeList.add(childRecipe.getDescriptor());
607+
List<RecipeDescriptor> preconditionDescriptors = new ArrayList<>();
608+
for (Recipe childRecipe : preconditions) {
609+
preconditionDescriptors.add(childRecipe.getDescriptor());
610+
}
611+
List<RecipeDescriptor> recipeDescriptors = new ArrayList<>();
612+
for (Recipe childRecipe : recipeList) {
613+
recipeDescriptors.add(childRecipe.getDescriptor());
595614
}
596615
return new RecipeDescriptor(getName(), getDisplayName(), getInstanceName(), getDescription() != null ? getDescription() : "",
597616
getTags(), getEstimatedEffortPerOccurrence(),
598-
emptyList(), recipeList, getDataTableDescriptors(), getMaintainers(), getContributors(),
617+
emptyList(), preconditionDescriptors, recipeDescriptors, getDataTableDescriptors(), getMaintainers(), getContributors(),
599618
getExamples(), source);
600619
}
601620

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ public Builder scanYamlResources() {
319319
}
320320

321321
/**
322-
* @param jar A path to a jar file to scan.
322+
* @param jar A path to a jar file or directory containing class files to scan.
323323
* @param classLoader A classloader that is populated with the transitive dependencies of the jar.
324324
* @return This builder.
325325
*/

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.openrewrite.config;
1717

18+
import com.fasterxml.jackson.annotation.JsonCreator;
19+
import lombok.AllArgsConstructor;
1820
import lombok.EqualsAndHashCode;
1921
import lombok.Value;
2022
import lombok.With;
@@ -36,6 +38,7 @@
3638

3739
@Value
3840
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
41+
@AllArgsConstructor(onConstructor = @__(@JsonCreator))
3942
public class RecipeDescriptor {
4043
@EqualsAndHashCode.Include
4144
String name;
@@ -57,6 +60,9 @@ public class RecipeDescriptor {
5760
@EqualsAndHashCode.Include
5861
List<OptionDescriptor> options;
5962

63+
@With
64+
List<RecipeDescriptor> preconditions;
65+
6066
@With
6167
List<RecipeDescriptor> recipeList;
6268

@@ -73,6 +79,16 @@ public class RecipeDescriptor {
7379
@Deprecated
7480
URI source;
7581

82+
@Deprecated
83+
public RecipeDescriptor(String name, String displayName, String instanceName, String description,
84+
Set<String> tags, @Nullable Duration estimatedEffortPerOccurrence,
85+
List<OptionDescriptor> options, List<RecipeDescriptor> recipeList,
86+
List<DataTableDescriptor> dataTables, List<Maintainer> maintainers,
87+
List<Contributor> contributors, List<RecipeExample> examples, URI source) {
88+
this(name, displayName, instanceName, description, tags, estimatedEffortPerOccurrence,
89+
options, emptyList(), recipeList, dataTables, maintainers, contributors, examples, source);
90+
}
91+
7692
/**
7793
* @param env Provides a source of category descriptors to build category names from more
7894
* than just the name segments.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2026 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.openrewrite.Recipe;
19+
20+
import java.util.List;
21+
22+
/**
23+
* Indicates that a recipe exposes a list of recipes used as preconditions.
24+
* This is purely informational and not taken into consideration by recipe execution.
25+
* Imperative recipes interact with preconditions as implementation details of their visitor(s).
26+
* There is no reason for an imperative recipe to implement this, it will not affect the behavior of the recipe in any way.
27+
*/
28+
public interface RecipePreconditions {
29+
List<Recipe> getPreconditions();
30+
}

rewrite-core/src/main/java/org/openrewrite/marketplace/RecipeClassLoader.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.net.MalformedURLException;
2323
import java.net.URL;
2424
import java.net.URLClassLoader;
25+
import java.nio.file.Files;
2526
import java.nio.file.Path;
2627
import java.util.*;
2728
import java.util.stream.Stream;
@@ -215,14 +216,23 @@ protected List<String> getAdditionalParentDelegatedPackages() {
215216

216217
/**
217218
* Convert paths to URL array for URLClassLoader.
219+
* Directories must have URLs ending with '/' for URLClassLoader to recognize them.
218220
*/
219221
public static URL[] getUrls(@Nullable Path recipeJar, List<Path> classpath) {
220222
return Stream.concat(classpath.stream(), Stream.of(recipeJar))
221223
.filter(Objects::nonNull)
222-
.map(Path::toUri)
223-
.map(uri -> {
224+
.map(path -> {
224225
try {
225-
return uri.toURL();
226+
if (Files.isDirectory(path)) {
227+
// For directories, ensure URL ends with '/'
228+
String urlString = path.toUri().toURL().toString();
229+
if (!urlString.endsWith("/")) {
230+
urlString += "/";
231+
}
232+
return new URL(urlString);
233+
} else {
234+
return path.toUri().toURL();
235+
}
226236
} catch (MalformedURLException e) {
227237
throw new UncheckedIOException(e);
228238
}

0 commit comments

Comments
 (0)