diff --git a/src/main/java/org/openrewrite/staticanalysis/ReplaceWeekYearWithYear.java b/src/main/java/org/openrewrite/staticanalysis/ReplaceWeekYearWithYear.java new file mode 100644 index 0000000000..6262fc2c97 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/ReplaceWeekYearWithYear.java @@ -0,0 +1,124 @@ +/* + * 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.staticanalysis; + +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.*; + +import java.util.*; + +public class ReplaceWeekYearWithYear extends Recipe { + private static final MethodMatcher SIMPLE_DATE_FORMAT_CONSTRUCTOR_MATCHER = new MethodMatcher("java.text.SimpleDateFormat (..)"); + private static final MethodMatcher OF_PATTERN_MATCHER = new MethodMatcher("java.time.format.DateTimeFormatter ofPattern(..)"); + + @Override + public String getDisplayName() { + return "Week Year (YYYY) should not be used for date formatting"; + } + + @Override + public String getDescription() { + return "For most dates Week Year (YYYY) and Year (yyyy) yield the same results. However, on the last week of" + + " December and first week of January Week Year could produce unexpected results."; + } + + @Override + public Set getTags() { + return Collections.singleton("RSPEC-3986"); + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.or( + new UsesType<>("java.util.Date", false), + new UsesType<>("java.time.format.DateTimeFormatter", false), + new UsesType<>("java.text.SimpleDateFormat", false) + ), + new ReplaceWeekYearVisitor() + ); + } + + private static class ReplaceWeekYearVisitor extends JavaIsoVisitor { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, ExecutionContext ctx) { + if (OF_PATTERN_MATCHER.matches(mi)) { + getCursor().putMessage("KEY", mi); + } + + return super.visitMethodInvocation(mi, ctx); + } + + @Override + public J.NewClass visitNewClass(J.NewClass nc, ExecutionContext ctx) { + if (SIMPLE_DATE_FORMAT_CONSTRUCTOR_MATCHER.matches(nc)) { + getCursor().putMessage("KEY", nc); + } + + return super.visitNewClass(nc, ctx); + } + + @Override + public J.Literal visitLiteral(J.Literal li, ExecutionContext ctx) { + if (li.getValue() instanceof String) { + Cursor c = getCursor().dropParentWhile(is -> is instanceof J.Parentheses || !(is instanceof Tree)); + if (c.getMessage("KEY") != null) { + Object value = li.getValue(); + + if (value == null) { + return li; + } + + String newValue = replaceY(value.toString()); + + if (newValue.equals(value.toString())) { + return li; + } + + return li.withValueSource("\""+newValue+"\"").withValue(newValue); + } + } + + return li; + } + + public static String replaceY(String input) { + StringBuilder output = new StringBuilder(); + boolean insideQuotes = false; + + for (int i = 0; i < input.length(); i++) { + char currentChar = input.charAt(i); + char nextChar = (i < input.length() - 1) ? input.charAt(i + 1) : '\0'; + + if (currentChar == '\'') { + insideQuotes = !insideQuotes; + output.append(currentChar); + } else if (currentChar == 'Y' && !insideQuotes) { + output.append('y'); + } else if (currentChar == 'Y' && nextChar == '\'') { + output.append(currentChar); + } else { + output.append(currentChar); + } + } + + return output.toString(); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/openrewrite/staticanalysis/ReplaceWeekYearWithYearTest.java b/src/test/java/org/openrewrite/staticanalysis/ReplaceWeekYearWithYearTest.java new file mode 100644 index 0000000000..0a7b8bd506 --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/ReplaceWeekYearWithYearTest.java @@ -0,0 +1,261 @@ +/* + * 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.staticanalysis; + +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.Issue; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +@SuppressWarnings("SuspiciousDateFormat") +class ReplaceWeekYearWithYearTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec + .recipe(new ReplaceWeekYearWithYear()); + } + + @Test + @DocumentExample + @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/58") + void changeSimpleDateFormat() { + //language=java + rewriteRun( + java( + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31"); + String result = new SimpleDateFormat("YYYY/MM/dd").format(date); + } + } + """, + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31"); + String result = new SimpleDateFormat("yyyy/MM/dd").format(date); + } + } + """ + ) + ); + } + + @Test + void worksWithOfPatternFormatter() { + //language=java + rewriteRun( + java( + """ + import java.text.SimpleDateFormat; + import java.time.format.DateTimeFormatter; + import java.util.Date; + + class Test { + public void formatDate() { + Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31"); + String result = DateTimeFormatter.ofPattern("YYYY/MM/dd").format(date.toInstant()); + } + } + """, + """ + import java.text.SimpleDateFormat; + import java.time.format.DateTimeFormatter; + import java.util.Date; + + class Test { + public void formatDate() { + Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31"); + String result = DateTimeFormatter.ofPattern("yyyy/MM/dd").format(date.toInstant()); + } + } + """ + ) + ); + } + + @Test + void worksWithYYUses() { + //language=java + rewriteRun( + java( + """ + import java.text.SimpleDateFormat; + import java.time.format.DateTimeFormatter; + import java.util.Date; + + class Test { + public void formatDate() { + Date date = new SimpleDateFormat("yy/MM/dd").parse("2015/12/31"); + String result = DateTimeFormatter.ofPattern("YY/MM/dd").format(date.toInstant()); + } + } + """, + """ + import java.text.SimpleDateFormat; + import java.time.format.DateTimeFormatter; + import java.util.Date; + + class Test { + public void formatDate() { + Date date = new SimpleDateFormat("yy/MM/dd").parse("2015/12/31"); + String result = DateTimeFormatter.ofPattern("yy/MM/dd").format(date.toInstant()); + } + } + """ + ) + ); + } + + @Test + void onlyRunsWhenFormatAndDateTypesAreUsed() { + //language=java + rewriteRun( + java( + """ + class Test { + public static void main(String[] args) { + String pattern = "YYYY/MM/dd"; + System.out.println(pattern); + } + } + """ + ) + ); + } + + @Test + void standaloneNewClassCall() { + //language=java + rewriteRun( + java( + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + SimpleDateFormat format = new SimpleDateFormat("YYYY-MM-dd"); + Date date = format.parse("2015/12/31"); + } + } + """, + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + Date date = format.parse("2015/12/31"); + } + } + """ + ) + ); + } + @Test + void patternUsesSingleQuotes() { + //language=java + rewriteRun( + java( + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + SimpleDateFormat format = new SimpleDateFormat("'Your date is:' YYYY-MM-dd"); + Date date = format.parse("2015/12/31"); + } + } + """, + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + SimpleDateFormat format = new SimpleDateFormat("'Your date is:' yyyy-MM-dd"); + Date date = format.parse("2015/12/31"); + } + } + """ + ) + ); + } + + @Test + void patternUsesMultipleSingleQuotes() { + //language=java + rewriteRun( + java( + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + SimpleDateFormat format = new SimpleDateFormat("'Your date is:' YYYY-MM-dd, 'yy'"); + Date date = format.parse("2015/12/31"); + } + } + """, + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + SimpleDateFormat format = new SimpleDateFormat("'Your date is:' yyyy-MM-dd, 'yy'"); + Date date = format.parse("2015/12/31"); + } + } + """ + ) + ); + } + + @Test + void doesNotChangeWhyInSingleQuotes() { + //language=java + rewriteRun( + java( + """ + import java.text.SimpleDateFormat; + import java.util.Date; + + class Test { + public void formatDate() { + SimpleDateFormat format = new SimpleDateFormat("'Y' dd-MM"); + Date date = format.parse("2015/12/31"); + } + } + """ + ) + ); + } +} \ No newline at end of file