Skip to content

Commit

Permalink
Add a check to reverse Yoda conditions.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 500187575
  • Loading branch information
graememorgan authored and Error Prone Team committed Jan 6, 2023
1 parent 181f991 commit a57309b
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2023 The Error Prone Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.errorprone.bugpatterns;

import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.fixes.SuggestedFixes.qualifyType;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.matchers.Matchers.instanceEqualsInvocation;
import static com.google.errorprone.matchers.Matchers.staticEqualsInvocation;
import static com.google.errorprone.util.ASTHelpers.constValue;
import static com.google.errorprone.util.ASTHelpers.getReceiver;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static java.lang.String.format;

import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.BinaryTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import java.util.Objects;

/** See the summary. */
@BugPattern(
summary = "The non-constant portion of an equals check generally comes first.",
severity = WARNING)
public final class YodaCondition extends BugChecker
implements BinaryTreeMatcher, MethodInvocationTreeMatcher {
@Override
public Description matchBinary(BinaryTree tree, VisitorState state) {
switch (tree.getKind()) {
case EQUAL_TO:
case NOT_EQUAL_TO:
return fix(
tree,
tree.getLeftOperand(),
tree.getRightOperand(),
/* provideNullSafeFix= */ false,
state);
default:
return NO_MATCH;
}
}

@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (staticEqualsInvocation().matches(tree, state)) {
return fix(
tree,
tree.getArguments().get(0),
tree.getArguments().get(1),
/* provideNullSafeFix= */ false,
state);
}
if (instanceEqualsInvocation().matches(tree, state)) {
return fix(
tree,
getReceiver(tree),
tree.getArguments().get(0),
/* provideNullSafeFix= */ true,
state);
}
return NO_MATCH;
}

private Description fix(
Tree tree, Tree lhs, Tree rhs, boolean provideNullSafeFix, VisitorState state) {
if (seemsConstant(lhs) && !seemsConstant(rhs)) {
var description = buildDescription(lhs).addFix(SuggestedFix.swap(lhs, rhs));
if (provideNullSafeFix) {
var fix = SuggestedFix.builder().setShortDescription("null-safe fix");
description.addFix(
fix.replace(
tree,
format(
"%s.equals(%s, %s)",
qualifyType(state, fix, Objects.class.getName()),
state.getSourceForNode(rhs),
state.getSourceForNode(lhs)))
.build());
}
return description.build();
}
return NO_MATCH;
}

private static boolean seemsConstant(Tree tree) {
if (constValue(tree) != null) {
return true;
}
var symbol = getSymbol(tree);
return symbol instanceof VarSymbol && symbol.isEnum();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@
import com.google.errorprone.bugpatterns.WithSignatureDiscouraged;
import com.google.errorprone.bugpatterns.WrongOneof;
import com.google.errorprone.bugpatterns.XorPower;
import com.google.errorprone.bugpatterns.YodaCondition;
import com.google.errorprone.bugpatterns.android.BinderIdentityRestoredDangerously;
import com.google.errorprone.bugpatterns.android.BundleDeserializationCast;
import com.google.errorprone.bugpatterns.android.FragmentInjection;
Expand Down Expand Up @@ -1164,7 +1165,8 @@ public static ScannerSupplier errorChecks() {
VarChecker.class,
Varifier.class,
VoidMissingNullable.class,
WildcardImport.class
WildcardImport.class,
YodaCondition.class
// keep-sorted end
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2023 The Error Prone Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.errorprone.bugpatterns;

import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.FixChoosers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class YodaConditionTest {
private final BugCheckerRefactoringTestHelper refactoring =
BugCheckerRefactoringTestHelper.newInstance(YodaCondition.class, getClass());

@Test
public void primitive() {
refactoring
.addInputLines(
"Test.java",
"class Test {",
" boolean yoda(int a) {",
" return 4 == a;",
" }",
" boolean notYoda(int a) {",
" return a == 4;",
" }",
"}")
.addOutputLines(
"Test.java",
"class Test {",
" boolean yoda(int a) {",
" return a == 4;",
" }",
" boolean notYoda(int a) {",
" return a == 4;",
" }",
"}")
.doTest();
}

@Test
public void enums() {
refactoring
.addInputLines(
"E.java",
"enum E {",
" A, B;",
" boolean foo(E e) {",
" return this == e;",
" }",
"}")
.expectUnchanged()
.addInputLines(
"Test.java",
"class Test {",
" boolean yoda(E a) {",
" return E.A == a;",
" }",
" boolean notYoda(E a) {",
" return a == E.A;",
" }",
"}")
.addOutputLines(
"Test.java",
"class Test {",
" boolean yoda(E a) {",
" return a == E.A;",
" }",
" boolean notYoda(E a) {",
" return a == E.A;",
" }",
"}")
.doTest();
}

@Test
public void nullUnsafeFix() {
refactoring
.addInputLines("E.java", "enum E {A, B}")
.expectUnchanged()
.addInputLines(
"Test.java",
"class Test {",
" boolean yoda(E a) {",
" return E.A.equals(a);",
" }",
"}")
.addOutputLines(
"Test.java",
"class Test {",
" boolean yoda(E a) {",
" return a.equals(E.A);",
" }",
"}")
.doTest();
}

@Test
public void nullSafeFix() {
refactoring
.addInputLines("E.java", "enum E {A, B}")
.expectUnchanged()
.addInputLines(
"Test.java",
"class Test {",
" boolean yoda(E a) {",
" return E.A.equals(a);",
" }",
"}")
.addOutputLines(
"Test.java",
"import java.util.Objects;",
"class Test {",
" boolean yoda(E a) {",
" return Objects.equals(a, E.A);",
" }",
"}")
.setFixChooser(FixChoosers.SECOND)
.doTest();
}
}

0 comments on commit a57309b

Please sign in to comment.