diff --git a/src/main/java/org/openrewrite/staticanalysis/AvoidRepeatedPatternCompile.java b/src/main/java/org/openrewrite/staticanalysis/AvoidRepeatedPatternCompile.java
new file mode 100644
index 000000000..2286b192b
--- /dev/null
+++ b/src/main/java/org/openrewrite/staticanalysis/AvoidRepeatedPatternCompile.java
@@ -0,0 +1,830 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * 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.staticanalysis;
+
+import org.openrewrite.Cursor;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.MethodMatcher;
+import org.openrewrite.java.tree.Expression;
+import org.openrewrite.java.tree.Flag;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.Space;
+import org.openrewrite.java.tree.Statement;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+public class AvoidRepeatedPatternCompile extends Recipe {
+
+ private static final MethodMatcher PATTERN_COMPILE =
+ new MethodMatcher(
+ "java.util.regex.Pattern compile(java.lang.String)"
+ );
+
+ private static final MethodMatcher PATTERN_COMPILE_WITH_FLAGS =
+ new MethodMatcher(
+ "java.util.regex.Pattern compile(java.lang.String, int)"
+ );
+
+ private static final String METHOD_CANDIDATES =
+ "avoidRepeatedPatternCompile.methodCandidates";
+
+ private static final String CLASS_CANDIDATES =
+ "avoidRepeatedPatternCompile.classCandidates";
+
+ private static final String USED_FIELD_NAMES =
+ "avoidRepeatedPatternCompile.usedFieldNames";
+
+ @Override
+ public String getDisplayName() {
+ return "Avoid repeated `Pattern.compile()` calls";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Moves `Pattern.compile()` calls with constant regular expressions " +
+ "from method-local variables into private static final fields.";
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return new JavaIsoVisitor() {
+
+ @Override
+ public J.CompilationUnit visitCompilationUnit(
+ J.CompilationUnit compilationUnit,
+ ExecutionContext ctx) {
+
+ J.CompilationUnit cu =
+ super.visitCompilationUnit(compilationUnit, ctx);
+
+ if (cu != compilationUnit) {
+ maybeAddImport("java.util.regex.Pattern");
+ }
+
+ return cu;
+ }
+
+ @Override
+ public J.ClassDeclaration visitClassDeclaration(
+ J.ClassDeclaration classDeclaration,
+ ExecutionContext ctx) {
+
+ J.ClassDeclaration cd =
+ super.visitClassDeclaration(classDeclaration, ctx);
+
+ List candidates =
+ getCursor().getMessage(CLASS_CANDIDATES);
+
+ if (candidates == null || candidates.isEmpty()) {
+ return cd;
+ }
+
+ /*
+ * Add in reverse order because firstStatement()
+ * always inserts at the beginning.
+ */
+ for (int i = candidates.size() - 1; i >= 0; i--) {
+
+ Candidate candidate = candidates.get(i);
+
+ JavaTemplate template =
+ JavaTemplate.builder(
+ "private static final Pattern " +
+ candidate.constantName +
+ " = #{any(java.util.regex.Pattern)};"
+ )
+ .imports("java.util.regex.Pattern")
+ .build();
+
+ cd = template.apply(
+ updateCursor(cd),
+ cd.getBody()
+ .getCoordinates()
+ .firstStatement(),
+ candidate.initializer
+ );
+ }
+
+ /*
+ * FIX:
+ *
+ * JavaTemplate may leave:
+ *
+ * private static final Pattern EMAIL_PATTERN = ...;
+ *
+ * private static final Pattern PHONE_PATTERN = ...;
+ *
+ * Remove the extra blank line between consecutive
+ * generated Pattern constants.
+ */
+ if (candidates.size() > 1) {
+
+ List statements =
+ new ArrayList<>(
+ cd.getBody()
+ .getStatements()
+ );
+
+ String firstWhitespace =
+ statements.get(0)
+ .getPrefix()
+ .getWhitespace();
+
+ int lastNewline =
+ firstWhitespace.lastIndexOf('\n');
+
+ String indentation =
+ lastNewline >= 0
+ ? firstWhitespace.substring(
+ lastNewline + 1
+ )
+ : "";
+
+ /*
+ * The first N statements are the generated
+ * Pattern constants.
+ *
+ * Keep only one newline between them.
+ */
+ for (int i = 1;
+ i < candidates.size();
+ i++) {
+
+ statements.set(
+ i,
+ statements.get(i)
+ .withPrefix(
+ Space.format(
+ "\n" +
+ indentation
+ )
+ )
+ );
+ }
+
+ cd = cd.withBody(
+ cd.getBody()
+ .withStatements(statements)
+ );
+ }
+
+ return cd;
+ }
+
+ @Override
+ public J.MethodDeclaration visitMethodDeclaration(
+ J.MethodDeclaration method,
+ ExecutionContext ctx) {
+
+ if (method.getBody() == null) {
+ return super.visitMethodDeclaration(method, ctx);
+ }
+
+ J.ClassDeclaration enclosingClass =
+ getCursor()
+ .firstEnclosing(
+ J.ClassDeclaration.class
+ );
+
+ if (enclosingClass == null ||
+ enclosingClass.getType() == null) {
+
+ return super.visitMethodDeclaration(method, ctx);
+ }
+
+ /*
+ * Keep the first version conservative.
+ * Don't process inner/nested classes.
+ */
+ if (enclosingClass
+ .getType()
+ .getOwningClass() != null) {
+
+ return super.visitMethodDeclaration(method, ctx);
+ }
+
+ /*
+ * Only normal classes for now.
+ */
+ if (enclosingClass.getKind() !=
+ J.ClassDeclaration.Kind.Type.Class) {
+
+ return super.visitMethodDeclaration(method, ctx);
+ }
+
+ Cursor classCursor =
+ getCursor().dropParentUntil(
+ J.ClassDeclaration.class::isInstance
+ );
+
+ Set usedFieldNames =
+ classCursor.computeMessageIfAbsent(
+ USED_FIELD_NAMES,
+ key ->
+ collectExistingFieldNames(
+ enclosingClass
+ )
+ );
+
+ List candidates =
+ new ArrayList<>();
+
+ /*
+ * Version 1 only handles direct method statements.
+ *
+ * Example:
+ *
+ * void validate() {
+ * Pattern p = Pattern.compile("...");
+ * }
+ */
+ for (Statement statement :
+ method.getBody()
+ .getStatements()) {
+
+ if (!(statement instanceof
+ J.VariableDeclarations)) {
+ continue;
+ }
+
+ J.VariableDeclarations declarations =
+ (J.VariableDeclarations) statement;
+
+ /*
+ * Don't handle:
+ *
+ * Pattern a = ..., b = ...;
+ */
+ if (declarations
+ .getVariables()
+ .size() != 1) {
+
+ continue;
+ }
+
+ J.VariableDeclarations.NamedVariable variable =
+ declarations
+ .getVariables()
+ .get(0);
+
+ Expression initializer =
+ variable.getInitializer();
+
+ if (!(initializer instanceof
+ J.MethodInvocation)) {
+
+ continue;
+ }
+
+ J.MethodInvocation compile =
+ (J.MethodInvocation) initializer;
+
+ boolean oneArgument =
+ PATTERN_COMPILE.matches(compile);
+
+ boolean twoArguments =
+ PATTERN_COMPILE_WITH_FLAGS.matches(
+ compile
+ );
+
+ if (!oneArgument && !twoArguments) {
+ continue;
+ }
+
+ List arguments =
+ compile.getArguments();
+
+ if (arguments.isEmpty()) {
+ continue;
+ }
+
+ /*
+ * Version 1 only accepts literal regexes.
+ *
+ * YES:
+ *
+ * Pattern.compile("[0-9]+")
+ *
+ * NO:
+ *
+ * Pattern.compile(regex)
+ *
+ * NO:
+ *
+ * Pattern.compile(createRegex())
+ */
+ Expression regex =
+ arguments.get(0);
+
+ if (!(regex instanceof J.Literal)) {
+ continue;
+ }
+
+ Object literalValue =
+ ((J.Literal) regex).getValue();
+
+ if (!(literalValue instanceof String)) {
+ continue;
+ }
+
+ /*
+ * For:
+ *
+ * Pattern.compile(regex, flags)
+ *
+ * make sure flags are safe to move
+ * to class scope.
+ */
+ if (twoArguments) {
+
+ if (arguments.size() != 2 ||
+ !isConstantExpression(
+ arguments.get(1)
+ )) {
+
+ continue;
+ }
+ }
+
+ JavaType.Variable variableType =
+ variable.getVariableType();
+
+ /*
+ * Need type attribution so identifier
+ * replacement is safe.
+ */
+ if (variableType == null) {
+ continue;
+ }
+
+ String constantName =
+ createConstantName(
+ variable.getSimpleName(),
+ method.getSimpleName()
+ );
+
+ constantName =
+ makeUnique(
+ constantName,
+ usedFieldNames
+ );
+
+ usedFieldNames.add(constantName);
+
+ JavaType.Variable newFieldType =
+ variableType
+ .withName(constantName)
+ .withOwner(
+ enclosingClass.getType()
+ );
+
+ Set flags =
+ new HashSet<>(
+ newFieldType.getFlags()
+ );
+
+ flags.add(Flag.Private);
+ flags.add(Flag.Static);
+ flags.add(Flag.Final);
+
+ newFieldType =
+ newFieldType.withFlags(flags);
+
+ Candidate candidate =
+ new Candidate(
+ declarations.getId(),
+ variableType,
+ newFieldType,
+ constantName,
+ compile.withPrefix(
+ Space.EMPTY
+ )
+ );
+
+ candidates.add(candidate);
+ }
+
+ if (candidates.isEmpty()) {
+ return super.visitMethodDeclaration(
+ method,
+ ctx
+ );
+ }
+
+ /*
+ * Child visitors use this to:
+ *
+ * 1. replace identifiers
+ * 2. remove local declarations
+ */
+ getCursor().putMessage(
+ METHOD_CANDIDATES,
+ candidates
+ );
+
+ /*
+ * Tell enclosing class which static fields
+ * need to be generated.
+ */
+ List classCandidates =
+ classCursor.computeMessageIfAbsent(
+ CLASS_CANDIDATES,
+ key -> new ArrayList<>()
+ );
+
+ classCandidates.addAll(candidates);
+
+ return super.visitMethodDeclaration(
+ method,
+ ctx
+ );
+ }
+
+ /*
+ * Replace:
+ *
+ * emailPattern.matcher(...)
+ *
+ * with:
+ *
+ * EMAIL_PATTERN.matcher(...)
+ */
+ @Override
+ public J.Identifier visitIdentifier(
+ J.Identifier identifier,
+ ExecutionContext ctx) {
+
+ J.Identifier id =
+ super.visitIdentifier(
+ identifier,
+ ctx
+ );
+
+ List candidates =
+ getCursor().getNearestMessage(
+ METHOD_CANDIDATES
+ );
+
+ if (candidates == null) {
+ return id;
+ }
+
+ for (Candidate candidate : candidates) {
+
+ /*
+ * Match the actual variable metadata,
+ * not just its String name.
+ */
+ if (Objects.equals(
+ id.getFieldType(),
+ candidate.originalVariableType)) {
+
+ return id
+ .withSimpleName(
+ candidate.constantName
+ )
+ .withFieldType(
+ candidate.newFieldType
+ );
+ }
+ }
+
+ return id;
+ }
+
+ /*
+ * Remove:
+ *
+ * Pattern emailPattern =
+ * Pattern.compile(...);
+ */
+ @Override
+ public J.VariableDeclarations
+ visitVariableDeclarations(
+ J.VariableDeclarations declarations,
+ ExecutionContext ctx) {
+
+ List candidates =
+ getCursor().getNearestMessage(
+ METHOD_CANDIDATES
+ );
+
+ if (candidates != null) {
+
+ for (Candidate candidate :
+ candidates) {
+
+ if (declarations
+ .getId()
+ .equals(
+ candidate.declarationId
+ )) {
+
+ return null;
+ }
+ }
+ }
+
+ return super.visitVariableDeclarations(
+ declarations,
+ ctx
+ );
+ }
+ };
+ }
+
+ /*
+ * Checks whether Pattern.compile(..., flags)
+ * uses flags that are safe to move to a
+ * static field.
+ */
+ private static boolean isConstantExpression(
+ Expression expression) {
+
+ if (expression instanceof J.Literal) {
+ return true;
+ }
+
+ if (expression instanceof J.Identifier) {
+
+ JavaType.Variable variable =
+ ((J.Identifier) expression)
+ .getFieldType();
+
+ return variable != null &&
+ variable.hasFlags(
+ Flag.Static,
+ Flag.Final
+ );
+ }
+
+ if (expression instanceof J.FieldAccess) {
+
+ JavaType.Variable variable =
+ ((J.FieldAccess) expression)
+ .getName()
+ .getFieldType();
+
+ return variable != null &&
+ variable.hasFlags(
+ Flag.Static,
+ Flag.Final
+ );
+ }
+
+ /*
+ * Supports:
+ *
+ * Pattern.CASE_INSENSITIVE |
+ * Pattern.UNICODE_CASE
+ */
+ if (expression instanceof J.Binary) {
+
+ J.Binary binary =
+ (J.Binary) expression;
+
+ return isConstantExpression(
+ binary.getLeft()
+ ) &&
+ isConstantExpression(
+ binary.getRight()
+ );
+ }
+
+ return false;
+ }
+
+ /*
+ * emailPattern -> EMAIL_PATTERN
+ * phonePattern -> PHONE_PATTERN
+ *
+ * If the variable is simply:
+ *
+ * Pattern pattern
+ *
+ * use the method name where possible.
+ *
+ * isValidEmail -> EMAIL_PATTERN
+ */
+ private static String createConstantName(
+ String variableName,
+ String methodName) {
+
+ if (!"pattern".equals(variableName)) {
+ return toUpperSnakeCase(variableName);
+ }
+
+ String subject = methodName;
+
+ String[] prefixes = {
+ "isValid",
+ "validate",
+ "valid",
+ "matches",
+ "match"
+ };
+
+ for (String prefix : prefixes) {
+
+ if (subject.startsWith(prefix) &&
+ subject.length() > prefix.length()) {
+
+ subject =
+ subject.substring(
+ prefix.length()
+ );
+
+ break;
+ }
+ }
+
+ if (subject.isEmpty()) {
+ return "PATTERN";
+ }
+
+ String result =
+ toUpperSnakeCase(subject);
+
+ if (!result.endsWith("_PATTERN")) {
+ result += "_PATTERN";
+ }
+
+ return result;
+ }
+
+ /*
+ * emailPattern -> EMAIL_PATTERN
+ * phonePattern -> PHONE_PATTERN
+ */
+ private static String toUpperSnakeCase(
+ String name) {
+
+ StringBuilder result =
+ new StringBuilder();
+
+ for (int i = 0;
+ i < name.length();
+ i++) {
+
+ char current =
+ name.charAt(i);
+
+ if (!Character.isLetterOrDigit(current)) {
+
+ if (result.length() > 0 &&
+ result.charAt(
+ result.length() - 1
+ ) != '_') {
+
+ result.append('_');
+ }
+
+ continue;
+ }
+
+ if (Character.isUpperCase(current) &&
+ i > 0) {
+
+ char previous =
+ name.charAt(i - 1);
+
+ if (Character.isLowerCase(previous) ||
+ Character.isDigit(previous)) {
+
+ result.append('_');
+ }
+ }
+
+ result.append(
+ Character.toUpperCase(current)
+ );
+ }
+
+ return result.toString();
+ }
+
+ /*
+ * Avoid duplicate generated field names.
+ *
+ * EMAIL_PATTERN
+ * EMAIL_PATTERN_2
+ * EMAIL_PATTERN_3
+ */
+ private static String makeUnique(
+ String desiredName,
+ Set usedNames) {
+
+ if (!usedNames.contains(desiredName)) {
+ return desiredName;
+ }
+
+ int number = 2;
+
+ while (usedNames.contains(
+ desiredName + "_" + number)) {
+
+ number++;
+ }
+
+ return desiredName + "_" + number;
+ }
+
+ /*
+ * Collect fields that already exist in the class
+ * so generated constants don't collide with them.
+ */
+ private static Set
+ collectExistingFieldNames(
+ J.ClassDeclaration classDeclaration) {
+
+ Set names =
+ new HashSet<>();
+
+ for (Statement statement :
+ classDeclaration
+ .getBody()
+ .getStatements()) {
+
+ if (!(statement instanceof
+ J.VariableDeclarations)) {
+
+ continue;
+ }
+
+ J.VariableDeclarations declarations =
+ (J.VariableDeclarations) statement;
+
+ for (J.VariableDeclarations.NamedVariable variable :
+ declarations.getVariables()) {
+
+ names.add(
+ variable.getSimpleName()
+ );
+ }
+ }
+
+ return names;
+ }
+
+ private static class Candidate {
+
+ private final UUID declarationId;
+
+ private final JavaType.Variable
+ originalVariableType;
+
+ private final JavaType.Variable
+ newFieldType;
+
+ private final String constantName;
+
+ private final J.MethodInvocation
+ initializer;
+
+ private Candidate(
+ UUID declarationId,
+ JavaType.Variable originalVariableType,
+ JavaType.Variable newFieldType,
+ String constantName,
+ J.MethodInvocation initializer) {
+
+ this.declarationId =
+ declarationId;
+
+ this.originalVariableType =
+ originalVariableType;
+
+ this.newFieldType =
+ newFieldType;
+
+ this.constantName =
+ constantName;
+
+ this.initializer =
+ initializer;
+ }
+ }
+}
diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv
index 417d4db7a..1bbde587c 100644
--- a/src/main/resources/META-INF/rewrite/recipes.csv
+++ b/src/main/resources/META-INF/rewrite/recipes.csv
@@ -9,6 +9,7 @@ maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanaly
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.AnnotateRequiredParameters,Annotate required method parameters with `@NonNull`,"Add `@NonNull` to parameters of public methods that are explicitly checked for `null` and throw an exception if null. By default `org.jspecify.annotations.NonNull` is used, but through the `nonNullAnnotationClass` option a custom annotation can be provided. When providing a custom `nonNullAnnotationClass` that annotation should be meta annotated with `@Target(TYPE_USE)`. This recipe scans for methods that do not already have parameters annotated with `@NonNull` annotation and checks for null validation patterns that throw exceptions, such as `if (param == null) throw new IllegalArgumentException()`.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,"[{""name"":""nonNullAnnotationClass"",""type"":""String"",""displayName"":""`@NonNull` annotation class"",""description"":""The fully qualified name of the @NonNull annotation. The annotation should be meta annotated with `@Target(TYPE_USE)`. Defaults to `org.jspecify.annotations.NonNull`"",""example"":""org.jspecify.annotations.NonNull""}]",
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.AtomicPrimitiveEqualsUsesGet,"Atomic Boolean, Integer, and Long equality checks compare their values","`AtomicBoolean#equals(Object)`, `AtomicInteger#equals(Object)` and `AtomicLong#equals(Object)` are only equal to their instance. This recipe converts `a.equals(b)` to `a.get() == b.get()`. These atomic classes do not override `equals` from `Object`, so calling it compares object identity rather than the wrapped value, which is almost never the intended behavior.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.AvoidBoxedBooleanExpressions,Avoid boxed boolean expressions,"Under certain conditions the `java.lang.Boolean` type is used as an expression, and it may throw a `NullPointerException` if the value is null. Using `Boolean.TRUE.equals(...)` guards against unboxing a `null` reference in control flow positions like `if` conditions and ternary operators.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
+maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.AvoidRepeatedPatternCompile,Avoid repeated `Pattern.compile()` calls,Moves `Pattern.compile()` calls with constant regular expressions from method-local variables into private static final fields.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.BigDecimalDoubleConstructorRecipe,`new BigDecimal(double)` should not be used,"Use of `new BigDecimal(double)` constructor can lead to loss of precision. Use `BigDecimal.valueOf(double)` instead.
For example writing `new BigDecimal(0.1)` does not create a `BigDecimal` which is exactly equal to `0.1`, but it is equal to `0.1000000000000000055511151231257827021181583404541015625`. This is because `0.1` cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). `BigDecimal.valueOf` avoids this by converting through a string representation, preserving the value you actually intended.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.BigDecimalRoundingConstantsToEnums,`BigDecimal` rounding constants to `RoundingMode` enums,Convert `BigDecimal` rounding constants to the equivalent `RoundingMode` enum. The integer-based rounding constants on `BigDecimal` are deprecated and lack type safety; the `RoundingMode` enum makes the rounding behavior self-documenting and prevents invalid values.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
diff --git a/src/test/java/org/openrewrite/staticanalysis/AvoidRepeatedPatternCompileTest.java b/src/test/java/org/openrewrite/staticanalysis/AvoidRepeatedPatternCompileTest.java
new file mode 100644
index 000000000..7ab2c0b14
--- /dev/null
+++ b/src/test/java/org/openrewrite/staticanalysis/AvoidRepeatedPatternCompileTest.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * 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.staticanalysis;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.Issue;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+
+class AvoidRepeatedPatternCompileTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new AvoidRepeatedPatternCompile());
+ }
+
+ /**
+ * Main happy path:
+ *
+ * Pattern compiled inside method from literal regex
+ * becomes static final field.
+ */
+ @Test
+ @DocumentExample
+ @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/622")
+ void extractLiteralPatternToConstant() {
+ rewriteRun(
+ java(
+ """
+ import java.util.regex.Pattern;
+
+ class EmailValidator {
+
+ boolean valid(String email) {
+ Pattern emailPattern = Pattern.compile("[a-z]+");
+ return emailPattern.matcher(email).matches();
+ }
+ }
+ """,
+ """
+ import java.util.regex.Pattern;
+
+ class EmailValidator {
+
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-z]+");
+
+ boolean valid(String email) {
+ return EMAIL_PATTERN.matcher(email).matches();
+ }
+ }
+ """
+ )
+ );
+ }
+
+ /**
+ * Dynamic regex must NOT be moved.
+ *
+ * We don't know its value until runtime.
+ */
+ @Test
+ void doNotChangeDynamicRegex() {
+ rewriteRun(
+ java(
+ """
+ import java.util.regex.Pattern;
+
+ class Validator {
+
+ boolean valid(String value, String regex) {
+ Pattern pattern = Pattern.compile(regex);
+ return pattern.matcher(value).matches();
+ }
+ }
+ """
+ )
+ );
+ }
+
+ /**
+ * Regex returned from another method is runtime-generated,
+ * so it must NOT be moved.
+ */
+ @Test
+ void doNotChangeMethodGeneratedRegex() {
+ rewriteRun(
+ java(
+ """
+ import java.util.regex.Pattern;
+
+ class Validator {
+
+ boolean valid(String value) {
+ Pattern pattern = Pattern.compile(createRegex());
+ return pattern.matcher(value).matches();
+ }
+
+ private String createRegex() {
+ return "[a-z]+";
+ }
+ }
+ """
+ )
+ );
+ }
+
+ /**
+ * Pattern.compile(String, int) should work when the flags
+ * are compile-time constants.
+ */
+ @Test
+ void extractPatternWithConstantFlags() {
+ rewriteRun(
+ java(
+ """
+ import java.util.regex.Pattern;
+
+ class EmailValidator {
+
+ boolean valid(String email) {
+ Pattern emailPattern = Pattern.compile("[a-z]+", Pattern.CASE_INSENSITIVE);
+ return emailPattern.matcher(email).matches();
+ }
+ }
+ """,
+ """
+ import java.util.regex.Pattern;
+
+ class EmailValidator {
+
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-z]+", Pattern.CASE_INSENSITIVE);
+
+ boolean valid(String email) {
+ return EMAIL_PATTERN.matcher(email).matches();
+ }
+ }
+ """
+ )
+ );
+ }
+
+ /**
+ * Runtime flags must NOT be moved to a static field.
+ */
+ @Test
+ void doNotChangeDynamicFlags() {
+ rewriteRun(
+ java(
+ """
+ import java.util.regex.Pattern;
+
+ class Validator {
+
+ boolean valid(String value, int flags) {
+ Pattern pattern = Pattern.compile("[a-z]+", flags);
+ return pattern.matcher(value).matches();
+ }
+ }
+ """
+ )
+ );
+ }
+
+ /**
+ * This verifies that we use JavaType.Variable rather
+ * than blindly replacing everything named "pattern".
+ */
+ @Test
+ void doesNotReplaceUnrelatedVariableWithSameName() {
+ rewriteRun(
+ java(
+ """
+ import java.util.regex.Pattern;
+
+ class Validator {
+
+ boolean first(String value) {
+ Pattern pattern = Pattern.compile("[a-z]+");
+ return pattern.matcher(value).matches();
+ }
+
+ boolean second() {
+ String pattern = "hello";
+ return pattern.isEmpty();
+ }
+ }
+ """,
+ """
+ import java.util.regex.Pattern;
+
+ class Validator {
+
+ private static final Pattern FIRST_PATTERN = Pattern.compile("[a-z]+");
+
+ boolean first(String value) {
+ return FIRST_PATTERN.matcher(value).matches();
+ }
+
+ boolean second() {
+ String pattern = "hello";
+ return pattern.isEmpty();
+ }
+ }
+ """
+ )
+ );
+ }
+
+ /**
+ * More than one local Pattern should result in
+ * more than one static final field.
+ */
+ @Test
+ void multiplePatternsInSameClass() {
+ rewriteRun(
+ java(
+ """
+ import java.util.regex.Pattern;
+
+ class Validator {
+
+ boolean valid(String email, String phone) {
+ Pattern emailPattern = Pattern.compile("[a-z]+");
+ Pattern phonePattern = Pattern.compile("[0-9]+");
+ return emailPattern.matcher(email).matches() &&
+ phonePattern.matcher(phone).matches();
+ }
+ }
+ """,
+ """
+ import java.util.regex.Pattern;
+
+ class Validator {
+
+ private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-z]+");
+ private static final Pattern PHONE_PATTERN = Pattern.compile("[0-9]+");
+
+ boolean valid(String email, String phone) {
+ return EMAIL_PATTERN.matcher(email).matches() &&
+ PHONE_PATTERN.matcher(phone).matches();
+ }
+ }
+ """
+ )
+ );
+ }
+}