Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <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.java.migrate.search;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.J;
import org.openrewrite.marker.SearchResult;

import java.util.Arrays;
import java.util.List;

/**
* Finds usages of locale-based date/time formatting APIs that may be affected by the
* JDK 20+ CLDR locale data changes. Starting with JDK 20, the Unicode CLDR 42 locale data
* changed the space character before AM/PM designators from a regular space to a narrow
* no-break space (NNBSP, \u202F).
* <p>
* This can cause parsing issues when user input contains regular spaces but the formatter
* expects NNBSP. The affected APIs include locale-based DateFormat and DateTimeFormatter
* factory methods.
*
* @see <a href="https://bugs.openjdk.org/browse/JDK-8324308">JDK-8324308</a>
* @see <a href="https://inside.java/2024/03/29/quality-heads-up/">Unicode CLDR Version 42 Heads-up</a>
*/
@EqualsAndHashCode(callSuper = false)
@Value
public class FindLocaleDateTimeFormats extends Recipe {

// DateFormat factory methods that return locale-sensitive formatters
private static final MethodMatcher DATE_FORMAT_GET_TIME_INSTANCE =
new MethodMatcher("java.text.DateFormat getTimeInstance(..)", true);
private static final MethodMatcher DATE_FORMAT_GET_DATE_TIME_INSTANCE =
new MethodMatcher("java.text.DateFormat getDateTimeInstance(..)", true);
private static final MethodMatcher DATE_FORMAT_GET_INSTANCE =
new MethodMatcher("java.text.DateFormat getInstance(..)", true);

// DateTimeFormatter factory methods that return locale-sensitive formatters
private static final MethodMatcher DTF_OF_LOCALIZED_TIME =
new MethodMatcher("java.time.format.DateTimeFormatter ofLocalizedTime(..)", true);
private static final MethodMatcher DTF_OF_LOCALIZED_DATE_TIME =
new MethodMatcher("java.time.format.DateTimeFormatter ofLocalizedDateTime(..)", true);

private static final List<MethodMatcher> ALL_MATCHERS = Arrays.asList(
DATE_FORMAT_GET_TIME_INSTANCE,
DATE_FORMAT_GET_DATE_TIME_INSTANCE,
DATE_FORMAT_GET_INSTANCE,
DTF_OF_LOCALIZED_TIME,
DTF_OF_LOCALIZED_DATE_TIME
);

String displayName = "Find locale-sensitive date/time formatting";

String description = "Finds usages of locale-based date/time formatting APIs that may be affected by " +
"JDK 20+ CLDR locale data changes, where the space before AM/PM was changed " +
"from a regular space to a narrow no-break space (NNBSP).";

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.or(
new UsesMethod<>("java.text.DateFormat get*Instance(..)", true),
new UsesMethod<>("java.time.format.DateTimeFormatter ofLocalized*Time(..)", true)
),
new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
for (MethodMatcher matcher : ALL_MATCHERS) {
if (matcher.matches(mi)) {
return SearchResult.found(mi, "JDK 20+ CLDR: may use NNBSP before AM/PM");
}
}
return mi;
}
}
);
}
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/rewrite/recipes.csv
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.s
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.search.FindDtoOverfetching,Find methods that only use one DTO data element,Find methods that have 'opportunities' for improvement.,1,,Search,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""dtoType"",""type"":""String"",""displayName"":""DTO type"",""description"":""The fully qualified name of the DTO."",""example"":""com.example.dto.*"",""required"":true}]",
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.search.FindInternalJavaxApis,Find uses of internal javax APIs,The libraries that define these APIs will have to be migrated before any of the repositories that use them.,1,,Search,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""methodPattern"",""type"":""String"",""displayName"":""Method pattern"",""description"":""Optionally limit the search to declarations that match the provided method pattern."",""example"":""java.util.List add(..)""}]","[{""name"":""org.openrewrite.java.table.MethodCalls"",""displayName"":""Method calls"",""description"":""The text of matching method invocations."",""columns"":[{""name"":""sourceFile"",""type"":""String"",""displayName"":""Source file"",""description"":""The source file that the method call occurred in.""},{""name"":""method"",""type"":""String"",""displayName"":""Method call"",""description"":""The text of the method call.""},{""name"":""className"",""type"":""String"",""displayName"":""Class name"",""description"":""The class name of the method call.""},{""name"":""methodName"",""type"":""String"",""displayName"":""Method name"",""description"":""The method name of the method call.""},{""name"":""argumentTypes"",""type"":""String"",""displayName"":""Argument types"",""description"":""The argument types of the method call.""}]}]"
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.search.FindJavaVersion,Find Java versions in use,Finds Java versions in use.,1,,Search,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.migrate.table.JavaVersionTable"",""displayName"":""Java version table"",""description"":""Records versions of Java in use"",""columns"":[{""name"":""sourceVersion"",""type"":""String"",""displayName"":""Source compatibility"",""description"":""The version of Java used to compile the source code""},{""name"":""targetVersion"",""type"":""String"",""displayName"":""Target compatibility"",""description"":""The version of Java the bytecode is compiled to run on""}]}]"
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.search.FindLocaleDateTimeFormats,Find locale-sensitive date/time formatting,"Finds usages of locale-based date/time formatting APIs that may be affected by JDK 20+ CLDR locale data changes, where the space before AM/PM was changed from a regular space to a narrow no-break space (NNBSP).",1,,Search,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.search.PlanJavaMigration,Plan a Java version migration,Study the set of Java versions and associated tools in use across many repositories.,1,,Search,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.java.migrate.table.JavaVersionMigrationPlan"",""displayName"":""Java version migration plan"",""description"":""A per-repository view of the current state of Java versions and associated build tools"",""columns"":[{""name"":""hasJava"",""type"":""boolean"",""displayName"":""Has Java"",""description"":""Whether this is a Java repository at all.""},{""name"":""sourceCompatibility"",""type"":""String"",""displayName"":""Source compatibility"",""description"":""The source compatibility of the source file.""},{""name"":""majorVersionSourceCompatibility"",""type"":""Integer"",""displayName"":""Major version source compatibility"",""description"":""The major version.""},{""name"":""targetCompatibility"",""type"":""String"",""displayName"":""Target compatibility"",""description"":""The target compatibility or `--release` version of the source file.""},{""name"":""gradleVersion"",""type"":""String"",""displayName"":""Gradle version"",""description"":""The version of Gradle in use, if any.""},{""name"":""hasGradleBuild"",""type"":""Boolean"",""displayName"":""Has Gradle build"",""description"":""Whether a build.gradle file exists in the repository.""},{""name"":""mavenVersion"",""type"":""String"",""displayName"":""Maven version"",""description"":""The version of Maven in use, if any.""},{""name"":""hasMavenPom"",""type"":""Boolean"",""displayName"":""Has Maven pom"",""description"":""Whether a pom.xml file exists in the repository.""}]}]"
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.sql.MigrateDriverManagerSetLogStream,Use `DriverManager#setLogWriter(java.io.PrintWriter)`,Use `DriverManager#setLogWriter(java.io.PrintWriter)` instead of the deprecated `DriverManager#setLogStream(java.io.PrintStream)` in Java 1.2 or higher.,1,,`java.sql` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.sql.JavaSqlAPIs,Use modernized `java.sql` APIs,"Certain Java sql APIs have become deprecated and their usages changed, necessitating usage changes.",3,,`java.sql` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <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.java.migrate.search;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

class FindLocaleDateTimeFormatsTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new FindLocaleDateTimeFormats());
}

@DocumentExample
@Test
void findDateFormatGetTimeInstance() {
rewriteRun(
//language=java
java(
"""
import java.text.DateFormat;
import java.util.Date;
class Test {
void test() {
DateFormat df = DateFormat.getTimeInstance();
String formatted = df.format(new Date());
}
}
""",
"""
import java.text.DateFormat;
import java.util.Date;
class Test {
void test() {
DateFormat df = /*~~(JDK 20+ CLDR: may use NNBSP before AM/PM)~~>*/DateFormat.getTimeInstance();
String formatted = df.format(new Date());
}
}
"""
)
);
}

@CsvSource(textBlock = """
DateFormat.getTimeInstance(DateFormat.SHORT)
DateFormat.getDateTimeInstance()
DateFormat.getInstance()
""")
@ParameterizedTest
void findDateFormatMethods(String methodCall) {
rewriteRun(
java(
"""
import java.text.DateFormat;
import java.util.Date;
class Test {
void test() {
DateFormat df = %s;
String formatted = df.format(new Date());
}
}
""".formatted(methodCall),
"""
import java.text.DateFormat;
import java.util.Date;
class Test {
void test() {
DateFormat df = /*~~(JDK 20+ CLDR: may use NNBSP before AM/PM)~~>*/%s;
String formatted = df.format(new Date());
}
}
""".formatted(methodCall)
)
);
}

@CsvSource(textBlock = """
'DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM)', 'LocalTime.now()'
'DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)', 'LocalDateTime.now()'
'DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)', 'LocalDateTime.now()'
""")
@ParameterizedTest
void findDateTimeFormatterMethods(String methodCall, String formatArg) {
rewriteRun(
java(
"""
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
class Test {
void test() {
DateTimeFormatter dtf = %s;
String formatted = dtf.format(%s);
}
}
""".formatted(methodCall, formatArg),
"""
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
class Test {
void test() {
DateTimeFormatter dtf = /*~~(JDK 20+ CLDR: may use NNBSP before AM/PM)~~>*/%s;
String formatted = dtf.format(%s);
}
}
""".formatted(methodCall, formatArg)
)
);
}

@Test
void findMultipleUsages() {
rewriteRun(
//language=java
java(
"""
import java.text.DateFormat;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Date;
class Test {
void test() {
DateFormat df1 = DateFormat.getTimeInstance();
DateFormat df2 = DateFormat.getDateTimeInstance();
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
}
}
""",
"""
import java.text.DateFormat;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Date;
class Test {
void test() {
DateFormat df1 = /*~~(JDK 20+ CLDR: may use NNBSP before AM/PM)~~>*/DateFormat.getTimeInstance();
DateFormat df2 = /*~~(JDK 20+ CLDR: may use NNBSP before AM/PM)~~>*/DateFormat.getDateTimeInstance();
DateTimeFormatter dtf = /*~~(JDK 20+ CLDR: may use NNBSP before AM/PM)~~>*/DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
}
}
"""
)
);
}

@Nested
class NoChange {
@Test
void noMatchForExplicitPattern() {
rewriteRun(
//language=java
java(
"""
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
class Test {
void test() {
// Explicit patterns are not affected by CLDR changes
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss a");
String formatted = dtf.format(LocalDateTime.now());
}
}
"""
)
);
}

@Test
void noMatchForDateFormatGetDateInstance() {
rewriteRun(
//language=java
java(
"""
import java.text.DateFormat;
import java.util.Date;
class Test {
void test() {
// Date-only formatting doesn't include AM/PM
DateFormat df = DateFormat.getDateInstance();
String formatted = df.format(new Date());
}
}
"""
)
);
}
}
}