From 4f0f6666df8da6854807c1240debeb0b0b626221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20Schn=C3=A9ider?= Date: Wed, 26 Feb 2025 08:39:53 -0500 Subject: [PATCH] Rewrite RPC (#5050) --- rewrite-core/build.gradle.kts | 3 + .../main/java/org/openrewrite/DataTable.java | 84 ++++--- .../DelegatingExecutionContext.java | 8 + .../org/openrewrite/ExecutionContext.java | 80 +++++- .../openrewrite/FindCollidingSourceFiles.java | 1 - .../openrewrite/InMemoryExecutionContext.java | 38 ++- .../java/org/openrewrite/LargeSourceSet.java | 2 +- .../org/openrewrite/ListRuntimeClasspath.java | 11 +- .../src/main/java/org/openrewrite/Recipe.java | 18 +- .../java/org/openrewrite/RecipeScheduler.java | 8 + .../java/org/openrewrite/ScanningRecipe.java | 5 +- .../main/java/org/openrewrite/Validated.java | 7 +- .../openrewrite/config/DeclarativeRecipe.java | 2 +- .../openrewrite/config/RecipeDescriptor.java | 3 + .../config/YamlResourceLoader.java | 56 ++--- .../openrewrite/internal/ObjectMappers.java | 44 ++++ .../openrewrite/internal/RecipeLoader.java | 58 +++++ .../java/org/openrewrite/rpc/ParserInput.java | 28 +++ .../openrewrite/rpc/RecipeRpcException.java | 22 ++ .../java/org/openrewrite/rpc/Reference.java | 65 +++++ .../java/org/openrewrite/rpc/RewriteRpc.java | 228 ++++++++++++++++++ .../java/org/openrewrite/rpc/RpcCodec.java | 45 ++++ .../org/openrewrite/rpc/RpcObjectData.java | 115 +++++++++ .../org/openrewrite/rpc/RpcReceiveQueue.java | 159 ++++++++++++ .../java/org/openrewrite/rpc/RpcRecipe.java | 138 +++++++++++ .../org/openrewrite/rpc/RpcSendQueue.java | 190 +++++++++++++++ .../org/openrewrite/rpc/package-info.java | 19 ++ .../org/openrewrite/rpc/request/Generate.java | 64 +++++ .../rpc/request/GenerateResponse.java | 21 ++ .../openrewrite/rpc/request/GetObject.java | 101 ++++++++ .../rpc/request/GetObjectResponse.java | 23 ++ .../rpc/request/GetRecipesResponse.java | 23 ++ .../rpc/request/PrepareRecipe.java | 47 ++++ .../rpc/request/PrepareRecipeResponse.java | 37 +++ .../org/openrewrite/rpc/request/Print.java | 33 +++ .../openrewrite/rpc/request/RpcRequest.java | 19 ++ .../org/openrewrite/rpc/request/Visit.java | 164 +++++++++++++ .../rpc/request/VisitResponse.java | 23 ++ .../openrewrite/rpc/request/package-info.java | 19 ++ .../scheduling/RecipeRunCycle.java | 3 +- .../openrewrite/scheduling/RecipeStack.java | 7 +- .../scheduling/WatchableExecutionContext.java | 39 +-- .../openrewrite/search/FindCommitters.java | 1 - .../org/openrewrite/table/CommitsByDay.java | 4 +- .../openrewrite/table/DistinctCommitters.java | 4 +- .../java/org/openrewrite/text/PlainText.java | 39 ++- .../java/org/openrewrite/DataTableTest.java | 3 +- .../org/openrewrite/RecipeLifecycleTest.java | 62 ++--- .../openrewrite/config/CategoryTreeTest.java | 2 +- .../org/openrewrite/rpc/RewriteRpcTest.java | 182 ++++++++++++++ .../org/openrewrite/rpc/RpcSendQueueTest.java | 72 ++++++ .../gradle/table/GradleWrappersInUse.java | 5 +- .../gradle/table/JVMTestSuitesDefined.java | 9 +- .../openrewrite/java/table/MethodCalls.java | 15 +- rewrite-json/build.gradle.kts | 7 +- .../json/internal/rpc/JsonReceiver.java | 112 +++++++++ .../json/internal/rpc/JsonSender.java | 114 +++++++++ .../json/internal/rpc/package-info.java | 21 ++ .../java/org/openrewrite/json/tree/Json.java | 20 +- .../internal/rpc/JsonSendReceiveTest.java | 116 +++++++++ .../maven/table/DependenciesInUse.java | 4 +- .../maven/table/DependencyGraph.java | 5 +- .../maven/table/DependencyResolutions.java | 2 +- .../maven/table/EffectiveMavenSettings.java | 5 +- .../maven/table/ManagedDependencyGraph.java | 4 +- .../maven/table/MavenMetadataFailures.java | 3 +- .../maven/table/MavenProperties.java | 4 +- .../maven/table/MavenRepositoryOrder.java | 3 +- .../maven/table/ParentPomsInUse.java | 4 +- 69 files changed, 2659 insertions(+), 223 deletions(-) create mode 100644 rewrite-core/src/main/java/org/openrewrite/internal/ObjectMappers.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/internal/RecipeLoader.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/ParserInput.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/RecipeRpcException.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/Reference.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/RewriteRpc.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/RpcCodec.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/RpcObjectData.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/RpcReceiveQueue.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/RpcRecipe.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/RpcSendQueue.java create mode 100755 rewrite-core/src/main/java/org/openrewrite/rpc/package-info.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/Generate.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/GenerateResponse.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObject.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObjectResponse.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/GetRecipesResponse.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipe.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipeResponse.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/Print.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/RpcRequest.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/Visit.java create mode 100644 rewrite-core/src/main/java/org/openrewrite/rpc/request/VisitResponse.java create mode 100755 rewrite-core/src/main/java/org/openrewrite/rpc/request/package-info.java create mode 100644 rewrite-core/src/test/java/org/openrewrite/rpc/RewriteRpcTest.java create mode 100644 rewrite-core/src/test/java/org/openrewrite/rpc/RpcSendQueueTest.java create mode 100644 rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonReceiver.java create mode 100644 rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonSender.java create mode 100644 rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/package-info.java create mode 100644 rewrite-json/src/test/java/org/openrewrite/json/internal/rpc/JsonSendReceiveTest.java diff --git a/rewrite-core/build.gradle.kts b/rewrite-core/build.gradle.kts index cce20ae54f8..23717e13eee 100644 --- a/rewrite-core/build.gradle.kts +++ b/rewrite-core/build.gradle.kts @@ -21,6 +21,9 @@ dependencies { implementation("io.github.classgraph:classgraph:latest.release") implementation("org.yaml:snakeyaml:latest.release") + implementation("io.moderne:jsonrpc:latest.release") + implementation("org.objenesis:objenesis:latest.release") + testImplementation("org.assertj:assertj-core:latest.release") testImplementation(project(":rewrite-test")) } diff --git a/rewrite-core/src/main/java/org/openrewrite/DataTable.java b/rewrite-core/src/main/java/org/openrewrite/DataTable.java index a56ad5db870..804889d7baf 100644 --- a/rewrite-core/src/main/java/org/openrewrite/DataTable.java +++ b/rewrite-core/src/main/java/org/openrewrite/DataTable.java @@ -16,9 +16,8 @@ package org.openrewrite; import com.fasterxml.jackson.annotation.JsonIgnoreType; -import com.fasterxml.jackson.core.type.TypeReference; import lombok.Getter; -import lombok.Setter; +import lombok.RequiredArgsConstructor; import org.intellij.lang.annotations.Language; import java.lang.reflect.ParameterizedType; @@ -27,67 +26,78 @@ import java.util.concurrent.ConcurrentHashMap; /** - * @param The model type for a single row of this extract. + * @param The model type for a single row of this data table. */ @Getter -@Incubating(since = "7.35.0") @JsonIgnoreType +@RequiredArgsConstructor public class DataTable { - private final String name; - private final Class type; - @Language("markdown") private final @NlsRewrite.DisplayName String displayName; @Language("markdown") private final @NlsRewrite.Description String description; - @Setter - private boolean enabled = true; - /** - * Ignore any row insertions after this cycle. This prevents - * data table producing recipes from having to keep track of state across - * multiple cycles to prevent duplicate row entries. + * @param recipe The recipe that this data table is associated with. + * @param type The model type for a single row of this data table. + * @param name The name of this data table. + * @param displayName The display name of this data table. + * @param description The description of this data table. + * @deprecated Use {@link #DataTable(Recipe, String, String)} instead. */ - protected int maxCycle = 1; - + @SuppressWarnings("unused") + @Deprecated public DataTable(Recipe recipe, Class type, String name, - @Language("markdown") String displayName, - @Language("markdown") String description) { - this.type = type; - this.name = name; + @NlsRewrite.DisplayName @Language("markdown") String displayName, + @NlsRewrite.Description @Language("markdown") String description) { this.displayName = displayName; this.description = description; recipe.addDataTable(this); } + /** + * Construct a new data table. + * + * @param recipe The recipe that this data table is associated with. + * @param displayName The display name of this data table. + * @param description The description of this data table. + */ public DataTable(Recipe recipe, - @Language("markdown") String displayName, - @Language("markdown") String description) { - //noinspection unchecked - this.type = (Class) ((ParameterizedType) getClass().getGenericSuperclass()) - .getActualTypeArguments()[0]; - this.name = getClass().getName(); + @NlsRewrite.DisplayName @Language("markdown") String displayName, + @NlsRewrite.Description @Language("markdown") String description) { this.displayName = displayName; this.description = description; - recipe.addDataTable(this); + + // Only null when transferring DataTables over RPC. + //noinspection ConstantValue + if (recipe != null) { + recipe.addDataTable(this); + } } - @SuppressWarnings("unused") - public TypeReference> getRowsTypeReference() { - return new TypeReference>() { - }; + public Class getType() { + //noinspection unchecked + return (Class) ((ParameterizedType) getClass().getGenericSuperclass()) + .getActualTypeArguments()[0]; + } + + public String getName() { + return getClass().getName(); } public void insertRow(ExecutionContext ctx, Row row) { - if (enabled && ctx.getCycle() <= maxCycle) { - ctx.computeMessage(ExecutionContext.DATA_TABLES, row, ConcurrentHashMap::new, (extract, allDataTables) -> { - //noinspection unchecked - List dataTablesOfType = (List) allDataTables.computeIfAbsent(this, c -> new ArrayList<>()); - dataTablesOfType.add(row); - return allDataTables; - }); + // Ignore any row insertions after this cycle. This prevents + // data table producing recipes from having to keep track of state across + // multiple cycles to prevent duplicate row entries. + if (ctx.getCycle() > 1) { + return; } + ctx.computeMessage(ExecutionContext.DATA_TABLES, row, ConcurrentHashMap::new, (extract, allDataTables) -> { + //noinspection unchecked + List dataTablesOfType = (List) allDataTables.computeIfAbsent(this, c -> new ArrayList<>()); + dataTablesOfType.add(row); + return allDataTables; + }); } } diff --git a/rewrite-core/src/main/java/org/openrewrite/DelegatingExecutionContext.java b/rewrite-core/src/main/java/org/openrewrite/DelegatingExecutionContext.java index 920471daa91..b1edcf8eb7d 100644 --- a/rewrite-core/src/main/java/org/openrewrite/DelegatingExecutionContext.java +++ b/rewrite-core/src/main/java/org/openrewrite/DelegatingExecutionContext.java @@ -15,18 +15,26 @@ */ package org.openrewrite; +import lombok.Getter; import org.jspecify.annotations.Nullable; +import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; public class DelegatingExecutionContext implements ExecutionContext { + @Getter private final ExecutionContext delegate; public DelegatingExecutionContext(ExecutionContext delegate) { this.delegate = delegate; } + @Override + public @Nullable Map getMessages() { + return delegate.getMessages(); + } + @Override public void putMessage(String key, @Nullable Object value) { delegate.putMessage(key, value); diff --git a/rewrite-core/src/main/java/org/openrewrite/ExecutionContext.java b/rewrite-core/src/main/java/org/openrewrite/ExecutionContext.java index 1b0f3b83a45..422afcff48c 100644 --- a/rewrite-core/src/main/java/org/openrewrite/ExecutionContext.java +++ b/rewrite-core/src/main/java/org/openrewrite/ExecutionContext.java @@ -16,21 +16,26 @@ package org.openrewrite; import org.jspecify.annotations.Nullable; +import org.openrewrite.rpc.RpcCodec; +import org.openrewrite.rpc.RpcReceiveQueue; +import org.openrewrite.rpc.RpcSendQueue; +import org.openrewrite.rpc.request.Visit; import org.openrewrite.scheduling.RecipeRunCycle; import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Supplier; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.*; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Objects.requireNonNull; /** * Passes messages between individual visitors or parsing operations and allows errors to be propagated * back to the process controlling parsing or recipe execution. */ -public interface ExecutionContext { +public interface ExecutionContext extends RpcCodec { String CURRENT_CYCLE = "org.openrewrite.currentCycle"; String CURRENT_RECIPE = "org.openrewrite.currentRecipe"; String DATA_TABLES = "org.openrewrite.dataTables"; @@ -49,6 +54,9 @@ default Set getObservers() { return getMessage("org.openrewrite.internal.treeObservers", Collections.emptySet()); } + @Nullable + Map getMessages(); + void putMessage(String key, @Nullable Object value); @Nullable T getMessage(String key); @@ -107,4 +115,66 @@ default int getCycle() { default RecipeRunCycle getCycleDetails() { return requireNonNull(getMessage(CURRENT_CYCLE)); } + + /** + * The after state will change if any messages have changed by a call to clone in the + * {@link Visit.Handler} implementation. + */ + @Override + default void rpcSend(ExecutionContext after, RpcSendQueue q) { + // The after state will change if any messages have changed by a call to clone + q.getAndSend(after, ctx -> { + Map messages = new HashMap<>(ctx.getMessages() == null ? + emptyMap() : ctx.getMessages()); + // The remote side will manage its own recipe and cycle state. + messages.remove(CURRENT_CYCLE); + messages.remove(CURRENT_RECIPE); + messages.remove(DATA_TABLES); + return messages; + }); + + Map, List> dt = after.getMessage(DATA_TABLES); + q.getAndSendList(after, sendWholeList(dt == null ? null : dt.keySet()), DataTable::getName, null); + if (dt != null) { + for (List rowSet : dt.values()) { + q.getAndSendList(after, sendWholeList(rowSet), + row -> Integer.toString(System.identityHashCode(row)), + null); + } + + } + } + + @Override + default ExecutionContext rpcReceive(ExecutionContext before, RpcReceiveQueue q) { + Map messages = q.receive(before.getMessages()); + for (Map.Entry e : messages.entrySet()) { + before.putMessage(e.getKey(), e.getValue()); + } + + List> dataTables = q.receiveList(emptyList(), null); + //noinspection ConstantValue + if (dataTables != null) { + for (DataTable dataTable : dataTables) { + List rows = q.receiveList(emptyList(), null); + before.computeMessage(ExecutionContext.DATA_TABLES, rows, ConcurrentHashMap::new, (extract, allDataTables) -> { + //noinspection unchecked + List dataTablesOfType = (List) allDataTables.computeIfAbsent(dataTable, c -> new ArrayList<>()); + dataTablesOfType.addAll(rows); + return allDataTables; + }); + } + } + return before; + } + + static Function> sendWholeList(@Nullable Collection list) { + AtomicBoolean retrievedAfter = new AtomicBoolean(false); + return ctx -> { + if (!retrievedAfter.getAndSet(true)) { + return list == null ? null : new ArrayList<>(list); + } + return null; + }; + } } diff --git a/rewrite-core/src/main/java/org/openrewrite/FindCollidingSourceFiles.java b/rewrite-core/src/main/java/org/openrewrite/FindCollidingSourceFiles.java index f260d4e9269..79e50b168a9 100644 --- a/rewrite-core/src/main/java/org/openrewrite/FindCollidingSourceFiles.java +++ b/rewrite-core/src/main/java/org/openrewrite/FindCollidingSourceFiles.java @@ -29,7 +29,6 @@ @Value @EqualsAndHashCode(callSuper = false) public class FindCollidingSourceFiles extends ScanningRecipe { - transient CollidingSourceFiles collidingSourceFiles = new CollidingSourceFiles(this); @Override diff --git a/rewrite-core/src/main/java/org/openrewrite/InMemoryExecutionContext.java b/rewrite-core/src/main/java/org/openrewrite/InMemoryExecutionContext.java index e3de51bc336..6e86dab043f 100644 --- a/rewrite-core/src/main/java/org/openrewrite/InMemoryExecutionContext.java +++ b/rewrite-core/src/main/java/org/openrewrite/InMemoryExecutionContext.java @@ -15,6 +15,7 @@ */ package org.openrewrite; +import lombok.Getter; import org.jspecify.annotations.Nullable; import java.time.Duration; @@ -23,16 +24,17 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; -public class InMemoryExecutionContext implements ExecutionContext { - private final Map messages = new ConcurrentHashMap<>(); +public class InMemoryExecutionContext implements ExecutionContext, Cloneable { + @Getter + @Nullable + private Map messages; + private final Consumer onError; private final BiConsumer onTimeout; public InMemoryExecutionContext() { - this( - t -> { - } - ); + this(t -> { + }); } public InMemoryExecutionContext(Consumer onError) { @@ -52,15 +54,23 @@ public InMemoryExecutionContext(Consumer onError, Duration runTimeout @Override public void putMessage(String key, @Nullable Object value) { - if (value == null) { + if (value == null && messages != null) { messages.remove(key); } else { - messages.put(key, value); + if (messages == null) { + messages = new ConcurrentHashMap<>(); + } + if (value != null) { + messages.put(key, value); + } } } @Override public @Nullable T getMessage(String key) { + if (messages == null) { + messages = new ConcurrentHashMap<>(); + } //noinspection unchecked return (T) messages.get(key); } @@ -68,7 +78,7 @@ public void putMessage(String key, @Nullable Object value) { @Override public @Nullable T pollMessage(String key) { //noinspection unchecked - return (T) messages.remove(key); + return (T) (messages == null ? null : messages.remove(key)); } @Override @@ -80,4 +90,14 @@ public Consumer getOnError() { public BiConsumer getOnTimeout() { return onTimeout; } + + @SuppressWarnings("MethodDoesntCallSuperMethod") + @Override + public InMemoryExecutionContext clone() { + InMemoryExecutionContext clone = new InMemoryExecutionContext(); + clone.messages = new ConcurrentHashMap<>(messages); + clone.messages.computeIfPresent(DATA_TABLES, (key, dt) -> + new ConcurrentHashMap<>(((Map) dt))); + return clone; + } } diff --git a/rewrite-core/src/main/java/org/openrewrite/LargeSourceSet.java b/rewrite-core/src/main/java/org/openrewrite/LargeSourceSet.java index 9e6d2c6f4e6..49319615952 100644 --- a/rewrite-core/src/main/java/org/openrewrite/LargeSourceSet.java +++ b/rewrite-core/src/main/java/org/openrewrite/LargeSourceSet.java @@ -68,7 +68,7 @@ default void beforeCycle(boolean definitelyLastCycle) { * @param map A transformation on T * @return A new source set if the map function results in any changes, otherwise this source set is returned. */ - LargeSourceSet edit(UnaryOperator map); + LargeSourceSet edit(UnaryOperator<@Nullable SourceFile> map); /** * Concatenate new items. Where possible, implementations should not iterate the entire source set in order diff --git a/rewrite-core/src/main/java/org/openrewrite/ListRuntimeClasspath.java b/rewrite-core/src/main/java/org/openrewrite/ListRuntimeClasspath.java index 2a44181ebb7..66262525a09 100644 --- a/rewrite-core/src/main/java/org/openrewrite/ListRuntimeClasspath.java +++ b/rewrite-core/src/main/java/org/openrewrite/ListRuntimeClasspath.java @@ -19,7 +19,6 @@ import io.github.classgraph.Resource; import io.github.classgraph.ResourceList; import io.github.classgraph.ScanResult; -import org.jspecify.annotations.Nullable; import org.openrewrite.table.ClasspathReport; import java.net.URI; @@ -29,7 +28,7 @@ import java.util.Map; import java.util.stream.Collectors; -public class ListRuntimeClasspath extends ScanningRecipe { +public class ListRuntimeClasspath extends ScanningRecipe { transient ClasspathReport report = new ClasspathReport(this); @Override @@ -43,17 +42,17 @@ public String getDescription() { } @Override - public @Nullable Void getInitialValue(ExecutionContext ctx) { - return null; + public Integer getInitialValue(ExecutionContext ctx) { + return 0; } @Override - public TreeVisitor getScanner(Void acc) { + public TreeVisitor getScanner(Integer acc) { return TreeVisitor.noop(); } @Override - public Collection generate(Void acc, ExecutionContext ctx) { + public Collection generate(Integer acc, ExecutionContext ctx) { try (ScanResult result = new ClassGraph().scan()) { ResourceList resources = result.getResourcesWithExtension(".jar"); Map> classpathEntriesWithJarResources = resources.stream() diff --git a/rewrite-core/src/main/java/org/openrewrite/Recipe.java b/rewrite-core/src/main/java/org/openrewrite/Recipe.java index a8c7effaabc..f2fec6471e8 100644 --- a/rewrite-core/src/main/java/org/openrewrite/Recipe.java +++ b/rewrite-core/src/main/java/org/openrewrite/Recipe.java @@ -67,6 +67,7 @@ public String getJacksonPolymorphicTypeTag() { return getClass().getName(); } + @Nullable private transient RecipeDescriptor descriptor; @Nullable @@ -223,7 +224,7 @@ protected RecipeDescriptor createRecipeDescriptor() { throw new RuntimeException(e); } - return new RecipeDescriptor(getName(), getDisplayName(), getDescription(), getTags(), + return new RecipeDescriptor(getName(), getDisplayName(), getInstanceName(), getDescription(), getTags(), getEstimatedEffortPerOccurrence(), options, recipeList1, getDataTableDescriptors(), getMaintainers(), getContributors(), getExamples(), recipeSource); } @@ -245,6 +246,7 @@ private List getOptionDescriptors() { value = null; } Option option = field.getAnnotation(Option.class); + //noinspection ConstantValue if (option != null) { options.add(new OptionDescriptor(field.getName(), field.getType().getSimpleName(), @@ -310,6 +312,17 @@ public boolean causesAnotherCycle() { return false; } + /** + * At the end of a recipe run, a {@link RecipeScheduler} will call this method to allow the + * recipe to perform any cleanup or finalization tasks. This method is guaranteed to be called + * only once per run. + * + * @param ctx The recipe run execution context. + */ + @Incubating(since = "8.48.0") + public void onComplete(ExecutionContext ctx) { + } + /** * A list of recipes that run, source file by source file, * after this recipe. This method is guaranteed to be called only once @@ -361,11 +374,12 @@ public TreeVisitor getVisitor() { return TreeVisitor.noop(); } - public void addDataTable(DataTable dataTable) { + public > D addDataTable(D dataTable) { if (dataTables == null) { dataTables = new ArrayList<>(); } dataTables.add(dataTableDescriptorFromDataTable(dataTable)); + return dataTable; } public final RecipeRun run(LargeSourceSet before, ExecutionContext ctx) { diff --git a/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java b/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java index 795dad5b841..bc0bd3a686a 100644 --- a/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java +++ b/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java @@ -113,10 +113,18 @@ private LargeSourceSet runRecipeCycles(Recipe recipe, LargeSourceSet sourceSet, } } finally { recipeRunStats.flush(ctx); + recursiveOnComplete(recipe, ctxWithWatch); } return after; } + private void recursiveOnComplete(Recipe recipe, ExecutionContext ctx) { + recipe.onComplete(ctx); + for (Recipe r : recipe.getRecipeList()) { + recursiveOnComplete(r, ctx); + } + } + private boolean hasScanningRecipe(Recipe recipe) { if (recipe instanceof ScanningRecipe) { return true; diff --git a/rewrite-core/src/main/java/org/openrewrite/ScanningRecipe.java b/rewrite-core/src/main/java/org/openrewrite/ScanningRecipe.java index 549f563a260..77ab2502435 100644 --- a/rewrite-core/src/main/java/org/openrewrite/ScanningRecipe.java +++ b/rewrite-core/src/main/java/org/openrewrite/ScanningRecipe.java @@ -102,6 +102,7 @@ public T getAccumulator(Cursor cursor, ExecutionContext ctx) { public final TreeVisitor getVisitor() { return new TreeVisitor() { + @Nullable private TreeVisitor delegate; private TreeVisitor delegate(ExecutionContext ctx) { @@ -128,7 +129,9 @@ public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { }; } - // For now, ScanningRecipes do not support `*RecipeList`, as the accumulator is not evaluated for these methods + /** + * For now, ScanningRecipes do not support `getRecipeList`, as the accumulator is not evaluated for these methods + */ @Override public final List getRecipeList() { return super.getRecipeList(); diff --git a/rewrite-core/src/main/java/org/openrewrite/Validated.java b/rewrite-core/src/main/java/org/openrewrite/Validated.java index 19e3f48bbb0..35efae6bcad 100644 --- a/rewrite-core/src/main/java/org/openrewrite/Validated.java +++ b/rewrite-core/src/main/java/org/openrewrite/Validated.java @@ -15,6 +15,7 @@ */ package org.openrewrite; +import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.Nullable; import org.openrewrite.internal.StringUtils; @@ -162,7 +163,7 @@ public Iterator> iterator() { } @Override - public T getValue() { + public @NotNull T getValue() { throw new IllegalStateException("Value does not exist"); } @@ -329,7 +330,7 @@ public Optional> findAny() { } @Override - public T getValue() { + public @NotNull T getValue() { return findAny() .map(Validated::getValue) .orElseThrow(() -> new IllegalStateException("Value does not exist")); @@ -367,7 +368,7 @@ public boolean isValid() { } @Override - public T getValue() { + public @Nullable T getValue() { return right.getValue(); } diff --git a/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java b/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java index 83a6527f506..db6754f6f30 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java @@ -369,7 +369,7 @@ protected RecipeDescriptor createRecipeDescriptor() { for (Recipe childRecipe : getRecipeList()) { recipeList.add(childRecipe.getDescriptor()); } - return new RecipeDescriptor(getName(), getDisplayName(), getDescription() != null ? getDescription() : "", + return new RecipeDescriptor(getName(), getDisplayName(), getInstanceName(), getDescription() != null ? getDescription() : "", getTags(), getEstimatedEffortPerOccurrence(), emptyList(), recipeList, getDataTableDescriptors(), getMaintainers(), getContributors(), getExamples(), source); diff --git a/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java b/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java index 5365e2d6e92..72648a53534 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java @@ -37,6 +37,9 @@ public class RecipeDescriptor { @NlsRewrite.DisplayName String displayName; + @NlsRewrite.DisplayName + String instanceName; + @NlsRewrite.Description String description; diff --git a/rewrite-core/src/main/java/org/openrewrite/config/YamlResourceLoader.java b/rewrite-core/src/main/java/org/openrewrite/config/YamlResourceLoader.java index 1984d383600..6e49275db63 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/YamlResourceLoader.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/YamlResourceLoader.java @@ -27,7 +27,9 @@ import org.intellij.lang.annotations.Language; import org.jspecify.annotations.Nullable; import org.openrewrite.*; +import org.openrewrite.internal.ObjectMappers; import org.openrewrite.internal.PropertyPlaceholderHelper; +import org.openrewrite.internal.RecipeLoader; import org.openrewrite.style.NamedStyles; import org.openrewrite.style.Style; import org.yaml.snakeyaml.LoaderOptions; @@ -127,10 +129,10 @@ public YamlResourceLoader(InputStream yamlInput, * Load a declarative recipe, optionally using the specified classloader and optionally including resource loaders * for recipes from dependencies. * - * @param yamlInput Declarative recipe yaml input stream - * @param source Declarative recipe source - * @param properties Placeholder properties - * @param classLoader Optional classloader to use with jackson. If not specified, the runtime classloader will be used. + * @param yamlInput Declarative recipe yaml input stream + * @param source Declarative recipe source + * @param properties Placeholder properties + * @param classLoader Optional classloader to use with jackson. If not specified, the runtime classloader will be used. * @param dependencyResourceLoaders Optional resource loaders for recipes from dependencies * @throws UncheckedIOException On unexpected IOException */ @@ -161,24 +163,12 @@ public YamlResourceLoader(InputStream yamlInput, URI source, Properties properti Consumer mapperCustomizer) { this.source = source; this.dependencyResourceLoaders = dependencyResourceLoaders; - - mapper = JsonMapper.builder() - .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) - .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) - .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) - .build() - .registerModule(new ParameterNamesModule()) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + this.mapper = ObjectMappers.propertyBasedMapper(classLoader); + this.classLoader = classLoader; mapperCustomizer.accept(mapper); maybeAddKotlinModule(mapper); - this.classLoader = classLoader; - - if (classLoader != null) { - TypeFactory tf = TypeFactory.defaultInstance().withClassLoader(classLoader); - mapper.setTypeFactory(tf); - } try { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); @@ -279,7 +269,8 @@ public Collection listRecipes() { recipeList.get(i), recipe::addUninitialized, recipe::addUninitialized, - recipe::addValidation); + recipe::addValidation + ); } List preconditions = (List) r.get("preconditions"); if (preconditions != null) { @@ -310,19 +301,10 @@ void loadRecipe(@Language("markdown") String name, if (recipeData instanceof String) { String recipeName = (String) recipeData; try { - // first try an explicitly-declared zero-arg constructor - addRecipe.accept((Recipe) Class.forName(recipeName, true, - classLoader == null ? this.getClass().getClassLoader() : classLoader) - .getDeclaredConstructor() - .newInstance()); - } catch (ReflectiveOperationException e) { - try { - // then try jackson - addRecipe.accept(instantiateRecipe(recipeName, new HashMap<>())); - } catch (IllegalArgumentException ignored) { - // else, it's probably declarative - addLazyLoadRecipe.accept(recipeName); - } + addRecipe.accept(new RecipeLoader(classLoader).load(recipeName, null)); + } catch (IllegalArgumentException ignored) { + // it's probably declarative + addLazyLoadRecipe.accept(recipeName); } catch (NoClassDefFoundError e) { addInvalidRecipeValidation( addValidation, @@ -337,7 +319,7 @@ void loadRecipe(@Language("markdown") String name, try { if (recipeArgs instanceof Map) { try { - addRecipe.accept(instantiateRecipe(recipeName, (Map) recipeArgs)); + addRecipe.accept(new RecipeLoader(classLoader).load(recipeName, (Map) recipeArgs)); } catch (IllegalArgumentException e) { if (e.getCause() instanceof InvalidTypeIdException) { addInvalidRecipeValidation( @@ -382,14 +364,8 @@ void loadRecipe(@Language("markdown") String name, } } - private Recipe instantiateRecipe(String recipeName, Map args) throws IllegalArgumentException { - Map withJsonType = new HashMap<>(args); - withJsonType.put("@c", recipeName); - return mapper.convertValue(withJsonType, Recipe.class); - } - private void addInvalidRecipeValidation(Consumer> addValidation, String recipeName, - Object recipeArgs, String message) { + @Nullable Object recipeArgs, String message) { addValidation.accept(Validated.invalid(recipeName, recipeArgs, message)); } diff --git a/rewrite-core/src/main/java/org/openrewrite/internal/ObjectMappers.java b/rewrite-core/src/main/java/org/openrewrite/internal/ObjectMappers.java new file mode 100644 index 00000000000..29990f163ef --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/internal/ObjectMappers.java @@ -0,0 +1,44 @@ +/* + * 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.internal; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.ConstructorDetector; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.jspecify.annotations.Nullable; + +public class ObjectMappers { + private ObjectMappers() { + } + + public static ObjectMapper propertyBasedMapper(@Nullable ClassLoader classLoader) { + ClassLoader cl = classLoader == null ? ObjectMappers.class.getClassLoader() : classLoader; + ObjectMapper m = JsonMapper.builder() + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) + .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) + .build() + .registerModule(new ParameterNamesModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + TypeFactory tf = TypeFactory.defaultInstance().withClassLoader(cl); + m.setTypeFactory(tf); + return m; + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/internal/RecipeLoader.java b/rewrite-core/src/main/java/org/openrewrite/internal/RecipeLoader.java new file mode 100644 index 00000000000..1237004c772 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/internal/RecipeLoader.java @@ -0,0 +1,58 @@ +/* + * 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.internal; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Recipe; + +import java.util.HashMap; +import java.util.Map; + +public class RecipeLoader { + @Nullable + private final ClassLoader classLoader; + + @Getter + private final ObjectMapper mapper; + + public RecipeLoader(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + this.mapper = ObjectMappers.propertyBasedMapper(classLoader); + } + + public Recipe load(String recipeName, @Nullable Map recipeArgs) { + if (recipeArgs == null || recipeArgs.isEmpty()) { + try { + // first try an explicitly-declared zero-arg constructor + return (Recipe) Class.forName(recipeName, true, + classLoader == null ? this.getClass().getClassLoader() : classLoader) + .getDeclaredConstructor() + .newInstance(); + } catch (ReflectiveOperationException e) { + return instantiateRecipe(recipeName, new HashMap<>()); + } + } + return instantiateRecipe(recipeName, recipeArgs); + } + + private Recipe instantiateRecipe(String recipeName, Map args) throws IllegalArgumentException { + Map withJsonType = new HashMap<>(args); + withJsonType.put("@c", recipeName); + return mapper.convertValue(withJsonType, Recipe.class); + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/ParserInput.java b/rewrite-core/src/main/java/org/openrewrite/rpc/ParserInput.java new file mode 100644 index 00000000000..ad1de6c91b4 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/ParserInput.java @@ -0,0 +1,28 @@ +/* + * 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.rpc; + +import lombok.Value; +import org.openrewrite.FileAttributes; + +import java.nio.file.Path; + +@Value +public class ParserInput { + Path sourcePath; + String text; + FileAttributes fileAttributes; +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/RecipeRpcException.java b/rewrite-core/src/main/java/org/openrewrite/rpc/RecipeRpcException.java new file mode 100644 index 00000000000..78eca2dbc3e --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/RecipeRpcException.java @@ -0,0 +1,22 @@ +/* + * 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.rpc; + +public class RecipeRpcException extends RuntimeException { + public RecipeRpcException(String message) { + super(message); + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/Reference.java b/rewrite-core/src/main/java/org/openrewrite/rpc/Reference.java new file mode 100644 index 00000000000..4459c866274 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/Reference.java @@ -0,0 +1,65 @@ +/* + * 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.rpc; + +import lombok.Getter; +import org.jspecify.annotations.Nullable; + +import static java.util.Objects.requireNonNull; + +/** + * An instance that is passed to the remote by reference (i.e. for instances + * that are referentially deduplicated in the LST). + */ +@Getter +public class Reference { + @SuppressWarnings("AccessStaticViaInstance") + private static final ThreadLocal flyweight = new ThreadLocal<>() + .withInitial(Reference::new); + + @Nullable + private Object value; + + /** + * @param t Any instance. + * @return A reference wrapper, which assists the sender to know when to pass by reference + * rather than by value. + */ + public static Reference asRef(@Nullable Object t) { + Reference ref = flyweight.get(); + ref.value = t; + return ref; + } + + /** + * @param maybeRef A reference (or not). + * @param The type of the value. + * @return The value of the reference, or the value itself if it is not a reference. + */ + public static @Nullable T getValue(@Nullable Object maybeRef) { + // noinspection unchecked + return (T) (maybeRef instanceof Reference ? ((Reference) maybeRef).getValue() : maybeRef); + } + + /** + * @param maybeRef A reference (or not). + * @param The type of the value. + * @return The value of the reference, or the value itself if it is not a reference. + */ + public static T getValueNonNull(@Nullable Object maybeRef) { + return requireNonNull(getValue(maybeRef)); + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/RewriteRpc.java b/rewrite-core/src/main/java/org/openrewrite/rpc/RewriteRpc.java new file mode 100644 index 00000000000..a95ea84f4fd --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/RewriteRpc.java @@ -0,0 +1,228 @@ +/* + * 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.rpc; + +import io.moderne.jsonrpc.JsonRpc; +import io.moderne.jsonrpc.JsonRpcMethod; +import io.moderne.jsonrpc.JsonRpcRequest; +import org.jetbrains.annotations.VisibleForTesting; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.config.Environment; +import org.openrewrite.config.RecipeDescriptor; +import org.openrewrite.rpc.request.*; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; +import static org.openrewrite.rpc.RpcObjectData.State.END_OF_OBJECT; + +public class RewriteRpc { + private final JsonRpc jsonRpc; + + private int batchSize = 10; + private Duration timeout = Duration.ofMinutes(1); + + /** + * Keeps track of the local and remote state of objects that are used in + * visits and other operations for which incremental state sharing is useful + * between two processes. + */ + private final Map remoteObjects = new HashMap<>(); + + @VisibleForTesting + final Map localObjects = new HashMap<>(); + + private final Map remoteRefs = new IdentityHashMap<>(); + + public RewriteRpc(JsonRpc jsonRpc, Environment marketplace) { + this.jsonRpc = jsonRpc; + + Map preparedRecipes = new HashMap<>(); + Map recipeCursors = new IdentityHashMap<>(); + + jsonRpc.rpc("Visit", new Visit.Handler(localObjects, preparedRecipes, recipeCursors, + this::getObject, this::getCursor)); + jsonRpc.rpc("Generate", new Generate.Handler(localObjects, preparedRecipes, recipeCursors, + this::getObject)); + jsonRpc.rpc("GetObject", new GetObject.Handler(batchSize, remoteObjects, localObjects)); + jsonRpc.rpc("GetRecipes", new JsonRpcMethod() { + @Override + protected Object handle(Void noParams) { + return marketplace.listRecipeDescriptors(); + } + }); + jsonRpc.rpc("PrepareRecipe", new PrepareRecipe.Handler(preparedRecipes)); + jsonRpc.rpc("Print", new JsonRpcMethod() { + @Override + protected Object handle(Print request) { + Tree tree = getObject(request.getTreeId()); + Cursor cursor = getCursor(request.getCursor()); + return tree.print(new Cursor(cursor, tree)); + } + }); + + jsonRpc.bind(); + } + + public RewriteRpc batchSize(int batchSize) { + this.batchSize = batchSize; + return this; + } + + public RewriteRpc timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + + public void shutdown() { + jsonRpc.shutdown(); + } + + public

@Nullable Tree visit(SourceFile sourceFile, String visitorName, P p) { + return visit(sourceFile, visitorName, p, null); + } + + public

@Nullable Tree visit(SourceFile sourceFile, String visitorName, P p, @Nullable Cursor cursor) { + VisitResponse response = scan(sourceFile, visitorName, p, cursor); + return response.isModified() ? + getObject(sourceFile.getId().toString()) : + sourceFile; + } + + public

VisitResponse scan(SourceFile sourceFile, String visitorName, P p) { + return scan(sourceFile, visitorName, p, null); + } + + public

VisitResponse scan(SourceFile sourceFile, String visitorName, P p, + @Nullable Cursor cursor) { + // Set the local state of this tree, so that when the remote + // asks for it, we know what to send. + localObjects.put(sourceFile.getId().toString(), sourceFile); + String pId = Integer.toString(System.identityHashCode(p)); + + Object p2 = p; + while (p2 instanceof DelegatingExecutionContext) { + p2 = ((DelegatingExecutionContext) p2).getDelegate(); + } + if (p2 instanceof ExecutionContext) { + ((ExecutionContext) p2).putMessage("org.openrewrite.rpc.id", pId); + } + localObjects.put(pId, p2); + + List cursorIds = getCursorIds(cursor); + + return send("Visit", new Visit(visitorName, null, sourceFile.getId().toString(), pId, cursorIds), + VisitResponse.class); + } + + public Collection generate(String remoteRecipeId, ExecutionContext ctx) { + List generated = send("Generate", new Generate(remoteRecipeId, + Integer.toString(System.identityHashCode(ctx))), GenerateResponse.class); + if (!generated.isEmpty()) { + return generated.stream() + .map(this::getObject) + .collect(Collectors.toList()); + } + return emptyList(); + } + + public List getRecipes() { + return send("GetRecipes", null, GetRecipesResponse.class); + } + + public Recipe prepareRecipe(String id, Map options) { + PrepareRecipeResponse r = send("PrepareRecipe", new PrepareRecipe(id, options), PrepareRecipeResponse.class); + return new RpcRecipe(this, r.getId(), r.getDescriptor(), r.getEditVisitor(), r.getScanVisitor()); + } + + public String print(SourceFile tree) { + return print(tree, new Cursor(null, Cursor.ROOT_VALUE)); + } + + public String print(Tree tree, Cursor parent) { + localObjects.put(tree.getId().toString(), tree); + return send("Print", new Print(tree.getId().toString(), getCursorIds(parent)), String.class); + } + + @VisibleForTesting + @Nullable + List getCursorIds(@Nullable Cursor cursor) { + List cursorIds = null; + if (cursor != null) { + cursorIds = cursor.getPathAsStream().map(c -> { + String id = c instanceof Tree ? + ((Tree) c).getId().toString() + : Integer.toString(System.identityHashCode(c)); + localObjects.put(id, c); + return id; + }).collect(Collectors.toList()); + } + return cursorIds; + } + + @VisibleForTesting + public T getObject(String id) { + RpcReceiveQueue q = new RpcReceiveQueue(remoteRefs, () -> send("GetObject", + new GetObject(id), GetObjectResponse.class)); + Object remoteObject = q.receive(localObjects.get(id), before -> { + if (before instanceof RpcCodec) { + //noinspection unchecked + return ((RpcCodec) before).rpcReceive(before, q); + } + return before; + }); + if (q.take().getState() != END_OF_OBJECT) { + throw new IllegalStateException("Expected END_OF_OBJECT"); + } + // We are now in sync with the remote state of the object. + remoteObjects.put(id, remoteObject); + + //noinspection unchecked + return (T) remoteObject; + } + + private

P send(String method, @Nullable RpcRequest body, Class

responseType) { + try { + // TODO handle error + return jsonRpc + .send(JsonRpcRequest.newRequest(method, body)) + .get(timeout.getSeconds(), TimeUnit.SECONDS) + .getResult(responseType); + } catch (ExecutionException | TimeoutException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + @VisibleForTesting + Cursor getCursor(@Nullable List cursorIds) { + Cursor cursor = new Cursor(null, Cursor.ROOT_VALUE); + if (cursorIds != null) { + for (int i = cursorIds.size() - 1; i >= 0; i--) { + String cursorId = cursorIds.get(i); + Object cursorObject = getObject(cursorId); + remoteObjects.put(cursorId, cursorObject); + cursor = new Cursor(cursor, cursorObject); + } + } + return cursor; + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/RpcCodec.java b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcCodec.java new file mode 100644 index 00000000000..c0408917508 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcCodec.java @@ -0,0 +1,45 @@ +/* + * 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.rpc; + +/** + * A codec decomposes a value into multiple RPC {@link RpcObjectData} events, and + * on the receiving side reconstitutes the value from those events. + * + * @param The type of the value being sent and received. + */ +public interface RpcCodec { + + /** + * When the value has been determined to have been changed, this method is called + * to send the values that comprise it. + * + * @param after The value that has been either added or changed. + * @param q The send queue that is collecting {@link RpcObjectData} to send. + */ + void rpcSend(T after, RpcSendQueue q); + + /** + * When the value has been determined to have been changed, this method is called + * to receive the values that comprise it. + * + * @param before The value that has been either added or changed. In the case where it is added, + * the before state will be non-null, but will be an initialized object with + * all null fields that are expecting to be populated by this method. + * @param q The queue that is receiving {@link RpcObjectData} from a remote. + */ + T rpcReceive(T before, RpcReceiveQueue q); +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/RpcObjectData.java b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcObjectData.java new file mode 100644 index 00000000000..6e1c2cd94a4 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcObjectData.java @@ -0,0 +1,115 @@ +/* + * 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.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.ConstructorDetector; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Tree; + +import java.util.Map; + +/** + * A single piece of data in a tree, which can be a marker, leaf value, tree element, etc. + */ +@Value +public class RpcObjectData { + private static final ObjectMapper mapper = JsonMapper.builder() + // to be able to construct classes that have @Data and a single field + // see https://cowtowncoder.medium.com/jackson-2-12-most-wanted-3-5-246624e2d3d0 + .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) + .build() + .registerModules(new ParameterNamesModule(), new JavaTimeModule()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + public static final int ADDED_LIST_ITEM = -1; + + State state; + + /** + * Used to construct a new instance of the class with + * initially only the ID populated. Subsequent {@link RpcObjectData} + * messages will fill in the object fully. + */ + @Nullable + String valueType; + + /** + * Not always a {@link Tree}. This can be a marker or leaf element + * value element of a tree as well. At any rate, it's a part of + * the data modeled by a {@link Tree}. + *

+ * In the case of a {@link Tree} ADD, this is the tree ID. + */ + @Nullable + Object value; + + /** + * Used for instances that should be referentially equal in multiple parts + * of the tree (e.g. Space, some Marker types, JavaType). The first time the + * object is seen, it is transmitted with a ref ID. Subsequent references + * to the same object are transmitted with the ref ID and no value. + */ + @Nullable + Integer ref; + + public RpcObjectData(State state, @Nullable String valueType, @Nullable Object value, @Nullable Integer ref) { + this.state = state; + this.valueType = valueType; + this.value = value; + this.ref = ref; + } + + public boolean hasValue() { + return value != null; + } + + public V getValue() { + if (value instanceof Map && valueType != null) { + try { + Class valueClass = Class.forName(valueType); + + // While we know exactly what type of value we are converting to, + // Jackson will still require the '@c' field in the map when the type + // we are converting to is annotated with @JsonTypeInfo. + //noinspection unchecked + ((Map) value).put("@c", valueType); + + //noinspection unchecked + return (V) mapper.convertValue(value, valueClass); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + //noinspection DataFlowIssue,unchecked + return (V) value; + } + + public enum State { + NO_CHANGE, + ADD, + DELETE, + CHANGE, + END_OF_OBJECT + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/RpcReceiveQueue.java b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcReceiveQueue.java new file mode 100644 index 00000000000..da1b7859b1a --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcReceiveQueue.java @@ -0,0 +1,159 @@ +/* + * 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.rpc; + +import org.jspecify.annotations.Nullable; +import org.objenesis.ObjenesisStd; +import org.openrewrite.marker.Markers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +import static java.util.Objects.requireNonNull; + +public class RpcReceiveQueue { + private final List batch; + private final Map refs; + private final Supplier> pull; + + public RpcReceiveQueue(Map refs, Supplier> pull) { + this.refs = refs; + this.batch = new ArrayList<>(); + this.pull = pull; + } + + public RpcObjectData take() { + if (batch.isEmpty()) { + List data = pull.get(); + batch.addAll(data); + } + return batch.remove(0); + } + + /** + * Receive a value from the queue and apply a function to it, usually to + * convert it to a string or fetch some nested object off of it. + * + * @param before The value to apply the function to, which may be null. + * @param apply A function that is called only when before is non-null. + * @param A before value ahead of the function call. + * @param The return type of the function. This will match the type that is + * being received from the remote. + * @return The received value. To set the correct before state when the received state + * is NO_CHANGE or CHANGE, the function is applied to the before parameter, unless before + * is null in which case the before state is assumed to be null. + */ + public U receiveAndGet(@Nullable T before, Function apply) { + return receive(before == null ? null : apply.apply(before), null); + } + + public Markers receiveMarkers(Markers markers) { + return receive(markers, m -> m.withMarkers( + receiveList(m.getMarkers(), null))); + } + + /** + * Receive a simple value from the remote. + * + * @param before The before state. + * @param The type of the value being received. + * @return The received value. + */ + public T receive(@Nullable T before) { + return receive(before, null); + } + + /** + * Receive a value from the remote and, when it is an ADD or CHANGE, invoke a callback + * to receive its constituent parts. + * + * @param before The before state. + * @param onChange When the state is ADD or CHANGE, this function is called to receive the + * pieces of this value. If the callback is null, the value is assumed to + * be in the value part of the message and is deserialized directly. + * @param The type of the value being received. + * @return The received value. + */ + @SuppressWarnings("DataFlowIssue") + public T receive(@Nullable T before, @Nullable UnaryOperator onChange) { + RpcObjectData message = take(); + Integer ref = null; + switch (message.getState()) { + case NO_CHANGE: + return before; + case DELETE: + return null; + case ADD: + ref = message.getRef(); + if (refs.containsKey(ref)) { + //noinspection unchecked + return (T) refs.get(ref); + } + before = onChange == null || message.getValueType() == null ? + message.getValue() : + newObj(message.getValueType()); + // Intentional fall-through... + case CHANGE: + T after = onChange == null ? message.getValue() : onChange.apply(before); + if (ref != null) { + refs.put(ref, after); + } + return after; + default: + throw new UnsupportedOperationException("Unknown state type " + message.getState()); + } + } + + public List receiveList(@Nullable List before, @Nullable UnaryOperator onChange) { + RpcObjectData msg = take(); + switch (msg.getState()) { + case NO_CHANGE: + //noinspection DataFlowIssue + return before; + case DELETE: + //noinspection DataFlowIssue + return null; + case ADD: + before = new ArrayList<>(); + // Intentional fall-through... + case CHANGE: + msg = take(); // the next message should be a CHANGE with a list of positions + assert msg.getState() == RpcObjectData.State.CHANGE; + List positions = msg.getValue(); + List after = new ArrayList<>(positions.size()); + for (int beforeIdx : positions) { + after.add(receive(beforeIdx >= 0 ? requireNonNull(before).get(beforeIdx) : null, onChange)); + } + return after; + default: + throw new UnsupportedOperationException(msg.getState() + " is not supported for lists."); + } + } + + private static T newObj(String type) { + try { + Class clazz = Class.forName(type); + //noinspection unchecked + return (T) new ObjenesisStd().newInstance(clazz); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/RpcRecipe.java b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcRecipe.java new file mode 100644 index 00000000000..bc2337e06c7 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcRecipe.java @@ -0,0 +1,138 @@ +/* + * 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.rpc; + +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.config.RecipeDescriptor; +import org.openrewrite.config.RecipeExample; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + + +@RequiredArgsConstructor +public class RpcRecipe extends ScanningRecipe { + private final transient RewriteRpc rpc; + + /** + * The ID that the remote is using to refer to this recipe. + */ + private final String remoteId; + private final RecipeDescriptor descriptor; + private final String editVisitor; + + @Nullable + private final String scanVisitor; + + @Override + public String getName() { + return descriptor.getName(); + } + + @Override + public String getDisplayName() { + return descriptor.getDisplayName(); + } + + @Override + public String getDescription() { + return descriptor.getDescription(); + } + + @Override + public Set getTags() { + return descriptor.getTags(); + } + + @Override + public @Nullable Duration getEstimatedEffortPerOccurrence() { + return descriptor.getEstimatedEffortPerOccurrence(); + } + + @Override + public List getExamples() { + return descriptor.getExamples(); + } + + @Override + public List getContributors() { + return descriptor.getContributors(); + } + + @Override + public List getMaintainers() { + return descriptor.getMaintainers(); + } + + @Override + public Integer getInitialValue(ExecutionContext ctx) { + return 0; + } + + @Override + public TreeVisitor getScanner(Integer acc) { + if (scanVisitor == null) { + return TreeVisitor.noop(); + } + return new TreeVisitor() { + @Override + public Tree preVisit(@NonNull Tree tree, ExecutionContext ctx) { + rpc.scan((SourceFile) tree, scanVisitor, ctx); + stopAfterPreVisit(); + return tree; + } + }; + } + + @Override + public Collection generate(Integer acc, ExecutionContext ctx) { + return rpc.generate(remoteId, ctx); + } + + @Override + public TreeVisitor getVisitor(Integer acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree preVisit(@NonNull Tree tree, ExecutionContext ctx) { + Tree t = rpc.visit((SourceFile) tree, editVisitor, ctx); + stopAfterPreVisit(); + return t; + } + }; + } + + @Override + public void onComplete(ExecutionContext ctx) { + // This will merge data tables from the remote into the local context. + // + // When multiple recipes ran on the same RPC peer, they will all have been + // adding to the same ExecutionContext instance on that peer, and so really + // a CHANGE will only be returned for the first of any recipes on that peer. + // It doesn't matter which one added data table entries, because they all share + // the same view of the data tables. + String id = ctx.getMessage("org.openrewrite.rpc.id"); + if (id != null) { + rpc.getObject(id); + } + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/RpcSendQueue.java b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcSendQueue.java new file mode 100644 index 00000000000..0bf2cf9fbea --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/RpcSendQueue.java @@ -0,0 +1,190 @@ +/* + * 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.rpc; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.internal.ThrowingConsumer; +import org.openrewrite.marker.Marker; +import org.openrewrite.marker.Markers; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.openrewrite.rpc.Reference.asRef; +import static org.openrewrite.rpc.RpcObjectData.ADDED_LIST_ITEM; +import static org.openrewrite.rpc.RpcObjectData.State.*; + +public class RpcSendQueue { + private final int batchSize; + private final List batch; + private final Consumer> drain; + private final Map refs; + + private @Nullable Object before; + + public RpcSendQueue(int batchSize, ThrowingConsumer> drain, Map refs) { + this.batchSize = batchSize; + this.batch = new ArrayList<>(batchSize); + this.drain = drain; + this.refs = refs; + } + + public void put(RpcObjectData rpcObjectData) { + batch.add(rpcObjectData); + if (batch.size() == batchSize) { + flush(); + } + } + + /** + * Called whenever the batch size is reached or at the end of the tree. + */ + public void flush() { + if (batch.isEmpty()) { + return; + } + drain.accept(new ArrayList<>(batch)); + batch.clear(); + } + + public void sendMarkers(@Nullable T parent, Function markersFn) { + getAndSend(parent, t2 -> asRef(markersFn.apply(t2)), markersRef -> { + Markers markers = Reference.getValue(markersRef); + getAndSendList(markers, Markers::getMarkers, Marker::getId, null); + }); + } + + public void getAndSend(@Nullable T parent, Function value) { + getAndSend(parent, value, null); + } + + public void getAndSend(@Nullable T parent, Function value, @Nullable Consumer onChange) { + U after = value.apply(parent); + //noinspection unchecked + U before = this.before == null ? null : value.apply((T) this.before); + send(after, before, onChange == null ? null : () -> onChange.accept(after)); + } + + public void getAndSendList(@Nullable T parent, + Function> values, + Function id, + @Nullable Consumer onChange) { + List after = values.apply(parent); + //noinspection unchecked + List before = this.before == null ? null : values.apply((T) this.before); + sendList(after, before, id, onChange); + } + + public void send(@Nullable T after, @Nullable T before, @Nullable Runnable onChange) { + Object afterVal = Reference.getValue(after); + Object beforeVal = Reference.getValue(before); + + if (beforeVal == afterVal) { + put(new RpcObjectData(NO_CHANGE, null, null, null)); + } else if (beforeVal == null) { + add(after, onChange); + } else if (afterVal == null) { + put(new RpcObjectData(DELETE, null, null, null)); + } else { + put(new RpcObjectData(CHANGE, null, onChange == null ? afterVal : null, null)); + doChange(after, before, onChange); + } + } + + public void sendList(@Nullable List after, + @Nullable List before, + Function id, + @Nullable Consumer onChange) { + send(after, before, () -> { + assert after != null : "A DELETE event should have been sent."; + + Map beforeIdx = putListPositions(after, before, id); + + for (T anAfter : after) { + Integer beforePos = beforeIdx.get(id.apply(anAfter)); + Runnable onChangeRun = onChange == null ? null : () -> onChange.accept(anAfter); + if (beforePos == null) { + add(anAfter, onChangeRun); + } else { + T aBefore = before == null ? null : before.get(beforePos); + if (aBefore == anAfter) { + put(new RpcObjectData(NO_CHANGE, null, null, null)); + } else { + put(new RpcObjectData(CHANGE, null, null, null)); + doChange(anAfter, aBefore, onChangeRun); + } + } + } + }); + } + + private Map putListPositions(List after, @Nullable List before, Function id) { + Map beforeIdx = new IdentityHashMap<>(); + if (before != null) { + for (int i = 0; i < before.size(); i++) { + beforeIdx.put(id.apply(before.get(i)), i); + } + } + List positions = new ArrayList<>(); + for (T t : after) { + Integer beforePos = beforeIdx.get(id.apply(t)); + positions.add(beforePos == null ? ADDED_LIST_ITEM : beforePos); + } + put(new RpcObjectData(CHANGE, null, positions, null)); + return beforeIdx; + } + + private void add(@Nullable Object after, @Nullable Runnable onChange) { + Object afterVal = Reference.getValue(after); + Integer ref = null; + if (afterVal != null && after != afterVal /* Is a reference */) { + if (refs.containsKey(afterVal)) { + put(new RpcObjectData(ADD, getValueType(afterVal), null, refs.get(afterVal))); + // No onChange call because the remote will be using an instance from its ref cache + return; + } + ref = refs.size() + 1; + refs.put(afterVal, ref); + } + put(new RpcObjectData(ADD, getValueType(afterVal), + onChange == null ? afterVal : null, ref)); + doChange(afterVal, null, onChange); + } + + private void doChange(@Nullable Object after, @Nullable Object before, @Nullable Runnable onChange) { + if (onChange != null) { + Object lastBefore = this.before; + this.before = before; + if (after != null) { + onChange.run(); + } + this.before = lastBefore; + } + } + + private static @Nullable String getValueType(@Nullable Object after) { + if (after == null) { + return null; + } + Class type = after.getClass(); + if (type.isPrimitive() || type.getPackage().getName().startsWith("java.lang") || + type.equals(UUID.class) || Iterable.class.isAssignableFrom(type)) { + return null; + } + return type.getName(); + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/package-info.java b/rewrite-core/src/main/java/org/openrewrite/rpc/package-info.java new file mode 100755 index 00000000000..72ca39501e8 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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. + */ +@NullMarked +package org.openrewrite.rpc; + +import org.jspecify.annotations.NullMarked; diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/Generate.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Generate.java new file mode 100644 index 00000000000..55b21c200cd --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Generate.java @@ -0,0 +1,64 @@ +/* + * 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.rpc.request; + +import io.moderne.jsonrpc.JsonRpcMethod; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.openrewrite.*; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; + +@Value +public class Generate implements RpcRequest { + String id; + + /** + * An ID of the p value stored in the caller's local object cache. + */ + String p; + + @RequiredArgsConstructor + public static class Handler extends JsonRpcMethod { + private final Map localObjects; + private final Map preparedRecipes; + private final Map recipeCursors; + private final Function getObject; + + @Override + protected Object handle(Generate request) throws Exception { + ExecutionContext ctx = (ExecutionContext) getObject.apply(request.getP()); + Recipe recipe = preparedRecipes.get(request.getId()); + if (recipe instanceof ScanningRecipe) { + //noinspection unchecked + ScanningRecipe scanningRecipe = (ScanningRecipe) recipe; + Object acc = scanningRecipe.getAccumulator(recipeCursors.computeIfAbsent(recipe, + r -> new Cursor(null, Cursor.ROOT_VALUE)), ctx); + Collection generated = scanningRecipe.generate(acc, ctx); + generated.forEach(g -> localObjects.put(g.getId().toString(), g)); + return generated.stream() + .map(SourceFile::getId) + .collect(Collectors.toList()); + } + return emptyList(); + } + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/GenerateResponse.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GenerateResponse.java new file mode 100644 index 00000000000..f218bcddedb --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GenerateResponse.java @@ -0,0 +1,21 @@ +/* + * 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.rpc.request; + +import java.util.ArrayList; + +public class GenerateResponse extends ArrayList { +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObject.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObject.java new file mode 100644 index 00000000000..14e790f6d01 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObject.java @@ -0,0 +1,101 @@ +/* + * 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.rpc.request; + +import io.moderne.jsonrpc.JsonRpcMethod; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.openrewrite.rpc.RpcCodec; +import org.openrewrite.rpc.RpcObjectData; +import org.openrewrite.rpc.RpcSendQueue; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; + +import static org.openrewrite.rpc.RpcObjectData.State.DELETE; +import static org.openrewrite.rpc.RpcObjectData.State.END_OF_OBJECT; + +@Value +public class GetObject implements RpcRequest { + String id; + + @RequiredArgsConstructor + public static class Handler extends JsonRpcMethod { + private static final ExecutorService forkJoin = ForkJoinPool.commonPool(); + + private final int batchSize; + private final Map remoteObjects; + private final Map localObjects; + + private final Map>> inProgressGetRpcObjects = new ConcurrentHashMap<>(); + + /** + * Keeps track of objects that need to be referentially deduplicated, and + * the ref IDs to look them up by on the remote. + */ + private final Map localRefs = new IdentityHashMap<>(); + + @Override + protected Object handle(GetObject request) throws Exception { + Object after = localObjects.get(request.getId()); + + if (after == null) { + List deleted = new ArrayList<>(2); + deleted.add(new RpcObjectData(DELETE, null, null, null)); + deleted.add(new RpcObjectData(END_OF_OBJECT, null, null, null)); + return deleted; + } + + BlockingQueue> q = inProgressGetRpcObjects.computeIfAbsent(request.getId(), id -> { + BlockingQueue> batch = new ArrayBlockingQueue<>(1); + Object before = remoteObjects.get(id); + + RpcSendQueue sendQueue = new RpcSendQueue(batchSize, batch::put, localRefs); + forkJoin.submit(() -> { + try { + Runnable onChange = after instanceof RpcCodec ? () -> { + //noinspection unchecked + ((RpcCodec) after).rpcSend(after, sendQueue); + } : null; + sendQueue.send(after, before, onChange); + + // All the data has been sent, and the remote should have received + // the full tree, so update our understanding of the remote state + // of this tree. + remoteObjects.put(id, after); + } catch (Throwable ignored) { + // TODO do something with this exception + } finally { + sendQueue.put(new RpcObjectData(END_OF_OBJECT, null, null, null)); + sendQueue.flush(); + } + return 0; + }); + return batch; + }); + + List batch = q.take(); + if (batch.get(batch.size() - 1).getState() == END_OF_OBJECT) { + inProgressGetRpcObjects.remove(request.getId()); + } + + return batch; + } + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObjectResponse.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObjectResponse.java new file mode 100644 index 00000000000..983b6e8a8a8 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetObjectResponse.java @@ -0,0 +1,23 @@ +/* + * 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.rpc.request; + +import org.openrewrite.rpc.RpcObjectData; + +import java.util.ArrayList; + +public class GetObjectResponse extends ArrayList { +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetRecipesResponse.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetRecipesResponse.java new file mode 100644 index 00000000000..9059ea15347 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/GetRecipesResponse.java @@ -0,0 +1,23 @@ +/* + * 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.rpc.request; + +import org.openrewrite.config.RecipeDescriptor; + +import java.util.ArrayList; + +public class GetRecipesResponse extends ArrayList { +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipe.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipe.java new file mode 100644 index 00000000000..fc26fa888c8 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipe.java @@ -0,0 +1,47 @@ +/* + * 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.rpc.request; + +import io.moderne.jsonrpc.JsonRpcMethod; +import io.moderne.jsonrpc.internal.SnowflakeId; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.openrewrite.Recipe; +import org.openrewrite.ScanningRecipe; +import org.openrewrite.internal.RecipeLoader; + +import java.util.Map; + +@Value +public class PrepareRecipe implements RpcRequest { + String id; + Map options; + + @RequiredArgsConstructor + public static class Handler extends JsonRpcMethod { + private final Map preparedRecipes; + + @Override + protected Object handle(PrepareRecipe request) throws Exception { + Recipe recipe = new RecipeLoader(null).load(request.getId(), request.getOptions()); + String instanceId = SnowflakeId.generateId(); + preparedRecipes.put(instanceId, recipe); + return new PrepareRecipeResponse(instanceId, recipe.getDescriptor(), + "edit:" + instanceId, + recipe instanceof ScanningRecipe ? "scan:" + instanceId : null); + } + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipeResponse.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipeResponse.java new file mode 100644 index 00000000000..331920e7122 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/PrepareRecipeResponse.java @@ -0,0 +1,37 @@ +/* + * 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.rpc.request; + +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.config.RecipeDescriptor; + +import java.util.List; + +@Value +public class PrepareRecipeResponse { + /** + * The ID that the remote is using to refer to a + * specific instance of the recipe. + */ + String id; + + RecipeDescriptor descriptor; + String editVisitor; + + @Nullable + String scanVisitor; +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/Print.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Print.java new file mode 100644 index 00000000000..cef8b9a1c83 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Print.java @@ -0,0 +1,33 @@ +/* + * 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.rpc.request; + +import lombok.Value; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +@Value +public class Print implements RpcRequest { + String treeId; + + /** + * A list of IDs representing the cursor whose objects are stored in the + * caller's local object cache. + */ + @Nullable + List cursor; +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/RpcRequest.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/RpcRequest.java new file mode 100644 index 00000000000..9a3e4dc44e3 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/RpcRequest.java @@ -0,0 +1,19 @@ +/* + * 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.rpc.request; + +public interface RpcRequest { +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/Visit.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Visit.java new file mode 100644 index 00000000000..9c62f525ae3 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Visit.java @@ -0,0 +1,164 @@ +/* + * 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.rpc.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import io.moderne.jsonrpc.JsonRpcMethod; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.internal.ObjectMappers; +import org.openrewrite.scheduling.RecipeRunCycle; +import org.openrewrite.scheduling.WatchableExecutionContext; +import org.openrewrite.table.RecipeRunStats; +import org.openrewrite.table.SourcesFileErrors; +import org.openrewrite.table.SourcesFileResults; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +@Value +public class Visit implements RpcRequest { + String visitor; + + @Nullable + Map visitorOptions; + + String treeId; + + /** + * An ID of the p value stored in the caller's local object cache. + */ + String p; + + /** + * A list of IDs representing the cursor whose objects are stored in the + * caller's local object cache. + */ + @Nullable + List cursor; + + @RequiredArgsConstructor + public static class Handler extends JsonRpcMethod { + private static final ObjectMapper mapper = ObjectMappers.propertyBasedMapper(null); + + private final Map localObjects; + + private final Map preparedRecipes; + private final Map recipeCursors; + + private final Function getObject; + private final Function<@Nullable List, Cursor> getCursor; + + @Override + protected Object handle(Visit request) throws Exception { + Tree before = (Tree) getObject.apply(request.getTreeId()); + localObjects.put(before.getId().toString(), before); + + Object p = getVisitorP(request); + + //noinspection unchecked + TreeVisitor visitor = (TreeVisitor) instantiateVisitor(request, p); + + SourceFile after = (SourceFile) visitor.visit(before, p, getCursor.apply( + request.getCursor())); + if (after == null) { + localObjects.remove(before.getId().toString()); + } else { + localObjects.put(after.getId().toString(), after); + } + + maybeUpdateExecutionContext(request.getP(), p); + + return new VisitResponse(before != after); + } + + private TreeVisitor instantiateVisitor(Visit request, Object p) { + String visitorName = request.getVisitor(); + + if (visitorName.startsWith("scan:")) { + assert p instanceof ExecutionContext; + + //noinspection unchecked + ScanningRecipe recipe = (ScanningRecipe) preparedRecipes.get(visitorName.substring("scan:".length())); + Object acc = recipe.getAccumulator(recipeCursors.computeIfAbsent(recipe, r -> new Cursor(null, Cursor.ROOT_VALUE)), + (ExecutionContext) p); + return recipe.getScanner(acc); + } else if (visitorName.startsWith("edit:")) { + Recipe recipe = preparedRecipes.get(visitorName.substring("edit:".length())); + return recipe.getVisitor(); + } + + Map withJsonType = request.getVisitorOptions() == null ? + new HashMap<>() : + new HashMap<>(request.getVisitorOptions()); + withJsonType.put("@c", visitorName); + try { + Class visitorType = TypeFactory.defaultInstance().findClass(visitorName); + return (TreeVisitor) mapper.convertValue(withJsonType, visitorType); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private Object getVisitorP(Visit request) { + Object p = getObject.apply(request.getP()); + // This is likely to be reused in subsequent visits, so we keep it. + localObjects.put(request.getP(), p); + + if (p instanceof ExecutionContext) { + String visitorName = request.getVisitor(); + + if (visitorName.startsWith("scan:") || visitorName.startsWith("edit:")) { + WatchableExecutionContext ctx = new WatchableExecutionContext((ExecutionContext) p); + Recipe recipe = preparedRecipes.get(visitorName.substring( + "edit:".length() /* 'scan:' has same length*/)); + // This is really probably particular to the Java implementation, + // because we are carrying forward the legacy of cycles that are likely to be + // removed from OpenRewrite in the future. + ctx.putCycle(new RecipeRunCycle<>(recipe, 0, new Cursor(null, Cursor.ROOT_VALUE), ctx, + new RecipeRunStats(Recipe.noop()), new SourcesFileResults(Recipe.noop()), + new SourcesFileErrors(Recipe.noop()), LargeSourceSet::edit)); + ctx.putCurrentRecipe(recipe); + return ctx; + } + } + return p; + } + + /** + * If the object is an instance of WatchableExecutionContext, and it has new messages, + * clone the underlying execution context and update the local state, so that when the + * remote asks for it, we see the ExecutionContext in CHANGE state. + * + * @param pId The ID of p + * @param p An object, which may be an ExecutionContext + */ + private void maybeUpdateExecutionContext(String pId, Object p) { + if (p instanceof WatchableExecutionContext && ((WatchableExecutionContext) p).hasNewMessages()) { + ExecutionContext ctx = ((WatchableExecutionContext) p).getDelegate(); + while (ctx instanceof DelegatingExecutionContext) { + ctx = ((DelegatingExecutionContext) ctx).getDelegate(); + } + localObjects.put(pId, ((InMemoryExecutionContext) ctx).clone()); + } + } + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/VisitResponse.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/VisitResponse.java new file mode 100644 index 00000000000..560ffc6f44f --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/VisitResponse.java @@ -0,0 +1,23 @@ +/* + * 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.rpc.request; + +import lombok.Value; + +@Value +public class VisitResponse { + boolean modified; +} diff --git a/rewrite-core/src/main/java/org/openrewrite/rpc/request/package-info.java b/rewrite-core/src/main/java/org/openrewrite/rpc/request/package-info.java new file mode 100755 index 00000000000..c85b4a38b2b --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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. + */ +@NullMarked +package org.openrewrite.rpc.request; + +import org.jspecify.annotations.NullMarked; diff --git a/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java b/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java index 9351968b164..7c3b8691e37 100644 --- a/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java +++ b/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java @@ -60,7 +60,7 @@ public class RecipeRunCycle { RecipeRunStats recipeRunStats; SourcesFileResults sourcesFileResults; SourcesFileErrors errorsTable; - BiFunction, LSS> sourceSetEditor; + BiFunction, LSS> sourceSetEditor; RecipeStack allRecipeStack = new RecipeStack(); long cycleStartTime = System.nanoTime(); @@ -112,6 +112,7 @@ public LSS generateSources(LSS sourceSet) { List generatedInThisCycle = allRecipeStack.reduce(sourceSet, recipe, ctx, (acc, recipeStack) -> { Recipe recipe = recipeStack.peek(); if (recipe instanceof ScanningRecipe) { + assert acc != null; //noinspection unchecked ScanningRecipe scanningRecipe = (ScanningRecipe) recipe; // If some sources have already been generated by prior recipes, scan them now diff --git a/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeStack.java b/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeStack.java index 45894232984..8a84b4fc3bb 100644 --- a/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeStack.java +++ b/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeStack.java @@ -17,6 +17,7 @@ import lombok.Getter; import lombok.experimental.NonFinal; +import org.jspecify.annotations.Nullable; import org.openrewrite.ExecutionContext; import org.openrewrite.LargeSourceSet; import org.openrewrite.Recipe; @@ -32,6 +33,8 @@ class RecipeStack { private final Map> recipeLists = new IdentityHashMap<>(); + + @SuppressWarnings("NotNullFieldNotInitialized") private Stack> allRecipesStack; /** @@ -41,8 +44,8 @@ class RecipeStack { @Getter int recipePosition; - public T reduce(LargeSourceSet sourceSet, Recipe recipe, ExecutionContext ctx, - BiFunction, T> consumer, T acc) { + public @Nullable T reduce(LargeSourceSet sourceSet, Recipe recipe, ExecutionContext ctx, + BiFunction<@Nullable T, Stack, @Nullable T> consumer, @Nullable T acc) { init(recipe); AtomicInteger recipePosition = new AtomicInteger(0); while (!allRecipesStack.isEmpty()) { diff --git a/rewrite-core/src/main/java/org/openrewrite/scheduling/WatchableExecutionContext.java b/rewrite-core/src/main/java/org/openrewrite/scheduling/WatchableExecutionContext.java index 2185bb736c6..579a56cf907 100644 --- a/rewrite-core/src/main/java/org/openrewrite/scheduling/WatchableExecutionContext.java +++ b/rewrite-core/src/main/java/org/openrewrite/scheduling/WatchableExecutionContext.java @@ -15,16 +15,15 @@ */ package org.openrewrite.scheduling; -import lombok.RequiredArgsConstructor; import org.jspecify.annotations.Nullable; +import org.openrewrite.DelegatingExecutionContext; import org.openrewrite.ExecutionContext; -import java.util.function.BiConsumer; -import java.util.function.Consumer; +public class WatchableExecutionContext extends DelegatingExecutionContext { + public WatchableExecutionContext(ExecutionContext delegate) { + super(delegate); + } -@RequiredArgsConstructor -public class WatchableExecutionContext implements ExecutionContext { - private final ExecutionContext delegate; private boolean hasNewMessages; public boolean hasNewMessages() { @@ -37,31 +36,13 @@ public void resetHasNewMessages() { @Override public void putMessage(String key, @Nullable Object value) { - hasNewMessages = true; - delegate.putMessage(key, value); + if (value != null) { + hasNewMessages = true; + super.putMessage(key, value); + } } public void putCycle(RecipeRunCycle cycle) { - delegate.putMessage(CURRENT_CYCLE, cycle); - } - - @Override - public @Nullable T getMessage(String key) { - return delegate.getMessage(key); - } - - @Override - public @Nullable T pollMessage(String key) { - return delegate.pollMessage(key); - } - - @Override - public Consumer getOnError() { - return delegate.getOnError(); - } - - @Override - public BiConsumer getOnTimeout() { - return delegate.getOnTimeout(); + super.putMessage(CURRENT_CYCLE, cycle); } } diff --git a/rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java b/rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java index 17075dd1d59..438fb018c83 100644 --- a/rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java +++ b/rewrite-core/src/main/java/org/openrewrite/search/FindCommitters.java @@ -36,7 +36,6 @@ @Value @EqualsAndHashCode(callSuper = false) public class FindCommitters extends ScanningRecipe> { - transient DistinctCommitters committers = new DistinctCommitters(this); transient CommitsByDay commitsByDay = new CommitsByDay(this); diff --git a/rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java b/rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java index a6ef08f0abe..62150fdb2e8 100644 --- a/rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java +++ b/rewrite-core/src/main/java/org/openrewrite/table/CommitsByDay.java @@ -25,9 +25,7 @@ public class CommitsByDay extends DataTable { public CommitsByDay(Recipe recipe) { - super(recipe, - "Commits by day", - "The commit activity by day by committer."); + super(recipe, "Commits by day", "The commit activity by day by committer."); } @Value diff --git a/rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java b/rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java index 9873abe0a34..3c23231f856 100644 --- a/rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java +++ b/rewrite-core/src/main/java/org/openrewrite/table/DistinctCommitters.java @@ -25,9 +25,7 @@ public class DistinctCommitters extends DataTable { public DistinctCommitters(Recipe recipe) { - super(recipe, - "Repository committers", - "The distinct set of committers per repository."); + super(recipe, "Repository committers", "The distinct set of committers per repository."); } @Value diff --git a/rewrite-core/src/main/java/org/openrewrite/text/PlainText.java b/rewrite-core/src/main/java/org/openrewrite/text/PlainText.java index 623122d9921..841b0c73e69 100644 --- a/rewrite-core/src/main/java/org/openrewrite/text/PlainText.java +++ b/rewrite-core/src/main/java/org/openrewrite/text/PlainText.java @@ -20,11 +20,15 @@ import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.marker.Markers; +import org.openrewrite.rpc.RpcCodec; +import org.openrewrite.rpc.RpcReceiveQueue; +import org.openrewrite.rpc.RpcSendQueue; import java.lang.ref.SoftReference; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -39,7 +43,7 @@ @Value @Builder @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class PlainText implements SourceFileWithReferences, Tree { +public class PlainText implements SourceFileWithReferences, Tree, RpcCodec { @Builder.Default @With @@ -154,6 +158,39 @@ public PlainText withSnippets(@Nullable List<Snippet> snippets) { return new PlainText(id, sourcePath, markers, charsetName, charsetBomMarked, fileAttributes, checksum, text, snippets, null); } + @Override + public void rpcSend(PlainText after, RpcSendQueue q) { + q.getAndSend(after, Tree::getId); + q.sendMarkers(after, Tree::getMarkers); + q.getAndSend(after, (PlainText d) -> d.getSourcePath().toString()); + q.getAndSend(after, (PlainText d) -> d.getCharset().name()); + q.getAndSend(after, PlainText::isCharsetBomMarked); + q.getAndSend(after, PlainText::getChecksum); + q.getAndSend(after, PlainText::getFileAttributes); + q.getAndSend(after, PlainText::getText); + q.getAndSendList(after, PlainText::getSnippets, Tree::getId, snippet -> { + q.getAndSend(snippet, Tree::getId); + q.sendMarkers(snippet, Tree::getMarkers); + q.getAndSend(snippet, Snippet::getText); + }); + } + + @Override + public PlainText rpcReceive(PlainText t, RpcReceiveQueue q) { + return t.withId(UUID.fromString(q.receiveAndGet(t.getId(), UUID::toString))) + .withMarkers(q.receiveMarkers(t.getMarkers())) + .withSourcePath(Paths.get(q.receiveAndGet(t.getSourcePath(), Path::toString))) + .withCharset(Charset.forName(q.receiveAndGet(t.getCharset(), Charset::name))) + .withCharsetBomMarked(q.receive(t.isCharsetBomMarked())) + .withChecksum(q.receive(t.getChecksum())) + .<PlainText>withFileAttributes(q.receive(t.getFileAttributes())) + .withText(q.receiveAndGet(t.getText(), String::toString)) + .withSnippets(q.receiveList(t.getSnippets(), s -> s + .withId(UUID.fromString(q.receiveAndGet(s.getId(), UUID::toString))) + .withMarkers(q.receiveMarkers(s.getMarkers())) + .withText(q.receiveAndGet(s.getText(), String::toString)))); + } + @Value @With public static class Snippet implements Tree { diff --git a/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java b/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java index 3e0d7271207..ba3150ca8de 100644 --- a/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java @@ -67,8 +67,7 @@ void descriptor() { @JsonIgnoreType static class WordTable extends DataTable<WordTable.Row> { public WordTable(Recipe recipe) { - super(recipe, Row.class, WordTable.class.getName(), - "Words", "Each word in the text."); + super(recipe, "Words", "Each word in the text."); } static class Row { diff --git a/rewrite-core/src/test/java/org/openrewrite/RecipeLifecycleTest.java b/rewrite-core/src/test/java/org/openrewrite/RecipeLifecycleTest.java index 20e818da49d..eb26102117e 100644 --- a/rewrite-core/src/test/java/org/openrewrite/RecipeLifecycleTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/RecipeLifecycleTest.java @@ -87,21 +87,21 @@ void generateFile() { @Test void twoGeneratingRecipesCreateOnlyOneFile() { rewriteRun(spec -> spec.recipeFromYaml(""" - --- - type: specs.openrewrite.org/v1beta/recipe - name: test.recipe - displayName: Create twice - description: Scanning recipes later in the stack should scan files created by earlier recipes, avoiding duplicate file creation. - recipeList: - - org.openrewrite.text.CreateTextFile: - fileContents: first - relativeFileName: test.txt - overwriteExisting: false - - org.openrewrite.text.CreateTextFile: - fileContents: second - relativeFileName: test.txt - overwriteExisting: false - """, + --- + type: specs.openrewrite.org/v1beta/recipe + name: test.recipe + displayName: Create twice + description: Scanning recipes later in the stack should scan files created by earlier recipes, avoiding duplicate file creation. + recipeList: + - org.openrewrite.text.CreateTextFile: + fileContents: first + relativeFileName: test.txt + overwriteExisting: false + - org.openrewrite.text.CreateTextFile: + fileContents: second + relativeFileName: test.txt + overwriteExisting: false + """, "test.recipe" ), text(null, "first", spec -> spec.path("test.txt"))); @@ -120,7 +120,7 @@ void errorDuringScanningPhase() { .as("Exception thrown in the scanning phase should record the responsible recipe") .matches(m -> "org.openrewrite.RecipeLifecycleTest$ErrorDuringScanningPhase".equals(m.getRecipes().iterator().next().get(0).getDescriptor().getName())) ) - )); + )); } @Value @@ -364,21 +364,21 @@ void canCallImperativeRecipeWithoutArgsFromDeclarative() { } @Test - void canNotCallImperativeRecipeWithUnnecessaryArgsFromDeclarativeInTests() { - assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> - rewriteRun(spec -> spec.recipeFromYaml(""" - --- - type: specs.openrewrite.org/v1beta/recipe - name: test.recipe - displayName: Test Recipe - description: Test Recipe. - recipeList: - - org.openrewrite.NoArgRecipe: - foo: bar - """, - "test.recipe" - ), - text("Hi", "NoArgRecipeHi"))); + void canCallImperativeRecipeWithUnnecessaryArgsFromDeclarativeInTests() { + rewriteRun(spec -> spec.recipeFromYaml(""" + --- + type: specs.openrewrite.org/v1beta/recipe + name: test.recipe + displayName: Test Recipe + description: Test Recipe. + recipeList: + - org.openrewrite.NoArgRecipe: + foo: bar + """, + "test.recipe" + ), + text("Hi", "NoArgRecipeHi") + ); } @Test diff --git a/rewrite-core/src/test/java/org/openrewrite/config/CategoryTreeTest.java b/rewrite-core/src/test/java/org/openrewrite/config/CategoryTreeTest.java index cf22f437891..96b126e2a6f 100644 --- a/rewrite-core/src/test/java/org/openrewrite/config/CategoryTreeTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/config/CategoryTreeTest.java @@ -111,7 +111,7 @@ void putRecipe() { private static RecipeDescriptor recipeDescriptor(String packageName) { return new RecipeDescriptor(packageName + ".MyRecipe", - "My recipe", "", emptySet(), null, emptyList(), + "My recipe", "", "", emptySet(), null, emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), URI.create("https://openrewrite.org")); } diff --git a/rewrite-core/src/test/java/org/openrewrite/rpc/RewriteRpcTest.java b/rewrite-core/src/test/java/org/openrewrite/rpc/RewriteRpcTest.java new file mode 100644 index 00000000000..9be230ed4f5 --- /dev/null +++ b/rewrite-core/src/test/java/org/openrewrite/rpc/RewriteRpcTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2025 the original author or authors. + * <p> + * 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 + * <p> + * https://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.rpc; + +import io.moderne.jsonrpc.JsonRpc; +import io.moderne.jsonrpc.handler.HeaderDelimitedMessageHandler; +import io.moderne.jsonrpc.handler.TraceMessageHandler; +import lombok.SneakyThrows; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openrewrite.*; +import org.openrewrite.config.Environment; +import org.openrewrite.table.TextMatches; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.text.PlainText; +import org.openrewrite.text.PlainTextVisitor; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.test.RewriteTest.toRecipe; +import static org.openrewrite.test.SourceSpecs.text; + +class RewriteRpcTest implements RewriteTest { + Environment env = Environment.builder() + .scanRuntimeClasspath("org.openrewrite.text") + .build(); + + RewriteRpc server; + RewriteRpc client; + + @BeforeEach + void before() throws IOException { + PipedOutputStream serverOut = new PipedOutputStream(); + PipedOutputStream clientOut = new PipedOutputStream(); + PipedInputStream serverIn = new PipedInputStream(clientOut); + PipedInputStream clientIn = new PipedInputStream(serverOut); + + JsonRpc serverJsonRpc = new JsonRpc(new TraceMessageHandler("server", + new HeaderDelimitedMessageHandler(serverIn, serverOut))); + server = new RewriteRpc(serverJsonRpc, env).batchSize(1).timeout(Duration.ofMinutes(10)); + + JsonRpc clientJsonRpc = new JsonRpc(new TraceMessageHandler("client", + new HeaderDelimitedMessageHandler(clientIn, clientOut))); + client = new RewriteRpc(clientJsonRpc, env).batchSize(1).timeout(Duration.ofMinutes(10)); + } + + @AfterEach + void after() { + server.shutdown(); + client.shutdown(); + } + + @Test + void sendReceiveExecutionContext() { + InMemoryExecutionContext ctx = new InMemoryExecutionContext(); + ctx.putMessage("key", "value"); + + client.localObjects.put("123", ctx); + InMemoryExecutionContext received = server.getObject("123"); + assertThat(received.<String>getMessage("key")).isEqualTo("value"); + } + + @Test + void sendReceiveIdempotence() { + rewriteRun( + spec -> spec.recipe(toRecipe(() -> new TreeVisitor<>() { + @SneakyThrows + @Override + public Tree preVisit(@NonNull Tree tree, ExecutionContext ctx) { + Tree t = server.visit((SourceFile) tree, ChangeText.class.getName(), 0); + stopAfterPreVisit(); + return t; + } + })), + text( + "Hello Jon!", + "Hello World!" + ) + ); + } + + @Test + void print() { + rewriteRun( + text( + "Hello Jon!", + spec -> spec.beforeRecipe(text -> + assertThat(server.print(text)).isEqualTo("Hello Jon!")) + ) + ); + } + + @Test + void getRecipes() { + assertThat(server.getRecipes()).isNotEmpty(); + } + + @Test + void prepareRecipe() { + Recipe recipe = server.prepareRecipe("org.openrewrite.text.Find", + Map.of("find", "hello")); + assertThat(recipe.getDescriptor().getDisplayName()).isEqualTo("Find text"); + } + + @Test + void runRecipe() { + CountDownLatch latch = new CountDownLatch(1); + rewriteRun( + spec -> spec + .recipe(server.prepareRecipe("org.openrewrite.text.Find", + Map.of("find", "hello"))) + .validateRecipeSerialization(false) + .dataTable(TextMatches.Row.class, rows -> { + assertThat(rows).contains(new TextMatches.Row( + "hello.txt", "~~>Hello Jon!")); + latch.countDown(); + }), + text( + "Hello Jon!", + "~~>Hello Jon!", + spec -> spec.path("hello.txt") + ) + ); + + assertThat(latch.getCount()).isEqualTo(0); + } + + @Test + void runScanningRecipeThatGenerates() { + rewriteRun( + spec -> spec + .recipe(server.prepareRecipe("org.openrewrite.text.CreateTextFile", + Map.of("fileContents", "hello", "relativeFileName", "hello.txt"))) + .validateRecipeSerialization(false), + text( + null, + "hello", + spec -> spec.path("hello.txt") + ) + ); + } + + @Test + void getCursor() { + Cursor parent = new Cursor(null, Cursor.ROOT_VALUE); + Cursor c1 = new Cursor(parent, 0); + Cursor c2 = new Cursor(c1, 1); + + Cursor clientC2 = client.getCursor(server.getCursorIds(c2)); + assertThat(clientC2.<Integer>getValue()).isEqualTo(1); + assertThat(clientC2.getParentOrThrow().<Integer>getValue()).isEqualTo(0); + assertThat(clientC2.getParentOrThrow(2).<String>getValue()).isEqualTo(Cursor.ROOT_VALUE); + } + + static class ChangeText extends PlainTextVisitor<Integer> { + @Override + public PlainText visitText(PlainText text, Integer p) { + return text.withText("Hello World!"); + } + } +} diff --git a/rewrite-core/src/test/java/org/openrewrite/rpc/RpcSendQueueTest.java b/rewrite-core/src/test/java/org/openrewrite/rpc/RpcSendQueueTest.java new file mode 100644 index 00000000000..e2252f35f7d --- /dev/null +++ b/rewrite-core/src/test/java/org/openrewrite/rpc/RpcSendQueueTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2025 the original author or authors. + * <p> + * 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 + * <p> + * https://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.rpc; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RpcSendQueueTest { + + @Test + void sendList() throws InterruptedException { + List<String> before = List.of("A", "B", "C", "D"); + List<String> after = List.of("A", "E", "F", "C"); + + CountDownLatch latch = new CountDownLatch(1); + RpcSendQueue q = new RpcSendQueue(10, t -> { + assertThat(t).containsExactly( + new RpcObjectData(RpcObjectData.State.CHANGE, null, null, null), + new RpcObjectData(RpcObjectData.State.CHANGE, null, List.of(0, -1, -1, 2), null), + new RpcObjectData(RpcObjectData.State.NO_CHANGE, null, null, null) /* A */, + new RpcObjectData(RpcObjectData.State.ADD, null, "E", null), + new RpcObjectData(RpcObjectData.State.ADD, null, "F", null), + new RpcObjectData(RpcObjectData.State.NO_CHANGE, null, null, null) /* C */ + ); + latch.countDown(); + }, new HashMap<>()); + + q.sendList(after, before, Function.identity(), null); + q.flush(); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + } + + @Test + void emptyList() throws InterruptedException { + List<String> after = List.of(); + + CountDownLatch latch = new CountDownLatch(1); + RpcSendQueue q = new RpcSendQueue(10, t -> { + assertThat(t).containsExactly( + new RpcObjectData(RpcObjectData.State.ADD, null, null, null), + new RpcObjectData(RpcObjectData.State.CHANGE, null, List.of(), null) + ); + latch.countDown(); + }, new HashMap<>()); + + q.sendList(after, null, Function.identity(), null); + q.flush(); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + } +} diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/GradleWrappersInUse.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/GradleWrappersInUse.java index 54cb88e95f8..d5c3c76e39d 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/GradleWrappersInUse.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/GradleWrappersInUse.java @@ -24,10 +24,7 @@ @JsonIgnoreType public class GradleWrappersInUse extends DataTable<GradleWrappersInUse.Row> { public GradleWrappersInUse(Recipe recipe) { - super(recipe, Row.class, - GradleWrappersInUse.class.getName(), - "Gradle wrappers in use", - "Gradle wrappers in use."); + super(recipe, "Gradle wrappers in use", "Gradle wrappers in use."); } @Value diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/JVMTestSuitesDefined.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/JVMTestSuitesDefined.java index fbaafed4186..38a0eab8d3d 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/JVMTestSuitesDefined.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/table/JVMTestSuitesDefined.java @@ -24,17 +24,14 @@ @JsonIgnoreType public class JVMTestSuitesDefined extends DataTable<JVMTestSuitesDefined.Row> { public JVMTestSuitesDefined(Recipe recipe) { - super(recipe, Row.class, - JVMTestSuitesDefined.class.getName(), - "JVMTestSuites defined", - "JVMTestSuites defined."); + super(recipe, "`JVMTestSuites` ", "The Gradle `JVMTestSuites` that are configured in a build."); } @Value public static class Row { - @Column(displayName = "JVMTestSuite name", - description = "Name of the defined JVMTestSuite.") + @Column(displayName = "`JVMTestSuite` name", + description = "Name of the defined `JVMTestSuite`.") String name; } } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/table/MethodCalls.java b/rewrite-java/src/main/java/org/openrewrite/java/table/MethodCalls.java index 48a71ace298..eb5e3a1585e 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/table/MethodCalls.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/table/MethodCalls.java @@ -16,18 +16,20 @@ package org.openrewrite.java.table; import com.fasterxml.jackson.annotation.JsonIgnoreType; +import lombok.Setter; import lombok.Value; import org.openrewrite.Column; import org.openrewrite.DataTable; +import org.openrewrite.ExecutionContext; import org.openrewrite.Recipe; @JsonIgnoreType +@Setter public class MethodCalls extends DataTable<MethodCalls.Row> { + private transient boolean enabled = true; public MethodCalls(Recipe recipe) { - super(recipe, - "Method calls", - "The text of matching method invocations."); + super(recipe, "Method calls", "The text of matching method invocations."); } @Value @@ -52,4 +54,11 @@ public static class Row { description = "The argument types of the method call.") String argumentTypes; } + + @Override + public void insertRow(ExecutionContext ctx, Row row) { + if (enabled) { + super.insertRow(ctx, row); + } + } } diff --git a/rewrite-json/build.gradle.kts b/rewrite-json/build.gradle.kts index 358fbf91134..5e4f823c059 100755 --- a/rewrite-json/build.gradle.kts +++ b/rewrite-json/build.gradle.kts @@ -6,9 +6,9 @@ tasks.register<JavaExec>("generateAntlrSources") { mainClass.set("org.antlr.v4.Tool") args = listOf( - "-o", "src/main/java/org/openrewrite/json/internal/grammar", - "-package", "org.openrewrite.json.internal.grammar", - "-visitor" + "-o", "src/main/java/org/openrewrite/json/internal/grammar", + "-package", "org.openrewrite.json.internal.grammar", + "-visitor" ) + fileTree("src/main/antlr").matching { include("**/*.g4") }.map { it.path } classpath = sourceSets["main"].runtimeClasspath @@ -26,4 +26,5 @@ dependencies { testImplementation(project(":rewrite-test")) testImplementation(project(":rewrite-yaml")) + testImplementation("io.moderne:jsonrpc:latest.release") } diff --git a/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonReceiver.java b/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonReceiver.java new file mode 100644 index 00000000000..bdd73ca2df2 --- /dev/null +++ b/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonReceiver.java @@ -0,0 +1,112 @@ +/* + * Copyright 2025 the original author or authors. + * <p> + * 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 + * <p> + * https://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.json.internal.rpc; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.openrewrite.json.JsonVisitor; +import org.openrewrite.json.tree.Json; +import org.openrewrite.json.tree.JsonRightPadded; +import org.openrewrite.json.tree.JsonValue; +import org.openrewrite.json.tree.Space; +import org.openrewrite.rpc.RpcReceiveQueue; + +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; + +import static java.util.Objects.requireNonNull; + +public class JsonReceiver extends JsonVisitor<RpcReceiveQueue> { + + @Override + public Json preVisit(@NonNull Json j, RpcReceiveQueue q) { + j = j.withId(UUID.fromString(q.receiveAndGet(j.getId(), UUID::toString))); + j = j.withPrefix(q.receive(j.getPrefix(), space -> visitSpace(space, q))); + j = j.withMarkers(q.receiveMarkers(j.getMarkers())); + return j; + } + + @Override + public Json visitDocument(Json.Document document, RpcReceiveQueue q) { + String sourcePath = q.receiveAndGet(document.getSourcePath(), Path::toString); + return document.withSourcePath(Paths.get(sourcePath)) + .withCharset(Charset.forName(q.receiveAndGet(document.getCharset(), Charset::name))) + .withCharsetBomMarked(q.receive(document.isCharsetBomMarked())) + .withChecksum(q.receive(document.getChecksum())) + .withFileAttributes(q.receive(document.getFileAttributes())) + .withValue(q.receive(document.getValue(), j -> (JsonValue) visitNonNull(j, q))) + .withEof(q.receive(document.getEof())); + } + + @Override + public Json visitArray(Json.Array array, RpcReceiveQueue q) { + return array.getPadding().withValues( + q.receiveList(array.getPadding().getValues(), j -> visitRightPadded(j, q))); + } + + @Override + public Json visitEmpty(Json.Empty empty, RpcReceiveQueue q) { + return empty; + } + + @Override + public Json visitIdentifier(Json.Identifier identifier, RpcReceiveQueue q) { + return identifier.withName(q.receive(identifier.getName())); + } + + @Override + public Json visitLiteral(Json.Literal literal, RpcReceiveQueue q) { + return literal.withSource(q.receive(literal.getSource())) + .withValue(q.receive(literal.getValue())); + } + + @Override + public Json visitMember(Json.Member member, RpcReceiveQueue q) { + return member + .getPadding().withKey(q.receive(member.getPadding().getKey(), + j -> requireNonNull(visitRightPadded(j, q)))) + .withValue(q.receive(member.getValue(), j -> (JsonValue) visitNonNull(j, q))); + } + + @Override + public Json visitObject(Json.JsonObject object, RpcReceiveQueue q) { + return object.getPadding().withMembers( + q.receiveList(object.getPadding().getMembers(), j -> visitRightPadded(j, q))); + } + + @Override + public Space visitSpace(Space space, RpcReceiveQueue q) { + return space + .withComments(q.receiveList(space.getComments(), c -> c + .withMultiline(q.receive(c.isMultiline())) + .withText(q.receive(c.getText())) + .withSuffix(q.receive(c.getSuffix())) + .withMarkers(q.receiveMarkers(c.getMarkers())))) + .withWhitespace(q.receive(space.getWhitespace())); + } + + @Override + public <T extends Json> JsonRightPadded<T> visitRightPadded(@Nullable JsonRightPadded<T> right, RpcReceiveQueue q) { + assert right != null : "TreeDataReceiveQueue should have instantiated an empty padding"; + + //noinspection unchecked + return right.withElement(q.receive(right.getElement(), j -> (T) visitNonNull(j, q))) + .withAfter(q.receive(right.getAfter(), space -> visitSpace(space, q))) + .withMarkers(q.receiveMarkers(right.getMarkers())); + } +} diff --git a/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonSender.java b/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonSender.java new file mode 100644 index 00000000000..9ce39b3c8bf --- /dev/null +++ b/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/JsonSender.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025 the original author or authors. + * <p> + * 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 + * <p> + * https://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.json.internal.rpc; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.Tree; +import org.openrewrite.json.JsonVisitor; +import org.openrewrite.json.tree.Comment; +import org.openrewrite.json.tree.Json; +import org.openrewrite.json.tree.JsonRightPadded; +import org.openrewrite.json.tree.Space; +import org.openrewrite.rpc.Reference; +import org.openrewrite.rpc.RpcSendQueue; + +import static org.openrewrite.rpc.Reference.asRef; + +public class JsonSender extends JsonVisitor<RpcSendQueue> { + + @Override + public Json preVisit(Json j, RpcSendQueue q) { + q.getAndSend(j, Tree::getId); + q.getAndSend(j, j2 -> asRef(j2.getPrefix()), space -> + visitSpace(Reference.getValueNonNull(space), q)); + q.sendMarkers(j, Tree::getMarkers); + return j; + } + + @Override + public Json visitDocument(Json.Document document, RpcSendQueue q) { + q.getAndSend(document, (Json.Document d) -> d.getSourcePath().toString()); + q.getAndSend(document, (Json.Document d) -> d.getCharset().name()); + q.getAndSend(document, Json.Document::isCharsetBomMarked); + q.getAndSend(document, Json.Document::getChecksum); + q.getAndSend(document, Json.Document::getFileAttributes); + q.getAndSend(document, Json.Document::getValue, j -> visit(j, q)); + q.getAndSend(document, d -> asRef(d.getEof())); + return document; + } + + @Override + public Json visitArray(Json.Array array, RpcSendQueue q) { + q.getAndSendList(array, a -> a.getPadding().getValues(), + j -> j.getElement().getId(), + j -> visitRightPadded(j, q)); + return array; + } + + @Override + public Json visitEmpty(Json.Empty empty, RpcSendQueue q) { + return empty; + } + + @Override + public Json visitIdentifier(Json.Identifier identifier, RpcSendQueue q) { + q.getAndSend(identifier, Json.Identifier::getName); + return identifier; + } + + @Override + public Json visitLiteral(Json.Literal literal, RpcSendQueue q) { + q.getAndSend(literal, Json.Literal::getSource); + q.getAndSend(literal, Json.Literal::getValue); + return literal; + } + + @Override + public Json visitMember(Json.Member member, RpcSendQueue q) { + q.getAndSend(member, m -> m.getPadding().getKey(), j -> visitRightPadded(j, q)); + q.getAndSend(member, Json.Member::getValue, j -> visit(j, q)); + return member; + } + + @Override + public Json visitObject(Json.JsonObject obj, RpcSendQueue q) { + q.getAndSendList(obj, o -> o.getPadding().getMembers(), + j -> j.getElement().getId(), + j -> visitRightPadded(j, q)); + return obj; + } + + @Override + public Space visitSpace(Space space, RpcSendQueue q) { + q.getAndSendList(space, Space::getComments, c -> c.getText() + c.getSuffix(), c -> { + q.getAndSend(c, Comment::isMultiline); + q.getAndSend(c, Comment::getText); + q.getAndSend(c, Comment::getSuffix); + q.sendMarkers(c, Comment::getMarkers); + }); + q.getAndSend(space, Space::getWhitespace); + return space; + } + + @Override + public @Nullable <T extends Json> JsonRightPadded<T> visitRightPadded(@Nullable JsonRightPadded<T> right, RpcSendQueue q) { + q.getAndSend(right, JsonRightPadded::getElement, j -> visit(j, q)); + q.getAndSend(right, j -> asRef(j.getAfter()), + space -> visitSpace(Reference.getValueNonNull(space), q)); + q.sendMarkers(right, JsonRightPadded::getMarkers); + return right; + } +} diff --git a/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/package-info.java b/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/package-info.java new file mode 100644 index 00000000000..640d8821bae --- /dev/null +++ b/rewrite-json/src/main/java/org/openrewrite/json/internal/rpc/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 the original author or authors. + * <p> + * 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 + * <p> + * https://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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. + */ +@NullMarked +@NonNullFields +package org.openrewrite.json.internal.rpc; + +import org.jspecify.annotations.NullMarked; +import org.openrewrite.internal.lang.NonNullFields; diff --git a/rewrite-json/src/main/java/org/openrewrite/json/tree/Json.java b/rewrite-json/src/main/java/org/openrewrite/json/tree/Json.java index 6648eef5361..8fdc3bcfcc0 100644 --- a/rewrite-json/src/main/java/org/openrewrite/json/tree/Json.java +++ b/rewrite-json/src/main/java/org/openrewrite/json/tree/Json.java @@ -22,7 +22,12 @@ import org.openrewrite.*; import org.openrewrite.json.JsonVisitor; import org.openrewrite.json.internal.JsonPrinter; +import org.openrewrite.json.internal.rpc.JsonReceiver; +import org.openrewrite.json.internal.rpc.JsonSender; import org.openrewrite.marker.Markers; +import org.openrewrite.rpc.RpcCodec; +import org.openrewrite.rpc.RpcReceiveQueue; +import org.openrewrite.rpc.RpcSendQueue; import java.lang.ref.WeakReference; import java.nio.charset.Charset; @@ -31,7 +36,7 @@ import java.util.List; import java.util.UUID; -public interface Json extends Tree { +public interface Json extends Tree, RpcCodec<Json> { @SuppressWarnings("unchecked") @Override @@ -52,6 +57,16 @@ default <P> boolean isAcceptable(TreeVisitor<?, P> v, P p) { <J extends Json> J withPrefix(Space prefix); + @Override + default void rpcSend(Json after, RpcSendQueue q) { + new JsonSender().visit(after, q); + } + + @Override + default Json rpcReceive(Json before, RpcReceiveQueue q) { + return new JsonReceiver().visitNonNull(before, q); + } + @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) @RequiredArgsConstructor @@ -163,8 +178,9 @@ public Charset getCharset() { return charsetName == null ? StandardCharsets.UTF_8 : Charset.forName(charsetName); } + @SuppressWarnings("unchecked") @Override - public SourceFile withCharset(Charset charset) { + public Json.Document withCharset(Charset charset) { return withCharsetName(charset.name()); } diff --git a/rewrite-json/src/test/java/org/openrewrite/json/internal/rpc/JsonSendReceiveTest.java b/rewrite-json/src/test/java/org/openrewrite/json/internal/rpc/JsonSendReceiveTest.java new file mode 100644 index 00000000000..aa6e6d5e619 --- /dev/null +++ b/rewrite-json/src/test/java/org/openrewrite/json/internal/rpc/JsonSendReceiveTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2025 the original author or authors. + * <p> + * 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 + * <p> + * https://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.json.internal.rpc; + +import io.moderne.jsonrpc.JsonRpc; +import io.moderne.jsonrpc.handler.HeaderDelimitedMessageHandler; +import io.moderne.jsonrpc.handler.TraceMessageHandler; +import lombok.SneakyThrows; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.SourceFile; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.config.Environment; +import org.openrewrite.json.JsonVisitor; +import org.openrewrite.json.tree.Json; +import org.openrewrite.rpc.RewriteRpc; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.time.Duration; + +import static org.openrewrite.json.Assertions.json; +import static org.openrewrite.test.RewriteTest.toRecipe; + +class JsonSendReceiveTest implements RewriteTest { + RewriteRpc server; + RewriteRpc client; + + @BeforeEach + void before() throws IOException { + PipedOutputStream serverOut = new PipedOutputStream(); + PipedOutputStream clientOut = new PipedOutputStream(); + PipedInputStream serverIn = new PipedInputStream(clientOut); + PipedInputStream clientIn = new PipedInputStream(serverOut); + + Environment env = Environment.builder().build(); + + JsonRpc serverJsonRpc = new JsonRpc(new TraceMessageHandler("server", + new HeaderDelimitedMessageHandler(serverIn, serverOut))); + server = new RewriteRpc(serverJsonRpc, env).batchSize(1).timeout(Duration.ofSeconds(10)); + + JsonRpc clientJsonRpc = new JsonRpc(new TraceMessageHandler("client", + new HeaderDelimitedMessageHandler(clientIn, clientOut))); + client = new RewriteRpc(clientJsonRpc, env).batchSize(1).timeout(Duration.ofSeconds(10)); + } + + @AfterEach + void after() { + server.shutdown(); + client.shutdown(); + } + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(toRecipe(() -> new TreeVisitor<>() { + @SneakyThrows + @Override + public Tree preVisit(@NonNull Tree tree, ExecutionContext ctx) { + Tree t = server.visit((SourceFile) tree, ChangeValue.class.getName(), 0); + stopAfterPreVisit(); + return t; + } + })); + } + + @Test + void sendReceiveIdempotence() { + rewriteRun( + //language=json + json( + """ + { + "key": "value", + "array": [1, 2, 3] + } + """, + """ + { + "key": "changed", + "array": [1, 2, 3] + } + """ + ) + ); + } + + static class ChangeValue extends JsonVisitor<Integer> { + @Override + public Json visitLiteral(Json.Literal literal, Integer p) { + if (literal.getValue().equals("value")) { + return literal.withValue("changed").withSource("\"changed\""); + } + return literal; + } + } +} diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependenciesInUse.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependenciesInUse.java index a02b808d2a0..b7244316cf1 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependenciesInUse.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependenciesInUse.java @@ -26,9 +26,7 @@ public class DependenciesInUse extends DataTable<DependenciesInUse.Row> { public DependenciesInUse(Recipe recipe) { - super(recipe, Row.class, - DependenciesInUse.class.getName(), - "Dependencies in use", "Direct and transitive dependencies in use."); + super(recipe, "Dependencies in use", "Direct and transitive dependencies in use."); } @Value diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyGraph.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyGraph.java index 1ba53788e17..b6756c59001 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyGraph.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyGraph.java @@ -23,10 +23,7 @@ public class DependencyGraph extends DataTable<DependencyGraph.Row> { public DependencyGraph(Recipe recipe) { - super(recipe, DependencyGraph.Row.class, - DependenciesInUse.class.getName(), - "Dependency graph", - "Relationships between dependencies."); + super(recipe, "Dependency graph", "Relationships between dependencies."); } @Value diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyResolutions.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyResolutions.java index 16526337993..7f0bde80275 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyResolutions.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/DependencyResolutions.java @@ -26,7 +26,7 @@ public class DependencyResolutions extends DataTable<DependencyResolutions.Row> { public DependencyResolutions(Recipe recipe) { - super(recipe, Row.class, DependencyResolutions.class.getName(), + super(recipe, "Dependency resolutions", "Latencies of individual dependency resolution requests and their outcomes."); } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/EffectiveMavenSettings.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/EffectiveMavenSettings.java index 15ba35d63eb..3d765261503 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/EffectiveMavenSettings.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/EffectiveMavenSettings.java @@ -23,10 +23,7 @@ public class EffectiveMavenSettings extends DataTable<EffectiveMavenSettings.Row> { public EffectiveMavenSettings(Recipe recipe) { - super(recipe, EffectiveMavenSettings.Row.class, - EffectiveMavenSettings.class.getName(), - "Effective maven settings", - "The maven settings file used by each pom."); + super(recipe, "Effective maven settings", "The maven settings file used by each pom."); } @Value diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/ManagedDependencyGraph.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/ManagedDependencyGraph.java index 349c90203a1..5c2dbed7c4d 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/ManagedDependencyGraph.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/ManagedDependencyGraph.java @@ -23,9 +23,7 @@ public class ManagedDependencyGraph extends DataTable<ManagedDependencyGraph.Row> { public ManagedDependencyGraph(Recipe recipe) { - super(recipe, ManagedDependencyGraph.Row.class, - DependenciesInUse.class.getName(), - "Managed dependency graph", + super(recipe, "Managed dependency graph", "Relationships between POMs and their ancestors that define managed dependencies."); } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenMetadataFailures.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenMetadataFailures.java index 3a4b55886ba..e0ddffd6af7 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenMetadataFailures.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenMetadataFailures.java @@ -31,8 +31,7 @@ @JsonIgnoreType public class MavenMetadataFailures extends DataTable<MavenMetadataFailures.Row> { public MavenMetadataFailures(Recipe recipe) { - super(recipe, Row.class, MavenMetadataFailures.class.getName(), - "Maven metadata failures", + super(recipe, "Maven metadata failures", "Attempts to resolve maven metadata that failed."); } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenProperties.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenProperties.java index e88c60a9cea..e561e94f9f0 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenProperties.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenProperties.java @@ -26,9 +26,7 @@ public class MavenProperties extends DataTable<MavenProperties.Row> { public MavenProperties(Recipe recipe) { - super(recipe, Row.class, - MavenProperties.class.getName(), - "Maven properties", "Property and value."); + super(recipe, "Maven properties", "Property and value."); } @Value diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenRepositoryOrder.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenRepositoryOrder.java index 9404a089526..51f2e158280 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenRepositoryOrder.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/MavenRepositoryOrder.java @@ -23,8 +23,7 @@ public class MavenRepositoryOrder extends DataTable<MavenRepositoryOrder.Row> { public MavenRepositoryOrder(Recipe recipe) { - super(recipe, MavenRepositoryOrder.Row.class, - MavenRepositoryOrder.class.getName(), + super(recipe, "Maven repository order", "The order in which dependencies will be resolved for each `pom.xml` based on its defined repositories and effective `settings.xml`."); } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/table/ParentPomsInUse.java b/rewrite-maven/src/main/java/org/openrewrite/maven/table/ParentPomsInUse.java index 133f7114adb..372aea02d94 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/table/ParentPomsInUse.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/table/ParentPomsInUse.java @@ -26,9 +26,7 @@ public class ParentPomsInUse extends DataTable<ParentPomsInUse.Row> { public ParentPomsInUse(Recipe recipe) { - super(recipe, Row.class, - ParentPomsInUse.class.getName(), - "Maven parent POMs in use", "Projects, GAVs and relativePaths for Maven parent POMs in use."); + super(recipe, "Maven parent POMs in use", "Projects, GAVs and relativePaths for Maven parent POMs in use."); } @Value