From 52c87adff75d730c14446a2ea0a1db4f27890990 Mon Sep 17 00:00:00 2001 From: l-ferguson <49286742+l-ferguson@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:31:38 -0400 Subject: [PATCH] Add HCL FindAndReplaceLiteral Recipe (#4362) * Adding recipe FindAndReplaceLiteral to find and replace string literals in HCL files * Removing "string" qualifier from recipe and field descriptions since recipe handles literals of other types as well * Fixing spacing in FindAndReplaceLiteralTest.java * Fixing spacing and variable naming to comply with coding standards * Polish * Simplify by stripping unlikely options * Drop MultiFindAndReplace from FindAndReplaceTest --------- Co-authored-by: Tim te Beek --- .../openrewrite/text/FindAndReplaceTest.java | 37 +-- .../hcl/search/FindAndReplaceLiteral.java | 105 +++++++ .../hcl/search/FindAndReplaceLiteralTest.java | 275 ++++++++++++++++++ 3 files changed, 386 insertions(+), 31 deletions(-) create mode 100644 rewrite-hcl/src/main/java/org/openrewrite/hcl/search/FindAndReplaceLiteral.java create mode 100644 rewrite-hcl/src/test/java/org/openrewrite/hcl/search/FindAndReplaceLiteralTest.java diff --git a/rewrite-core/src/test/java/org/openrewrite/text/FindAndReplaceTest.java b/rewrite-core/src/test/java/org/openrewrite/text/FindAndReplaceTest.java index b46b458501f..bf77a7f6ee4 100644 --- a/rewrite-core/src/test/java/org/openrewrite/text/FindAndReplaceTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/text/FindAndReplaceTest.java @@ -15,16 +15,10 @@ */ package org.openrewrite.text; -import lombok.EqualsAndHashCode; -import lombok.Value; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; -import org.openrewrite.Recipe; import org.openrewrite.test.RewriteTest; -import java.util.Arrays; -import java.util.List; - import static org.openrewrite.test.SourceSpecs.text; class FindAndReplaceTest implements RewriteTest { @@ -57,7 +51,7 @@ void removeWhenNullOrEmpty() { """, """ Foo - + Quz """ ) @@ -127,33 +121,14 @@ void dollarSignsTolerated() { ); } - @Value - @EqualsAndHashCode(callSuper = false) - static class MultiFindAndReplace extends Recipe { - - @Override - public String getDisplayName() { - return "Replaces \"one\" with \"two\" then \"three\" then \"four\""; - } - - @Override - public String getDescription() { - return "Replaces \"one\" with \"two\" then \"three\" then \"four\"."; - } - - @Override - public List getRecipeList() { - return Arrays.asList( - new FindAndReplace("one", "two", null, null, null, null, null, null), - new FindAndReplace("two", "three", null, null, null, null, null, null), - new FindAndReplace("three", "four", null, null, null, null, null, null)); - } - } - @Test void successiveReplacement() { rewriteRun( - spec -> spec.recipe(new MultiFindAndReplace()), + spec -> spec.recipes( + new FindAndReplace("one", "two", null, null, null, null, null, null), + new FindAndReplace("two", "three", null, null, null, null, null, null), + new FindAndReplace("three", "four", null, null, null, null, null, null) + ), text( """ one diff --git a/rewrite-hcl/src/main/java/org/openrewrite/hcl/search/FindAndReplaceLiteral.java b/rewrite-hcl/src/main/java/org/openrewrite/hcl/search/FindAndReplaceLiteral.java new file mode 100644 index 00000000000..68e9798fb6f --- /dev/null +++ b/rewrite-hcl/src/main/java/org/openrewrite/hcl/search/FindAndReplaceLiteral.java @@ -0,0 +1,105 @@ +/* + * Copyright 2024 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.hcl.search; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.hcl.HclIsoVisitor; +import org.openrewrite.hcl.tree.Hcl; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.AlreadyReplaced; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.openrewrite.Tree.randomId; + +@Value +@EqualsAndHashCode(callSuper = false) +public class FindAndReplaceLiteral extends Recipe { + + @Override + public String getDisplayName() { + return "Find and replace literals in HCL files"; + } + + @Override + public String getDescription() { + return "Find and replace literal values in HCL files. This recipe parses the source files on which it runs as HCL, " + + "meaning you can execute HCL language-specific recipes before and after this recipe in a single recipe run."; + } + + @Option(displayName = "Find", description = "The literal to find (and replace)", example = "blacklist") + String find; + + @Option(displayName = "Replace", + description = "The replacement literal for `find`. This snippet can be multiline.", + example = "denylist", + required = false) + @Nullable + String replace; + + @Option(displayName = "Regex", + description = "Default false. If true, `find` will be interpreted as a Regular Expression, and capture group contents will be available in `replace`.", + required = false) + @Nullable + Boolean regex; + + @Option(displayName = "Case sensitive", + description = "If `true` the search will be sensitive to case. Default `false`.", required = false) + @Nullable + Boolean caseSensitive; + + @Override + public TreeVisitor getVisitor() { + return new HclIsoVisitor() { + @Override + public Hcl.Literal visitLiteral(final Hcl.Literal literal, final ExecutionContext ctx) { + for (AlreadyReplaced alreadyReplaced : literal.getMarkers().findAll(AlreadyReplaced.class)) { + if (Objects.equals(find, alreadyReplaced.getFind()) && + Objects.equals(replace, alreadyReplaced.getReplace())) { + return literal; + } + } + String searchStr = find; + if (!Boolean.TRUE.equals(regex)) { + searchStr = Pattern.quote(searchStr); + } + int patternOptions = 0; + if (!Boolean.TRUE.equals(caseSensitive)) { + patternOptions |= Pattern.CASE_INSENSITIVE; + } + Pattern pattern = Pattern.compile(searchStr, patternOptions); + Matcher matcher = pattern.matcher(literal.getValue().toString()); + if (!matcher.find()) { + return literal; + } + String replacement = replace == null ? "" : replace; + if (!Boolean.TRUE.equals(regex)) { + replacement = replacement.replace("$", "\\$"); + } + String newLiteral = matcher.replaceAll(replacement); + return literal.withValue(newLiteral).withValueSource(newLiteral) + .withMarkers(literal.getMarkers().add(new AlreadyReplaced(randomId(), find, replace))); + } + }; + } +} diff --git a/rewrite-hcl/src/test/java/org/openrewrite/hcl/search/FindAndReplaceLiteralTest.java b/rewrite-hcl/src/test/java/org/openrewrite/hcl/search/FindAndReplaceLiteralTest.java new file mode 100644 index 00000000000..d5493a7022b --- /dev/null +++ b/rewrite-hcl/src/test/java/org/openrewrite/hcl/search/FindAndReplaceLiteralTest.java @@ -0,0 +1,275 @@ +/* + * Copyright 2024 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.hcl.search; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.hcl.Assertions.hcl; + +class FindAndReplaceLiteralTest implements RewriteTest { + + @Test + @DocumentExample + void defaultNonRegexReplace() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("app-cluster", "new-app-cluster", null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "new-app-cluster" + } + } + """ + ) + ); + } + + @Test + void removeWhenReplaceIsNullOrEmpty() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("prefix-", null, null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "prefix-app-cluster" + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + } + } + """ + ) + ); + } + + @Test + void regexReplace() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral(".", "a", true, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "aaaaaaaaaaa" + } + } + """ + ) + ); + } + + @Test + void captureGroupsReplace() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("old-([^.]+)", "new-$1", true, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "old-app-cluster" + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "new-app-cluster" + } + } + """ + ) + ); + } + + @Test + void noRecursiveReplace() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("app", "application", null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "application-cluster" + } + } + """ + ) + ); + } + + @Test + void compatibleWithDollarSigns() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("$${app-cluster}", "$${new-app-cluster}", null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "$${app-cluster}" + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "$${new-app-cluster}" + } + } + """ + ) + ); + } + + @Test + void doesNotReplaceStringTemplate() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("app-name", "new-app-name", null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "old-${app-name}-cluster" + } + } + """ + ) + ); + } + + @Test + void doesNothingIfLiteralNotFound() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("hello", "goodbye", null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + } + } + """ + ) + ); + } + + @Test + void doesNotReplaceVariableNames() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("app_deployment", "replacement_deployment_name", null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + } + } + """ + ) + ); + } + + @Test + void replacesNumericLiterals() { + rewriteRun( + spec -> spec.recipe(new FindAndReplaceLiteral("2", "1", null, null)), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + app_replica = 2 + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "app-cluster" + app_replica = 1 + } + } + """ + ) + ); + } + + @Test + void successiveReplacement() { + rewriteRun( + spec -> spec.recipes( + new FindAndReplaceLiteral("cluster-1", "cluster-2", null, null), + new FindAndReplaceLiteral("cluster-2", "cluster-3", null, null), + new FindAndReplaceLiteral("cluster-3", "cluster-4", null, null) + ), + //language=hcl + hcl( + """ + config = { + app_deployment = { + cluster_name = "cluster-1" + } + } + """, + """ + config = { + app_deployment = { + cluster_name = "cluster-4" + } + } + """ + ) + ); + } +}