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 0000000000..16fbd77478 --- /dev/null +++ b/error-prone-contrib/src/main/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheck.java @@ -0,0 +1,62 @@ +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 = "TimeZoneUsage", + summary = + "Derive the current time from an existing `Clock` Spring bean, and don't rely on a `Clock`'s time zone", + 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 IS_BANNED_TIME_METHOD = + anyOf( + instanceMethod().onDescendantOf(Clock.class.getName()).namedAnyOf("getZone", "withZone"), + staticMethod() + .onClass(Clock.class.getName()) + .namedAnyOf( + "system", + "systemDefaultZone", + "systemUTC", + "tickMillis", + "tickMinutes", + "tickSeconds"), + staticMethod() + .onClassAny( + Instant.class.getName(), + LocalDate.class.getName(), + LocalDateTime.class.getName(), + LocalTime.class.getName()) + .named("now")); + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return IS_BANNED_TIME_METHOD.matches(tree, state) + ? buildDescription(tree).build() + : Description.NO_MATCH; + } +} 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 0000000000..c798ac2ac7 --- /dev/null +++ b/error-prone-contrib/src/test/java/tech/picnic/errorprone/bugpatterns/TimeZoneUsageCheckTest.java @@ -0,0 +1,83 @@ +package tech.picnic.errorprone.bugpatterns; + +import com.google.common.base.Predicates; +import com.google.errorprone.CompilationTestHelper; +import org.junit.jupiter.api.Test; + +public final class TimeZoneUsageCheckTest { + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(TimeZoneUsageCheck.class, getClass()) + .expectErrorMessage( + "X", + Predicates.containsPattern( + "Derive the current time from an existing `Clock` Spring bean, and don't rely on a `Clock`'s time zone")); + + @Test + public void testIdentification() { + compilationHelper + .addSourceLines( + "A.java", + "import static java.time.ZoneOffset.UTC;", + "", + "import java.time.Clock;", + "import java.time.Duration;", + "import java.time.Instant;", + "import java.time.LocalDate;", + "import java.time.LocalDateTime;", + "import java.time.LocalTime;", + "", + "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);", + "", + " // BUG: Diagnostic matches: X", + " Clock.systemUTC();", + " // BUG: Diagnostic matches: X", + " Clock.systemDefaultZone();", + " // BUG: Diagnostic matches: X", + " Clock.system(UTC);", + " // BUG: Diagnostic matches: X", + " Clock.tickMillis(UTC);", + " // BUG: Diagnostic matches: X", + " Clock.tickMinutes(UTC);", + " // BUG: Diagnostic matches: X", + " Clock.tickSeconds(UTC);", + " // BUG: Diagnostic matches: X", + " clock.getZone();", + " // BUG: Diagnostic matches: X", + " clock.withZone(UTC);", + "", + " // BUG: Diagnostic matches: X", + " Instant.now();", + " // BUG: Diagnostic matches: X", + " Instant.now(clock);", + "", + " // BUG: Diagnostic matches: X", + " LocalDate.now();", + " // BUG: Diagnostic matches: X", + " LocalDate.now(clock);", + " // BUG: Diagnostic matches: X", + " LocalDate.now(UTC);", + "", + " // BUG: Diagnostic matches: X", + " LocalDateTime.now();", + " // BUG: Diagnostic matches: X", + " LocalDateTime.now(clock);", + " // BUG: Diagnostic matches: X", + " LocalDateTime.now(UTC);", + "", + " // BUG: Diagnostic matches: X", + " LocalTime.now();", + " // BUG: Diagnostic matches: X", + " LocalTime.now(clock);", + " // BUG: Diagnostic matches: X", + " LocalTime.now(UTC);", + " }", + "}") + .doTest(); + } +}