diff --git a/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheck.java b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheck.java new file mode 100644 index 00000000000..30e6de37822 --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheck.java @@ -0,0 +1,66 @@ +package tech.picnic.errorprone.bugpatterns; + +import static com.google.errorprone.matchers.Matchers.anyOf; +import static com.google.errorprone.matchers.Matchers.instanceMethod; +import static com.google.errorprone.matchers.Matchers.staticMethod; + +import com.google.auto.service.AutoService; +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.LinkType; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.BugPattern.StandardTags; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +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 java.time.Clock; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** A {@link BugChecker} which flags illegal time-zone related operations. */ +@AutoService(BugChecker.class) +@BugPattern( + name = "TimeZoneUsageCheck", + summary = "Avoid illegal operations on assorted time related objects", + linkType = LinkType.NONE, + severity = SeverityLevel.WARNING, + tags = StandardTags.FRAGILE_CODE) +public final class TimeZoneUsageCheck extends BugChecker implements MethodInvocationTreeMatcher { + private static final long serialVersionUID = 1L; + private static final Matcher ILLEGAL_CLOCK_USAGES = + anyOf( + instanceMethod().onDescendantOf(Clock.class.getName()).namedAnyOf("getZone", "withZone"), + staticMethod() + .onClass(Clock.class.getName()) + .namedAnyOf( + "system", + "systemDefaultZone", + "systemUTC", + "tickMillis", + "tickMinutes", + "tickSeconds")); + private static final Matcher NOW_USAGES = + anyOf( + staticMethod() + .onClassAny( + Instant.class.getName(), + LocalDate.class.getName(), + LocalDateTime.class.getName(), + LocalTime.class.getName()) + .namedAnyOf("now")); + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + if (!ILLEGAL_CLOCK_USAGES.matches(tree, state) && !NOW_USAGES.matches(tree, state)) { + return Description.NO_MATCH; + } + return buildDescription(tree) + .setMessage("This operation is not recommended, please refactor it") + .build(); + } +} diff --git a/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheckTest.java b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheckTest.java new file mode 100644 index 00000000000..2fa92ff9ff9 --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheckTest.java @@ -0,0 +1,91 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +public final class TimeZoneUsageCheckTest { + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(TimeZoneUsageCheck.class, getClass()); + + @Test + public void testNoIdentificationFoundCases() { + compilationHelper + .addSourceLines( + "A.java", + "import static java.time.ZoneOffset.UTC;", + "", + "import java.time.Clock;", + "import java.time.Duration;", + "import java.time.Instant;", + "", + "class A {", + "void m() {", + " Clock clock = Clock.fixed(Instant.EPOCH, UTC);", + "clock.instant();", + "clock.millis();", + "Clock.offset(clock, Duration.ZERO);", + "Clock.tick(clock, Duration.ZERO);", + " }", + "}") + .doTest(); + } + + @Test + public void testIdentifyCases() { + compilationHelper + .addSourceLines( + "A.java", + "import static java.time.ZoneOffset.UTC;", + "", + "import java.time.Clock;", + "import java.time.Instant;", + "import java.time.LocalDate;", + "import java.time.LocalDateTime;", + "import java.time.LocalTime;", + "", + "class A {", + "void m() {", + " // BUG: Diagnostic contains:", + "Clock.systemUTC();", + " // BUG: Diagnostic contains:", + "Clock.systemDefaultZone();", + " // BUG: Diagnostic contains:", + "Clock.system(UTC);", + " // BUG: Diagnostic contains:", + "Clock.tickMillis(UTC);", + " // BUG: Diagnostic contains:", + "Clock.tickMinutes(UTC);", + " // BUG: Diagnostic contains:", + "Clock.tickSeconds(UTC);", + " Clock clock = Clock.fixed(Instant.EPOCH, UTC);", + " // BUG: Diagnostic contains:", + "clock.getZone();", + " // BUG: Diagnostic contains:", + "clock.withZone(UTC);", + " // BUG: Diagnostic contains:", + "Instant.now();", + " // BUG: Diagnostic contains:", + "Instant.now(clock);", + " // BUG: Diagnostic contains:", + "LocalDate.now();", + " // BUG: Diagnostic contains:", + "LocalDate.now(clock);", + " // BUG: Diagnostic contains:", + "LocalDate.now(UTC);", + " // BUG: Diagnostic contains:", + "LocalDateTime.now();", + " // BUG: Diagnostic contains:", + "LocalDateTime.now(clock);", + " // BUG: Diagnostic contains:", + "LocalDateTime.now(UTC);", + " // BUG: Diagnostic contains:", + "LocalTime.now();", + " // BUG: Diagnostic contains:", + "LocalTime.now(clock);", + " // BUG: Diagnostic contains:", + "LocalTime.now(UTC);", + " }", + "}") + .doTest(); + } +}