diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/SpecifyLocale.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/SpecifyLocale.java new file mode 100644 index 00000000000..ad2e96d9f0c --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/SpecifyLocale.java @@ -0,0 +1,63 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.errorprone.BugPattern.LinkType.CUSTOM; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.BugPattern.StandardTags.LIKELY_ERROR; +import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod; +import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.fixes.Fix; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import tech.picnic.errorprone.bugpatterns.util.SourceCode; + +/** + * A {@link BugChecker} that flags {@link String#toLowerCase()} or {@link String#toUpperCase()} + * which do not specify a Locale. + */ +@AutoService(BugChecker.class) +@BugPattern( + summary = + "Specify `Locale.ROOT` or `Locale.getDefault()` when calling `String#to{Lower,Upper}Case` without a specific Locale", + link = BUG_PATTERNS_BASE_URL + "SpecifyLocale", + linkType = CUSTOM, + severity = WARNING, + tags = LIKELY_ERROR) +public final class SpecifyLocale extends BugChecker implements MethodInvocationTreeMatcher { + private static final long serialVersionUID = 1L; + private static final Matcher STRING_TO_UPPER_OR_LOWER_CASE = + instanceMethod() + .onExactClass(String.class.getName()) + .namedAnyOf("toLowerCase", "toUpperCase"); + + /** Instantiates a new {@link SpecifyLocale} instance. */ + public SpecifyLocale() {} + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (STRING_TO_UPPER_OR_LOWER_CASE.matches(tree, state) && tree.getArguments().isEmpty()) { + return buildDescription(tree) + .addFix(buildFix("Locale.ROOT", tree, state)) + .addFix(buildFix("Locale.getDefault()", tree, state)) + .build(); + } + return Description.NO_MATCH; + } + + private static Fix buildFix( + String localeToSpecify, MethodInvocationTree tree, VisitorState state) { + return SuggestedFix.builder() + .replace( + tree, SourceCode.treeToString(tree, state).replace("()", "(" + localeToSpecify + ")")) + .addImport("java.util.Locale") + .build(); + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/SpecifyLocaleTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/SpecifyLocaleTest.java new file mode 100644 index 00000000000..d611a62cc64 --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/SpecifyLocaleTest.java @@ -0,0 +1,95 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.errorprone.BugCheckerRefactoringTestHelper.newInstance; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers; +import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +final class SpecifyLocaleTest { + private final CompilationTestHelper compilationTestHelper = + CompilationTestHelper.newInstance(SpecifyLocale.class, getClass()); + private final BugCheckerRefactoringTestHelper refactoringTestHelper = + newInstance(SpecifyLocale.class, getClass()); + + @Test + void identification() { + compilationTestHelper + .addSourceLines( + "A.java", + "import java.util.Locale;", + "", + "class A {", + " void m() {", + " // BUG: Diagnostic contains:", + " \"a\".toUpperCase();", + "", + " // BUG: Diagnostic contains:", + " \"b\".toLowerCase();", + "", + " String c = \"c\";", + " // BUG: Diagnostic contains:", + " c.toUpperCase();", + "", + " String d = \"d\";", + " // BUG: Diagnostic contains:", + " d.toLowerCase();", + " }", + "}") + .doTest(); + } + + @Test + void replacementFirstSuggestedFix() { + refactoringTestHelper + .setFixChooser(FixChoosers.FIRST) + .addInputLines( + "A.java", + "", + "class A {", + " void m() {", + " \"a\".toUpperCase();", + " \"b\".toLowerCase();", + " }", + "}") + .addOutputLines( + "A.java", + "import java.util.Locale;", + "", + "class A {", + " void m() {", + " \"a\".toUpperCase(Locale.ROOT);", + " \"b\".toLowerCase(Locale.ROOT);", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } + + @Test + void replacementSecondSuggestedFix() { + refactoringTestHelper + .setFixChooser(FixChoosers.SECOND) + .addInputLines( + "A.java", + "", + "class A {", + " void m() {", + " \"a\".toUpperCase();", + " \"b\".toLowerCase();", + " }", + "}") + .addOutputLines( + "A.java", + "import java.util.Locale;", + "", + "class A {", + " void m() {", + " \"a\".toUpperCase(Locale.getDefault());", + " \"b\".toLowerCase(Locale.getDefault());", + " }", + "}") + .doTest(TestMode.TEXT_MATCH); + } +}