All Downloads are FREE. Search and download functionalities are using the official Maven repository.

tech.picnic.errorprone.refasterrules.StreamRules Maven / Gradle / Ivy

There is a newer version: 0.19.1
Show newest version
package tech.picnic.errorprone.refasterrules;

import static com.google.errorprone.refaster.ImportPolicy.STATIC_IMPORT_ALWAYS;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.reverseOrder;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.filtering;
import static java.util.stream.Collectors.flatMapping;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.maxBy;
import static java.util.stream.Collectors.minBy;
import static java.util.stream.Collectors.reducing;
import static java.util.stream.Collectors.summarizingDouble;
import static java.util.stream.Collectors.summarizingInt;
import static java.util.stream.Collectors.summarizingLong;
import static java.util.stream.Collectors.summingDouble;
import static java.util.stream.Collectors.summingInt;
import static java.util.stream.Collectors.summingLong;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.AlsoNegation;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import com.google.errorprone.refaster.annotation.Matches;
import com.google.errorprone.refaster.annotation.MayOptionallyUse;
import com.google.errorprone.refaster.annotation.NotMatches;
import com.google.errorprone.refaster.annotation.Placeholder;
import com.google.errorprone.refaster.annotation.Repeated;
import com.google.errorprone.refaster.annotation.UseImportPolicy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.DoubleSummaryStatistics;
import java.util.IntSummaryStatistics;
import java.util.LongSummaryStatistics;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
import tech.picnic.errorprone.refaster.matchers.IsIdentityOperation;
import tech.picnic.errorprone.refaster.matchers.IsLambdaExpressionOrMethodReference;
import tech.picnic.errorprone.refaster.matchers.IsRefasterAsVarargs;

/** Refaster rules related to expressions dealing with {@link Stream}s. */
@OnlineDocumentation
final class StreamRules {
  private StreamRules() {}

  /**
   * Prefer {@link Collectors#joining()} over {@link Collectors#joining(CharSequence)} with an empty
   * delimiter string.
   */
  static final class Joining {
    @BeforeTemplate
    Collector before() {
      return joining("");
    }

    @AfterTemplate
    @UseImportPolicy(STATIC_IMPORT_ALWAYS)
    Collector after() {
      return joining();
    }
  }

  /** Prefer {@link Stream#empty()} over less clear alternatives. */
  static final class EmptyStream {
    @BeforeTemplate
    Stream before() {
      return Stream.of();
    }

    @AfterTemplate
    Stream after() {
      return Stream.empty();
    }
  }

  /** Prefer {@link Stream#ofNullable(Object)} over more contrived alternatives. */
  static final class StreamOfNullable {
    @BeforeTemplate
    Stream before(T object) {
      return Refaster.anyOf(
          Stream.of(object).filter(Objects::nonNull), Optional.ofNullable(object).stream());
    }

    @AfterTemplate
    Stream after(T object) {
      return Stream.ofNullable(object);
    }
  }

  /**
   * Prefer {@link Arrays#stream(Object[])} over {@link Stream#of(Object[])}, as the former is
   * clearer.
   */
  static final class StreamOfArray {
    @BeforeTemplate
    Stream before(@NotMatches(IsRefasterAsVarargs.class) T[] array) {
      return Stream.of(array);
    }

    @AfterTemplate
    Stream after(T[] array) {
      return Arrays.stream(array);
    }
  }

  /** Don't unnecessarily call {@link Streams#concat(Stream...)}. */
  static final class ConcatOneStream {
    @BeforeTemplate
    Stream before(Stream stream) {
      return Streams.concat(stream);
    }

    @AfterTemplate
    @CanIgnoreReturnValue
    Stream after(Stream stream) {
      return stream;
    }
  }

  /** Prefer {@link Stream#concat(Stream, Stream)} over the Guava alternative. */
  static final class ConcatTwoStreams {
    @BeforeTemplate
    Stream before(Stream s1, Stream s2) {
      return Streams.concat(s1, s2);
    }

    @AfterTemplate
    Stream after(Stream s1, Stream s2) {
      return Stream.concat(s1, s2);
    }
  }

  /** Avoid unnecessary nesting of {@link Stream#filter(Predicate)} operations. */
  abstract static class FilterOuterStreamAfterFlatMap {
    @Placeholder
    abstract Stream toStreamFunction(@MayOptionallyUse T element);

    @BeforeTemplate
    Stream before(Stream stream, Predicate predicate) {
      return stream.flatMap(v -> toStreamFunction(v).filter(predicate));
    }

    @AfterTemplate
    Stream after(Stream stream, Predicate predicate) {
      return stream.flatMap(v -> toStreamFunction(v)).filter(predicate);
    }
  }

  /** Avoid unnecessary nesting of {@link Stream#map(Function)} operations. */
  abstract static class MapOuterStreamAfterFlatMap {
    @Placeholder
    abstract Stream toStreamFunction(@MayOptionallyUse T element);

    @BeforeTemplate
    Stream before(Stream stream, Function function) {
      return stream.flatMap(v -> toStreamFunction(v).map(function));
    }

    @AfterTemplate
    Stream after(Stream stream, Function function) {
      return stream.flatMap(v -> toStreamFunction(v)).map(function);
    }
  }

  /** Avoid unnecessary nesting of {@link Stream#flatMap(Function)} operations. */
  abstract static class FlatMapOuterStreamAfterFlatMap {
    @Placeholder
    abstract Stream toStreamFunction(@MayOptionallyUse T element);

    @BeforeTemplate
    Stream before(
        Stream stream, Function> function) {
      return stream.flatMap(v -> toStreamFunction(v).flatMap(function));
    }

    @AfterTemplate
    Stream after(Stream stream, Function> function) {
      return stream.flatMap(v -> toStreamFunction(v)).flatMap(function);
    }
  }

  /**
   * Apply {@link Stream#filter(Predicate)} before {@link Stream#sorted()} to reduce the number of
   * elements to sort.
   */
  static final class StreamFilterSorted {
    @BeforeTemplate
    Stream before(Stream stream, Predicate predicate) {
      return stream.sorted().filter(predicate);
    }

    @AfterTemplate
    Stream after(Stream stream, Predicate predicate) {
      return stream.filter(predicate).sorted();
    }
  }

  /**
   * Apply {@link Stream#filter(Predicate)} before {@link Stream#sorted(Comparator)} to reduce the
   * number of elements to sort.
   */
  static final class StreamFilterSortedWithComparator {
    @BeforeTemplate
    Stream before(
        Stream stream, Predicate predicate, Comparator comparator) {
      return stream.sorted(comparator).filter(predicate);
    }

    @AfterTemplate
    Stream after(
        Stream stream, Predicate predicate, Comparator comparator) {
      return stream.filter(predicate).sorted(comparator);
    }
  }

  /**
   * Where possible, clarify that a mapping operation will be applied only to a single stream
   * element.
   */
  // XXX: Implement a similar rule for `.findAny()`. For parallel streams this wouldn't be quite the
  // same, so such a rule requires a `Matcher` that heuristically identifies `Stream` expressions
  // with deterministic order.
  // XXX: This change is not equivalent for `null`-returning functions, as the original code throws
  // an NPE if the first element is `null`, while the latter yields an empty `Optional`.
  static final class StreamMapFirst {
    @BeforeTemplate
    Optional before(Stream stream, Function function) {
      return stream.map(function).findFirst();
    }

    @AfterTemplate
    Optional after(Stream stream, Function function) {
      return stream.findFirst().map(function);
    }
  }

  /** In order to test whether a stream has any element, simply try to find one. */
  // XXX: This rule assumes that any matched `Collector` does not perform any filtering.
  // (Perhaps we could add a `@Matches` guard that validates that the collector expression does not
  // contain a `Collectors#filtering` call. That'd still not be 100% accurate, though.)
  static final class StreamFindAnyIsEmpty, M extends Map> {
    @BeforeTemplate
    boolean before(Stream stream, Collector collector) {
      return Refaster.anyOf(
          stream.count() == 0,
          stream.count() <= 0,
          stream.count() < 1,
          stream.findFirst().isEmpty(),
          stream.collect(collector).isEmpty(),
          stream.collect(collectingAndThen(collector, C::isEmpty)));
    }

    @BeforeTemplate
    boolean before2(Stream stream, Collector collector) {
      return stream.collect(collectingAndThen(collector, M::isEmpty));
    }

    @AfterTemplate
    @AlsoNegation
    boolean after(Stream stream) {
      return stream.findAny().isEmpty();
    }
  }

  /**
   * Prefer {@link Stream#findAny()} over {@link Stream#findFirst()} if one only cares whether the
   * stream is nonempty.
   */
  static final class StreamFindAnyIsPresent {
    @BeforeTemplate
    boolean before(Stream stream) {
      return stream.findFirst().isPresent();
    }

    @AfterTemplate
    boolean after(Stream stream) {
      return stream.findAny().isPresent();
    }
  }

  static final class StreamMin {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    Optional before(Stream stream, Comparator comparator) {
      return Refaster.anyOf(
          stream.max(comparator.reversed()),
          stream.sorted(comparator).findFirst(),
          stream.collect(minBy(comparator)));
    }

    @AfterTemplate
    Optional after(Stream stream, Comparator comparator) {
      return stream.min(comparator);
    }
  }

  static final class StreamMinNaturalOrder> {
    @BeforeTemplate
    Optional before(Stream stream) {
      return Refaster.anyOf(stream.max(reverseOrder()), stream.sorted().findFirst());
    }

    @AfterTemplate
    @UseImportPolicy(STATIC_IMPORT_ALWAYS)
    Optional after(Stream stream) {
      return stream.min(naturalOrder());
    }
  }

  static final class StreamMax {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    Optional before(Stream stream, Comparator comparator) {
      return Refaster.anyOf(
          stream.min(comparator.reversed()),
          Streams.findLast(stream.sorted(comparator)),
          stream.collect(maxBy(comparator)));
    }

    @AfterTemplate
    Optional after(Stream stream, Comparator comparator) {
      return stream.max(comparator);
    }
  }

  static final class StreamMaxNaturalOrder> {
    @BeforeTemplate
    Optional before(Stream stream) {
      return Refaster.anyOf(stream.min(reverseOrder()), Streams.findLast(stream.sorted()));
    }

    @AfterTemplate
    @UseImportPolicy(STATIC_IMPORT_ALWAYS)
    Optional after(Stream stream) {
      return stream.max(naturalOrder());
    }
  }

  /** Prefer {@link Stream#noneMatch(Predicate)} over more contrived alternatives. */
  static final class StreamNoneMatch {
    @BeforeTemplate
    @SuppressWarnings("java:S4034" /* This violation will be rewritten. */)
    boolean before(Stream stream, Predicate predicate) {
      return Refaster.anyOf(
          !stream.anyMatch(predicate),
          stream.allMatch(Refaster.anyOf(not(predicate), predicate.negate())),
          stream.filter(predicate).findAny().isEmpty());
    }

    // XXX: Consider extending `@Matches(IsIdentityOperation.class)` such that it can replace this
    // template's `Refaster.anyOf` usage.
    @BeforeTemplate
    boolean before2(
        Stream stream,
        @Matches(IsLambdaExpressionOrMethodReference.class)
            Function predicate) {
      return stream.map(predicate).noneMatch(Refaster.anyOf(Boolean::booleanValue, b -> b));
    }

    @AfterTemplate
    boolean after(Stream stream, Predicate predicate) {
      return stream.noneMatch(predicate);
    }
  }

  abstract static class StreamNoneMatch2 {
    @Placeholder(allowsIdentity = true)
    abstract boolean test(@MayOptionallyUse T element);

    @BeforeTemplate
    boolean before(Stream stream) {
      return stream.allMatch(e -> !test(e));
    }

    @AfterTemplate
    boolean after(Stream stream) {
      return stream.noneMatch(e -> test(e));
    }
  }

  /** Prefer {@link Stream#anyMatch(Predicate)} over more contrived alternatives. */
  static final class StreamAnyMatch {
    @BeforeTemplate
    @SuppressWarnings("java:S4034" /* This violation will be rewritten. */)
    boolean before(Stream stream, Predicate predicate) {
      return Refaster.anyOf(
          !stream.noneMatch(predicate), stream.filter(predicate).findAny().isPresent());
    }

    // XXX: Consider extending `@Matches(IsIdentityOperation.class)` such that it can replace this
    // template's `Refaster.anyOf` usage.
    @BeforeTemplate
    boolean before2(
        Stream stream,
        @Matches(IsLambdaExpressionOrMethodReference.class)
            Function predicate) {
      return stream.map(predicate).anyMatch(Refaster.anyOf(Boolean::booleanValue, b -> b));
    }

    @AfterTemplate
    boolean after(Stream stream, Predicate predicate) {
      return stream.anyMatch(predicate);
    }
  }

  static final class StreamAllMatch {
    @BeforeTemplate
    boolean before(Stream stream, Predicate predicate) {
      return stream.noneMatch(Refaster.anyOf(not(predicate), predicate.negate()));
    }

    // XXX: Consider extending `@Matches(IsIdentityOperation.class)` such that it can replace this
    // template's `Refaster.anyOf` usage.
    @BeforeTemplate
    boolean before2(
        Stream stream,
        @Matches(IsLambdaExpressionOrMethodReference.class)
            Function predicate) {
      return stream.map(predicate).allMatch(Refaster.anyOf(Boolean::booleanValue, b -> b));
    }

    @AfterTemplate
    boolean after(Stream stream, Predicate predicate) {
      return stream.allMatch(predicate);
    }
  }

  abstract static class StreamAllMatch2 {
    @Placeholder(allowsIdentity = true)
    abstract boolean test(@MayOptionallyUse T element);

    @BeforeTemplate
    boolean before(Stream stream) {
      return stream.noneMatch(e -> !test(e));
    }

    @AfterTemplate
    boolean after(Stream stream) {
      return stream.allMatch(e -> test(e));
    }
  }

  static final class StreamMapToIntSum {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    long before(Stream stream, ToIntFunction mapper) {
      return stream.collect(summingInt(mapper));
    }

    @BeforeTemplate
    int before2(
        Stream stream,
        @Matches(IsLambdaExpressionOrMethodReference.class) Function mapper) {
      return stream.map(mapper).reduce(0, Integer::sum);
    }

    @AfterTemplate
    int after(Stream stream, ToIntFunction mapper) {
      return stream.mapToInt(mapper).sum();
    }
  }

  static final class StreamMapToDoubleSum {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    double before(Stream stream, ToDoubleFunction mapper) {
      return stream.collect(summingDouble(mapper));
    }

    @BeforeTemplate
    double before2(
        Stream stream,
        @Matches(IsLambdaExpressionOrMethodReference.class) Function mapper) {
      return stream.map(mapper).reduce(0.0, Double::sum);
    }

    @AfterTemplate
    double after(Stream stream, ToDoubleFunction mapper) {
      return stream.mapToDouble(mapper).sum();
    }
  }

  static final class StreamMapToLongSum {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    long before(Stream stream, ToLongFunction mapper) {
      return stream.collect(summingLong(mapper));
    }

    @BeforeTemplate
    long before2(
        Stream stream,
        @Matches(IsLambdaExpressionOrMethodReference.class) Function mapper) {
      return stream.map(mapper).reduce(0L, Long::sum);
    }

    @AfterTemplate
    long after(Stream stream, ToLongFunction mapper) {
      return stream.mapToLong(mapper).sum();
    }
  }

  static final class StreamMapToIntSummaryStatistics {
    @BeforeTemplate
    IntSummaryStatistics before(Stream stream, ToIntFunction mapper) {
      return stream.collect(summarizingInt(mapper));
    }

    @AfterTemplate
    IntSummaryStatistics after(Stream stream, ToIntFunction mapper) {
      return stream.mapToInt(mapper).summaryStatistics();
    }
  }

  static final class StreamMapToDoubleSummaryStatistics {
    @BeforeTemplate
    DoubleSummaryStatistics before(Stream stream, ToDoubleFunction mapper) {
      return stream.collect(summarizingDouble(mapper));
    }

    @AfterTemplate
    DoubleSummaryStatistics after(Stream stream, ToDoubleFunction mapper) {
      return stream.mapToDouble(mapper).summaryStatistics();
    }
  }

  static final class StreamMapToLongSummaryStatistics {
    @BeforeTemplate
    LongSummaryStatistics before(Stream stream, ToLongFunction mapper) {
      return stream.collect(summarizingLong(mapper));
    }

    @AfterTemplate
    LongSummaryStatistics after(Stream stream, ToLongFunction mapper) {
      return stream.mapToLong(mapper).summaryStatistics();
    }
  }

  static final class StreamCount {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    long before(Stream stream) {
      return stream.collect(counting());
    }

    @AfterTemplate
    long after(Stream stream) {
      return stream.count();
    }
  }

  static final class StreamReduce {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    Optional before(Stream stream, BinaryOperator accumulator) {
      return stream.collect(reducing(accumulator));
    }

    @AfterTemplate
    Optional after(Stream stream, BinaryOperator accumulator) {
      return stream.reduce(accumulator);
    }
  }

  static final class StreamReduceWithIdentity {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    T before(Stream stream, T identity, BinaryOperator accumulator) {
      return stream.collect(reducing(identity, accumulator));
    }

    @AfterTemplate
    T after(Stream stream, T identity, BinaryOperator accumulator) {
      return stream.reduce(identity, accumulator);
    }
  }

  static final class StreamFilterCollect {
    @BeforeTemplate
    R before(
        Stream stream, Predicate predicate, Collector collector) {
      return stream.collect(filtering(predicate, collector));
    }

    @AfterTemplate
    R after(
        Stream stream, Predicate predicate, Collector collector) {
      return stream.filter(predicate).collect(collector);
    }
  }

  static final class StreamMapCollect {
    @BeforeTemplate
    @SuppressWarnings("java:S4266" /* This violation will be rewritten. */)
    R before(
        Stream stream,
        Function mapper,
        Collector collector) {
      return stream.collect(mapping(mapper, collector));
    }

    @AfterTemplate
    R after(
        Stream stream,
        Function mapper,
        Collector collector) {
      return stream.map(mapper).collect(collector);
    }
  }

  static final class StreamFlatMapCollect {
    @BeforeTemplate
    R before(
        Stream stream,
        Function> mapper,
        Collector collector) {
      return stream.collect(flatMapping(mapper, collector));
    }

    @AfterTemplate
    R after(
        Stream stream,
        Function> mapper,
        Collector collector) {
      return stream.flatMap(mapper).collect(collector);
    }
  }

  static final class StreamsConcat {
    @BeforeTemplate
    Stream before(
        @Repeated Stream stream,
        @Matches(IsIdentityOperation.class)
            Function, ? extends Stream> mapper) {
      return Stream.of(Refaster.asVarargs(stream)).flatMap(mapper);
    }

    @AfterTemplate
    Stream after(@Repeated Stream stream) {
      return Streams.concat(Refaster.asVarargs(stream));
    }
  }

  static final class StreamTakeWhile {
    @BeforeTemplate
    Stream before(Stream stream, Predicate predicate) {
      return stream.takeWhile(predicate).filter(predicate);
    }

    @AfterTemplate
    Stream after(Stream stream, Predicate predicate) {
      return stream.takeWhile(predicate);
    }
  }

  /**
   * Prefer {@link Stream#iterate(Object, Predicate, UnaryOperator)} over more contrived
   * alternatives.
   */
  static final class StreamIterate {
    @BeforeTemplate
    Stream before(T seed, Predicate hasNext, UnaryOperator next) {
      return Stream.iterate(seed, next).takeWhile(hasNext);
    }

    @AfterTemplate
    Stream after(T seed, Predicate hasNext, UnaryOperator next) {
      return Stream.iterate(seed, hasNext, next);
    }
  }

  /** Prefer {@link Stream#of(Object)} over more contrived alternatives. */
  // XXX: Generalize this and similar rules using an Error Prone check.
  static final class StreamOf1 {
    @BeforeTemplate
    Stream before(T e1) {
      return ImmutableList.of(e1).stream();
    }

    @AfterTemplate
    Stream after(T e1) {
      return Stream.of(e1);
    }
  }

  /** Prefer {@link Stream#of(Object[])} over more contrived alternatives. */
  // XXX: Generalize this and similar rules using an Error Prone check.
  static final class StreamOf2 {
    @BeforeTemplate
    Stream before(T e1, T e2) {
      return ImmutableList.of(e1, e2).stream();
    }

    @AfterTemplate
    Stream after(T e1, T e2) {
      return Stream.of(e1, e2);
    }
  }

  /** Prefer {@link Stream#of(Object[])} over more contrived alternatives. */
  // XXX: Generalize this and similar rules using an Error Prone check.
  static final class StreamOf3 {
    @BeforeTemplate
    Stream before(T e1, T e2, T e3) {
      return ImmutableList.of(e1, e2, e3).stream();
    }

    @AfterTemplate
    Stream after(T e1, T e2, T e3) {
      return Stream.of(e1, e2, e3);
    }
  }

  /** Prefer {@link Stream#of(Object[])} over more contrived alternatives. */
  // XXX: Generalize this and similar rules using an Error Prone check.
  static final class StreamOf4 {
    @BeforeTemplate
    Stream before(T e1, T e2, T e3, T e4) {
      return ImmutableList.of(e1, e2, e3, e4).stream();
    }

    @AfterTemplate
    Stream after(T e1, T e2, T e3, T e4) {
      return Stream.of(e1, e2, e3, e4);
    }
  }

  /** Prefer {@link Stream#of(Object[])} over more contrived alternatives. */
  // XXX: Generalize this and similar rules using an Error Prone check.
  static final class StreamOf5 {
    @BeforeTemplate
    Stream before(T e1, T e2, T e3, T e4, T e5) {
      return ImmutableList.of(e1, e2, e3, e4, e5).stream();
    }

    @AfterTemplate
    Stream after(T e1, T e2, T e3, T e4, T e5) {
      return Stream.of(e1, e2, e3, e4, e5);
    }
  }
}