Skip to content

Commit

Permalink
Suggestions
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephan202 committed May 20, 2023
1 parent d61816d commit a127136
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import com.google.errorprone.refaster.Refaster;
import com.google.errorprone.refaster.annotation.AfterTemplate;
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.UseImportPolicy;
import java.util.Comparator;
Expand All @@ -19,7 +19,7 @@
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import tech.picnic.errorprone.refaster.annotation.OnlineDocumentation;
import tech.picnic.errorprone.refaster.matchers.IsMethodInvocationWithTwoOrMoreArgs;
import tech.picnic.errorprone.refaster.matchers.IsLikelyTrivialComputation;

/** Refaster rules related to expressions dealing with {@link Optional}s. */
@OnlineDocumentation
Expand Down Expand Up @@ -119,7 +119,7 @@ Optional<T> after(Iterator<T> it) {
/** Prefer {@link Optional#filter(Predicate)} over usage of the ternary operator. */
// XXX: This rule may introduce a compilation error: the `test` expression may reference a
// non-effectively final variable, which is not allowed in the replacement lambda expression.
// Maybe our `Refaster` checker should test `compilesWithFix`?
// Review whether a `@Matcher` can be used to avoid this.
abstract static class TernaryOperatorOptionalPositiveFiltering<T> {
@Placeholder
abstract boolean test(T value);
Expand All @@ -139,7 +139,7 @@ Optional<T> after(T input) {
/** Prefer {@link Optional#filter(Predicate)} over usage of the ternary operator. */
// XXX: This rule may introduce a compilation error: the `test` expression may reference a
// non-effectively final variable, which is not allowed in the replacement lambda expression.
// Maybe our `Refaster` checker should test `compilesWithFix`?
// Review whether a `@Matcher` can be used to avoid this.
abstract static class TernaryOperatorOptionalNegativeFiltering<T> {
@Placeholder
abstract boolean test(T value);
Expand All @@ -162,9 +162,9 @@ Optional<T> after(T input) {
*/
static final class MapOptionalToBoolean<T> {
@BeforeTemplate
@SuppressWarnings("OptionalOrElseGet" /* Rule is confused by `Refaster#anyOf` usage. */)
boolean before(Optional<T> optional, Function<? super T, Boolean> predicate) {
return Refaster.anyOf(
optional.map(predicate).orElse(false), optional.map(predicate).orElse(Boolean.FALSE));
return optional.map(predicate).orElse(Refaster.anyOf(false, Boolean.FALSE));
}

@AfterTemplate
Expand Down Expand Up @@ -227,13 +227,18 @@ T after(Optional<T> o1, Optional<T> o2) {
}

/**
* Prefer {@link Optional#orElseGet(Supplier)} over {@link Optional#orElse(Object)} if the given
* value is a method invocation with two or more arguments.
* Prefer {@link Optional#orElseGet(Supplier)} over {@link Optional#orElse(Object)} if the
* fallback value is not the result of a trivial computation.
*/
// XXX: Extend rule to all method invocations (with less than 2 arguments).
// XXX: This rule may introduce a compilation error: the `value` expression may reference a
// non-effectively final variable, which is not allowed in the replacement lambda expression.
// Review whether a `@Matcher` can be used to avoid this.
// XXX: Once `MethodReferenceUsage` is "production ready", replace
// `@NotMatches(IsLikelyTrivialComputation.class)` with `@Matches(RequiresComputation.class)` (and
// reimplement the matcher accordingly).
static final class OptionalOrElseGet<T> {
@BeforeTemplate
T before(Optional<T> optional, @Matches(IsMethodInvocationWithTwoOrMoreArgs.class) T value) {
T before(Optional<T> optional, @NotMatches(IsLikelyTrivialComputation.class) T value) {
return optional.orElse(value);
}

Expand Down Expand Up @@ -344,6 +349,9 @@ static final class OptionalOrOtherOptional<T> {
Optional<T> before(Optional<T> optional1, Optional<T> optional2) {
// XXX: Note that rewriting the first and third variant will change the code's behavior if
// `optional2` has side-effects.
// XXX: Note that rewriting the first and third variant will introduce a compilation error if
// `optional2` is not effectively final. Review whether a `@Matcher` can be used to avoid
// this.
return Refaster.anyOf(
optional1.map(Optional::of).orElse(optional2),
optional1.map(Optional::of).orElseGet(() -> optional2),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,6 @@ ImmutableSet<String> testOptionalOrElseGet() {
return ImmutableSet.of(
Optional.of("foo").orElse("bar"),
Optional.of("baz").orElse(toString()),
Optional.of("qux").orElse(String.valueOf(true)),
Optional.of("quux").orElse(String.format("%s", "quuz")),
Optional.of("corge").orElse(String.format("%s", "grault", "garply")));
Optional.of("qux").orElse(String.valueOf(true)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ ImmutableSet<String> testOptionalOrElseGet() {
return ImmutableSet.of(
Optional.of("foo").orElse("bar"),
Optional.of("baz").orElse(toString()),
Optional.of("qux").orElse(String.valueOf(true)),
Optional.of("quux").orElseGet(() -> String.format("%s", "quuz")),
Optional.of("corge").orElseGet(() -> String.format("%s", "grault", "garply")));
Optional.of("qux").orElseGet(() -> String.valueOf(true)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package tech.picnic.errorprone.refaster.matchers;

import com.google.errorprone.VisitorState;
import com.google.errorprone.matchers.Matcher;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.LiteralTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.UnaryTree;

/** A matcher of expressions that likely require little to no computation. */
public final class IsLikelyTrivialComputation implements Matcher<ExpressionTree> {
private static final long serialVersionUID = 1L;

/** Instantiates a new {@link IsLikelyTrivialComputation} instance. */
public IsLikelyTrivialComputation() {}

@Override
public boolean matches(ExpressionTree expressionTree, VisitorState state) {
if (expressionTree instanceof MethodInvocationTree) {
// XXX: Method invocations are generally *not* trivial computations, but we make an exception
// for nullary method invocations on the result of a trivial computation. This exception
// allows this `Matcher` to by the `OptionalOrElseGet` Refaster rule, such that it does not
// suggest the introduction of lambda expressions that are better expressed as method
// references. Once the `MethodReferenceUsage` bug checker is production-ready, this exception
// should be removed. (But at that point, instead defining a `RequiresComputation` matcher may
// be more appropriate.)
MethodInvocationTree methodInvocation = (MethodInvocationTree) expressionTree;
if (methodInvocation.getArguments().isEmpty()
&& matches(methodInvocation.getMethodSelect())) {
return true;
}
}

return matches(expressionTree);
}

// XXX: Some `BinaryTree`s may represent what could be considered "trivial computations".
// Depending on feedback such trees may be matched in the future.
private static boolean matches(ExpressionTree expressionTree) {
if (expressionTree instanceof ArrayAccessTree) {
return matches(((ArrayAccessTree) expressionTree).getExpression())
&& matches(((ArrayAccessTree) expressionTree).getIndex());
}

if (expressionTree instanceof LiteralTree) {
return true;
}

if (expressionTree instanceof LambdaExpressionTree) {
/*
* Lambda expressions encapsulate computations, but their definition does not involve
* significant computation.
*/
return true;
}

if (expressionTree instanceof IdentifierTree) {
return true;
}

if (expressionTree instanceof MemberReferenceTree) {
return matches(((MemberReferenceTree) expressionTree).getQualifierExpression());
}

if (expressionTree instanceof MemberSelectTree) {
return matches(((MemberSelectTree) expressionTree).getExpression());
}

if (expressionTree instanceof ParenthesizedTree) {
return matches(((ParenthesizedTree) expressionTree).getExpression());
}

if (expressionTree instanceof TypeCastTree) {
return matches(((TypeCastTree) expressionTree).getExpression());
}

if (expressionTree instanceof UnaryTree) {
// XXX: Arguably side-effectful options such as pre- and post-increment and -decrement are not
// trivial.
return matches(((UnaryTree) expressionTree).getExpression());
}

return false;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package tech.picnic.errorprone.refaster.matchers;

import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;

import com.google.errorprone.BugPattern;
import com.google.errorprone.CompilationTestHelper;
import com.google.errorprone.bugpatterns.BugChecker;
import com.sun.source.tree.ReturnTree;
import org.junit.jupiter.api.Test;

final class IsLikelyTrivialComputationTest {
@Test
void matches() {
CompilationTestHelper.newInstance(MatcherTestChecker.class, getClass())
.addSourceLines(
"A.java",
"import java.util.function.Predicate;",
"",
"class A {",
" String negative1() {",
" return String.valueOf(1);",
" }",
"",
" String negative2() {",
" return toString().toString();",
" }",
"",
" String negative3() {",
" return \"foo\" + toString();",
" }",
"",
" byte negative4() {",
" return \"foo\".getBytes()[0];",
" }",
"",
" int negative5() {",
" int[] arr = new int[0];",
" return arr[hashCode()];",
" }",
"",
" int negative6() {",
" return 1 * 2;",
" }",
"",
" Predicate<String> negative7() {",
" return toString()::equals;",
" }",
"",
" String negative8() {",
" return (toString());",
" }",
"",
" Object negative9() {",
" return (Object) toString();",
" }",
"",
" int negative10() {",
" return -hashCode();",
" }",
"",
" String positive1() {",
" // BUG: Diagnostic contains:",
" return toString();",
" }",
"",
" String positive2() {",
" // BUG: Diagnostic contains:",
" return this.toString();",
" }",
"",
" int positive3() {",
" int[] arr = new int[0];",
" // BUG: Diagnostic contains:",
" return arr[0];",
" }",
"",
" String positive4() {",
" // BUG: Diagnostic contains:",
" return null;",
" }",
"",
" boolean positive5() {",
" // BUG: Diagnostic contains:",
" return false;",
" }",
"",
" int positive6() {",
" // BUG: Diagnostic contains:",
" return 0;",
" }",
"",
" String positive7() {",
" // BUG: Diagnostic contains:",
" return \"foo\" + \"bar\";",
" }",
"",
" Predicate<String> positive8() {",
" // BUG: Diagnostic contains:",
" return v -> \"foo\".equals(v);",
" }",
"",
" A positive9() {",
" // BUG: Diagnostic contains:",
" return this;",
" }",
"",
" Predicate<String> positive10() {",
" // BUG: Diagnostic contains:",
" return \"foo\"::equals;",
" }",
"",
" A positive11() {",
" // BUG: Diagnostic contains:",
" return (this);",
" }",
"",
" Object positive12() {",
" // BUG: Diagnostic contains:",
" return (Object) this;",
" }",
"",
" boolean positive13() {",
" // BUG: Diagnostic contains:",
" return !false;",
" }",
"}")
.doTest();
}

/** A {@link BugChecker} that simply delegates to {@link IsLikelyTrivialComputation}. */
@BugPattern(
summary = "Flags return statement expressions matched by `IsLikelyTrivialComputation`",
severity = ERROR)
public static final class MatcherTestChecker extends AbstractMatcherTestChecker {
private static final long serialVersionUID = 1L;

// XXX: This is a false positive reported by Checkstyle. See
// https://github.com/checkstyle/checkstyle/issues/10161#issuecomment-1242732120.
@SuppressWarnings("RedundantModifier")
public MatcherTestChecker() {
super(
(expressionTree, state) ->
state.getPath().getParentPath().getLeaf() instanceof ReturnTree
&& new IsLikelyTrivialComputation().matches(expressionTree, state));
}
}
}
Loading

0 comments on commit a127136

Please sign in to comment.