diff --git a/rewrite-core/src/main/java/org/openrewrite/Recipe.java b/rewrite-core/src/main/java/org/openrewrite/Recipe.java index b64077ab7f..28d13ace22 100644 --- a/rewrite-core/src/main/java/org/openrewrite/Recipe.java +++ b/rewrite-core/src/main/java/org/openrewrite/Recipe.java @@ -37,6 +37,7 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.NullUtils; import org.openrewrite.table.RecipeRunStats; +import org.openrewrite.table.SearchResults; import org.openrewrite.table.SourcesFileErrors; import org.openrewrite.table.SourcesFileResults; @@ -315,6 +316,7 @@ private List getOptionDescriptors() { private static final List GLOBAL_DATA_TABLES = Arrays.asList( dataTableDescriptorFromDataTable(new SourcesFileResults(Recipe.noop())), + dataTableDescriptorFromDataTable(new SearchResults(Recipe.noop())), dataTableDescriptorFromDataTable(new SourcesFileErrors(Recipe.noop())), dataTableDescriptorFromDataTable(new RecipeRunStats(Recipe.noop())) ); diff --git a/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java b/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java index 0c9f8d6d9a..9f75c127e6 100644 --- a/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java +++ b/rewrite-core/src/main/java/org/openrewrite/RecipeScheduler.java @@ -18,6 +18,7 @@ import org.openrewrite.scheduling.RecipeRunCycle; import org.openrewrite.scheduling.WatchableExecutionContext; import org.openrewrite.table.RecipeRunStats; +import org.openrewrite.table.SearchResults; import org.openrewrite.table.SourcesFileErrors; import org.openrewrite.table.SourcesFileResults; @@ -56,6 +57,7 @@ private LargeSourceSet runRecipeCycles(Recipe recipe, LargeSourceSet sourceSet, RecipeRunStats recipeRunStats = new RecipeRunStats(Recipe.noop()); SourcesFileErrors errorsTable = new SourcesFileErrors(Recipe.noop()); + SearchResults searchResults = new SearchResults(Recipe.noop()); SourcesFileResults sourceFileResults = new SourcesFileResults(Recipe.noop()); LargeSourceSet after = sourceSet; @@ -72,7 +74,7 @@ private LargeSourceSet runRecipeCycles(Recipe recipe, LargeSourceSet sourceSet, Cursor rootCursor = new Cursor(null, Cursor.ROOT_VALUE); try { RecipeRunCycle cycle = new RecipeRunCycle<>(recipe, i, rootCursor, ctxWithWatch, - recipeRunStats, sourceFileResults, errorsTable, LargeSourceSet::edit); + recipeRunStats, searchResults, sourceFileResults, errorsTable, LargeSourceSet::edit); ctxWithWatch.putCycle(cycle); after.beforeCycle(i == maxCycles); 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 index bbfe0b54d1..6717fd5dd0 100644 --- a/rewrite-core/src/main/java/org/openrewrite/rpc/request/Generate.java +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Generate.java @@ -24,6 +24,7 @@ import org.openrewrite.scheduling.RecipeRunCycle; import org.openrewrite.scheduling.WatchableExecutionContext; import org.openrewrite.table.RecipeRunStats; +import org.openrewrite.table.SearchResults; import org.openrewrite.table.SourcesFileErrors; import org.openrewrite.table.SourcesFileResults; @@ -58,8 +59,9 @@ protected Object handle(Generate request) throws Exception { if (ctx.getMessage(CURRENT_RECIPE) == null) { WatchableExecutionContext wctx = new WatchableExecutionContext(ctx); wctx.putCycle(new RecipeRunCycle<>(recipe, 0, new Cursor(null, Cursor.ROOT_VALUE), wctx, - new RecipeRunStats(Recipe.noop()), new SourcesFileResults(Recipe.noop()), - new SourcesFileErrors(Recipe.noop()), LargeSourceSet::edit)); + new RecipeRunStats(Recipe.noop()), new SearchResults(Recipe.noop()), + new SourcesFileResults(Recipe.noop()), new SourcesFileErrors(Recipe.noop()), + LargeSourceSet::edit)); ctx.putCurrentRecipe(recipe); } 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 index a558976e46..36bada433a 100644 --- a/rewrite-core/src/main/java/org/openrewrite/rpc/request/Visit.java +++ b/rewrite-core/src/main/java/org/openrewrite/rpc/request/Visit.java @@ -24,6 +24,7 @@ import org.openrewrite.scheduling.RecipeRunCycle; import org.openrewrite.scheduling.WatchableExecutionContext; import org.openrewrite.table.RecipeRunStats; +import org.openrewrite.table.SearchResults; import org.openrewrite.table.SourcesFileErrors; import org.openrewrite.table.SourcesFileResults; @@ -91,8 +92,9 @@ private Object getVisitorP(Visit request) { // removed from OpenRewrite in the future. WatchableExecutionContext ctx = new WatchableExecutionContext((ExecutionContext) p); 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)); + new RecipeRunStats(Recipe.noop()), new SearchResults(Recipe.noop()), + new SourcesFileResults(Recipe.noop()), new SourcesFileErrors(Recipe.noop()), + LargeSourceSet::edit)); ctx.putCurrentRecipe(recipe); return ctx; } 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 c2468068d3..46ad387abc 100644 --- a/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java +++ b/rewrite-core/src/main/java/org/openrewrite/scheduling/RecipeRunCycle.java @@ -26,11 +26,10 @@ import org.openrewrite.internal.ExceptionUtils; import org.openrewrite.internal.FindRecipeRunException; import org.openrewrite.internal.RecipeRunException; -import org.openrewrite.marker.Generated; -import org.openrewrite.marker.Markers; -import org.openrewrite.marker.RecipesThatMadeChanges; +import org.openrewrite.marker.*; import org.openrewrite.quark.Quark; import org.openrewrite.table.RecipeRunStats; +import org.openrewrite.table.SearchResults; import org.openrewrite.table.SourcesFileErrors; import org.openrewrite.table.SourcesFileResults; @@ -41,8 +40,7 @@ import java.util.function.BiFunction; import java.util.function.UnaryOperator; -import static java.util.Collections.newSetFromMap; -import static java.util.Collections.unmodifiableList; +import static java.util.Collections.*; import static org.openrewrite.ExecutionContext.SCANNING_MUTATION_VALIDATION; import static org.openrewrite.Recipe.PANIC; @@ -64,6 +62,7 @@ public class RecipeRunCycle { Cursor rootCursor; WatchableExecutionContext ctx; RecipeRunStats recipeRunStats; + SearchResults searchResults; SourcesFileResults sourcesFileResults; SourcesFileErrors errorsTable; BiFunction, LSS> sourceSetEditor; @@ -152,7 +151,7 @@ public LSS generateSources(LSS sourceSet) { generated.replaceAll(source -> addRecipesThatMadeChanges(recipeStack, source)); if (!generated.isEmpty()) { acc.addAll(generated); - generated.forEach(source -> recordSourceFileResult(null, source, recipeStack, ctx)); + generated.forEach(source -> recordSourceFileResultAndSearchResults(null, source, recipeStack, ctx)); madeChangesInThisCycle.add(recipe); } } catch (Throwable t) { @@ -214,7 +213,7 @@ public LSS editSources(LSS sourceSet) { if (after != source) { madeChangesInThisCycle.add(recipe); - recordSourceFileResult(source, after, recipeStack, ctx); + recordSourceFileResultAndSearchResults(source, after, recipeStack, ctx); if (source.getMarkers().findFirst(Generated.class).isPresent()) { // skip edits made to generated source files so that they don't show up in a diff // that later fails to apply on a freshly cloned repository @@ -234,11 +233,11 @@ public LSS editSources(LSS sourceSet) { } return after; }, sourceFile); - } + } ); } - private void recordSourceFileResult(@Nullable SourceFile before, @Nullable SourceFile after, Stack recipeStack, ExecutionContext ctx) { + private void recordSourceFileResultAndSearchResults(@Nullable SourceFile before, @Nullable SourceFile after, Stack recipeStack, ExecutionContext ctx) { String beforePath = (before == null) ? "" : before.getSourcePath().toString(); String afterPath = (after == null) ? "" : after.getSourcePath().toString(); Recipe recipe = recipeStack.peek(); @@ -250,14 +249,19 @@ private void recordSourceFileResult(@Nullable SourceFile before, @Nullable Sourc if (hierarchical) { parentName = recipeStack.get(recipeStack.size() - 2).getName(); } - String recipeName = recipe.getName(); sourcesFileResults.insertRow(ctx, new SourcesFileResults.Row( beforePath, afterPath, parentName, - recipeName, + recipe.getName(), effortSeconds, cycle)); + + List searchMarkers = collectSearchResults(before, after); + for (String searchResult : searchMarkers) { + searchResults.insertRow(ctx, new SearchResults.Row(beforePath, afterPath, searchResult, recipe.getInstanceName())); + } + if (hierarchical) { recordSourceFileResult(beforePath, afterPath, recipeStack.subList(0, recipeStack.size() - 1), effortSeconds, ctx); } @@ -347,4 +351,35 @@ private static boolean isScanningRequired(Recipe recipe) { } return false; } + + private List collectSearchResults(@Nullable SourceFile before, @Nullable SourceFile after) { + if (after == null) { + return emptyList(); + } + Set alreadyPresentMarkers = new TreeVisitor>() { + @Override + public M visitMarker(Marker marker, Set ctx) { + if (marker instanceof SearchResult) { + ctx.add((SearchResult) marker); + } + return super.visitMarker(marker, ctx); + } + }.reduce(before, newSetFromMap(new IdentityHashMap<>())); + + return new TreeVisitor>() { + @Override + public M visitMarker(Marker marker, List ctx) { + if (marker instanceof SearchResult && !alreadyPresentMarkers.contains(marker)) { + Cursor cursor = getCursor(); + if (!(cursor.getValue() instanceof Tree)) { + cursor = cursor.getParentTreeCursor(); + } + if (cursor.getValue() instanceof Tree) { + ctx.add(((Tree) cursor.getValue()).print(getCursor(), new PrintOutputCapture<>(0, PrintOutputCapture.MarkerPrinter.SANITIZED))); + } + } + return super.visitMarker(marker, ctx); + } + }.reduce(after, new ArrayList<>()); + } } diff --git a/rewrite-core/src/main/java/org/openrewrite/table/SearchResults.java b/rewrite-core/src/main/java/org/openrewrite/table/SearchResults.java new file mode 100644 index 0000000000..b8001fdbc4 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/table/SearchResults.java @@ -0,0 +1,51 @@ +/* + * 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. + */ +package org.openrewrite.table; + +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Column; +import org.openrewrite.DataTable; +import org.openrewrite.Recipe; + +public class SearchResults extends DataTable { + + public SearchResults(Recipe recipe) { + super(recipe, "Source files that had search results", + "Search results that were found during the recipe run."); + } + + @Value + public static class Row { + @Column(displayName = "Source path of search result before the run", + description = "The source path of the file with the search result markers present.") + @Nullable + String sourcePath; + + @Column(displayName = "Source path of search result after run the run", + description = "A recipe may modify the source path. This is the path after the run. `null` when a source file was deleted during the run.") + @Nullable + String afterSourcePath; + + @Column(displayName = "Result", + description = "The trimmed printed tree of the LST element that the marker is attached to.") + String result; + + @Column(displayName = "Recipe that added the search marker", + description = "The specific recipe that added the Search marker.") + String recipe; + } +} diff --git a/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java b/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java index d783a136d5..d5c8fff317 100644 --- a/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/DataTableTest.java @@ -63,7 +63,7 @@ public PlainText visitText(PlainText text, ExecutionContext ctx) { void descriptor() { Recipe recipe = toRecipe(); new WordTable(recipe); - assertThat(recipe.getDataTableDescriptors()).hasSize(4); + assertThat(recipe.getDataTableDescriptors()).hasSize(5); assertThat(recipe.getDataTableDescriptors().getFirst().getColumns()).hasSize(2); } diff --git a/rewrite-core/src/test/java/org/openrewrite/table/SearchResultsTest.java b/rewrite-core/src/test/java/org/openrewrite/table/SearchResultsTest.java new file mode 100644 index 0000000000..1e4bb70662 --- /dev/null +++ b/rewrite-core/src/test/java/org/openrewrite/table/SearchResultsTest.java @@ -0,0 +1,226 @@ +/* + * Copyright 2023 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.table; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.openrewrite.test.SourceSpecs.text; + +class SearchResultsTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipeFromYaml( + //language=yml + """ + type: specs.openrewrite.org/v1beta/recipe + name: test.SearchMarkerScraping + displayName: Find text and add the results to the global datatable + description: Hello world. + recipeList: + - org.openrewrite.text.Find: + find: hi + caseSensitive: true + - org.openrewrite.text.Find: + find: SOME OTHER TEXT + - org.openrewrite.text.Find: + find: ([A-Z])\\w+ + regex: true + caseSensitive: true + """, + "test.SearchMarkerScraping" + ); + } + + @Test + void searchMarkersAreDetectedDuringRecipeRun() { + rewriteRun( + spec -> spec.dataTable(SearchResults.Row.class, rows -> assertThat(rows).extracting( + SearchResults.Row::getSourcePath, + SearchResults.Row::getResult, + SearchResults.Row::getRecipe) + .containsExactlyInAnyOrder( + tuple("matched", "hi", "Find text `hi`"), + tuple("nested/matched", "hi", "Find text `hi`") + )), + text( + "hi", + "~~>hi", + spec -> spec.path("matched") + ), + text( + "hello", + spec -> spec.path("non-matched") + ), + text( + "hi", + "~~>hi", + spec -> spec.path("nested/matched") + ) + ); + } + + @Test + void multipleMarkersAddedByRecipeReported() { + rewriteRun( + spec -> spec.dataTable(SearchResults.Row.class, rows -> assertThat(rows).extracting( + SearchResults.Row::getSourcePath, + SearchResults.Row::getResult, + SearchResults.Row::getRecipe) + .containsExactlyInAnyOrder( + tuple("match-capitalized-words", "We", "Find text `([A-Z])\\w+`"), + tuple("match-capitalized-words", "SearchMarkers", "Find text `([A-Z])\\w+`"), + tuple("match-capitalized-words", "File", "Find text `([A-Z])\\w+`"), + tuple("match-capitalized-words", "End", "Find text `([A-Z])\\w+`") + )), + text( + """ + We add all SearchMarkers that are found during recipe run. + File contains capitalized words. + End of file + """, + """ + ~~>We add all ~~>SearchMarkers that are found during recipe run. + ~~>File contains capitalized words. + ~~>End of file + """, + spec -> spec.path("match-capitalized-words") + ) + ); + } + + @Test + void markersOfGeneratedFilesReported() { + rewriteRun( + spec -> spec + .recipeFromYaml( + //language=yml + """ + type: specs.openrewrite.org/v1beta/recipe + name: test.SearchMarkerScraping + displayName: Find text and add the results to the global datatable + description: Hello world. + recipeList: + - org.openrewrite.text.CreateTextFile: + fileContents: this file matches + relativeFileName: matched + - org.openrewrite.text.Find: + find: hi + caseSensitive: true + """, + "test.SearchMarkerScraping" + ).dataTable(SearchResults.Row.class, rows -> assertThat(rows).extracting( + SearchResults.Row::getSourcePath, + SearchResults.Row::getResult, + SearchResults.Row::getRecipe) + .containsExactlyInAnyOrder( + tuple("matched", "hi", "Find text `hi`") + )), + text( + doesNotExist(), + "t~~>his file matches", + spec -> spec.path("matched") + ) + ); + } + + @Test + void multipleMarkersAddedByDifferentRecipesReported() { + rewriteRun( + spec -> spec.dataTable(SearchResults.Row.class, rows -> assertThat(rows).extracting( + SearchResults.Row::getSourcePath, + SearchResults.Row::getResult, + SearchResults.Row::getRecipe) + .containsExactlyInAnyOrder( + tuple("different-recipes-matching", "We", "Find text `([A-Z])\\w+`"), + tuple("different-recipes-matching", "SearchMarkers", "Find text `([A-Z])\\w+`"), + tuple("different-recipes-matching", "some other text", "Find text `SOME OTHER TEXT`") + )), + text( + """ + We add all SearchMarkers that are found during recipe run. + file contains some other text somewhere in the middle resulting in 2 different recipes matches. + """, + """ + ~~>We add all ~~>SearchMarkers that are found during recipe run. + file contains ~~>some other text somewhere in the middle resulting in 2 different recipes matches. + """, + spec -> spec.path("different-recipes-matching") + ) + ); + } + + @Test + void nestedRecipesReported() { + rewriteRun( + spec -> spec + .recipeFromYaml( + //language=yml + """ + type: specs.openrewrite.org/v1beta/recipe + name: test.SearchMarkerScraping + displayName: Find text and add the results to the global datatable + description: Hello world. + recipeList: + - test.FindCapitalizedWords + - test.FindSomeOtherText + --- + type: specs.openrewrite.org/v1beta/recipe + name: test.FindCapitalizedWords + displayName: Find capitalized text and add the results to the global datatable + description: Hello world. + recipeList: + - org.openrewrite.text.Find: + find: ([A-Z])\\w+ + regex: true + caseSensitive: true + --- + type: specs.openrewrite.org/v1beta/recipe + name: test.FindSomeOtherText + displayName: Find some other text and add the results to the global datatable + description: Hello world. + recipeList: + - org.openrewrite.text.Find: + find: SOME OTHER TEXT + """, + "test.SearchMarkerScraping" + ).dataTable(SearchResults.Row.class, rows -> assertThat(rows).extracting( + SearchResults.Row::getSourcePath, + SearchResults.Row::getResult, + SearchResults.Row::getRecipe) + .containsExactlyInAnyOrder( + tuple("different-recipes-matching", "We", "Find text `([A-Z])\\w+`"), + tuple("different-recipes-matching", "SearchMarkers", "Find text `([A-Z])\\w+`"), + tuple("different-recipes-matching", "some other text", "Find text `SOME OTHER TEXT`") + )), + text( + """ + We add all SearchMarkers that are found during recipe run. + file contains some other text somewhere in the middle resulting in 2 different recipes matches. + """, + """ + ~~>We add all ~~>SearchMarkers that are found during recipe run. + file contains ~~>some other text somewhere in the middle resulting in 2 different recipes matches. + """, + spec -> spec.path("different-recipes-matching") + ) + ); + } +}