diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DuplicateAssertion.java b/core/src/main/java/com/google/errorprone/bugpatterns/DuplicateAssertion.java new file mode 100644 index 00000000000..14c1f61b35b --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DuplicateAssertion.java @@ -0,0 +1,96 @@ +/* + * Copyright 2025 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.common.collect.Iterables.getOnlyElement; +import static com.google.errorprone.matchers.Description.NO_MATCH; +import static com.google.errorprone.util.ASTHelpers.getReceiver; +import static com.google.errorprone.util.ASTHelpers.getSymbol; + +import com.google.common.collect.ImmutableSetMultimap; +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.SeverityLevel; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.BlockTreeMatcher; +import com.google.errorprone.bugpatterns.threadsafety.ConstantExpressions; +import com.google.errorprone.bugpatterns.threadsafety.ConstantExpressions.ConstantExpression; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import javax.inject.Inject; + +/** A BugPattern; see the {@code summary}. */ +@BugPattern(summary = "This assertion is duplicate.", severity = SeverityLevel.WARNING) +public final class DuplicateAssertion extends BugChecker implements BlockTreeMatcher { + private final ConstantExpressions constantExpressions; + + @Inject + DuplicateAssertion(ConstantExpressions constantExpressions) { + this.constantExpressions = constantExpressions; + } + + @Override + public Description matchBlock(BlockTree tree, VisitorState state) { + var assertionLines = extractAssertionLines(tree, state); + + for (var entry : assertionLines.entries()) { + var source = entry.getKey(); + var line = entry.getValue(); + if (assertionLines.containsEntry(source, line - 1)) { + state.reportMatch( + buildDescription(tree.getStatements().get(line)) + .setMessage("This assertion is duplicated on the line above. Is that a mistake?") + .build()); + } + } + + return NO_MATCH; + } + + private ImmutableSetMultimap extractAssertionLines( + BlockTree tree, VisitorState state) { + ImmutableSetMultimap.Builder lines = ImmutableSetMultimap.builder(); + for (int i = 0; i < tree.getStatements().size(); i++) { + int finalI = i; + var statement = tree.getStatements().get(i); + if (!(statement instanceof ExpressionStatementTree est)) { + continue; + } + if (!(est.getExpression() instanceof MethodInvocationTree mit)) { + continue; + } + for (ExpressionTree receiver = getReceiver(mit); + receiver != null; + receiver = getReceiver(receiver)) { + var symbol = getSymbol(receiver); + if ((receiver instanceof MethodInvocationTree method) + && symbol.getSimpleName().contentEquals("assertThat") + && method.getArguments().size() == 1) { + constantExpressions + .constantExpression(getOnlyElement(method.getArguments()), state) + .ifPresent( + ce -> lines.put(new Assertion(state.getSourceForNode(statement), ce), finalI)); + } + } + } + return lines.build(); + } + + private record Assertion(String line, ConstantExpression assertee) {} +} diff --git a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 67dd2e009f8..166516ee748 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -122,6 +122,7 @@ import com.google.errorprone.bugpatterns.DoNotMockAutoValue; import com.google.errorprone.bugpatterns.DoNotMockChecker; import com.google.errorprone.bugpatterns.DoubleBraceInitialization; +import com.google.errorprone.bugpatterns.DuplicateAssertion; import com.google.errorprone.bugpatterns.DuplicateBranches; import com.google.errorprone.bugpatterns.DuplicateDateFormatField; import com.google.errorprone.bugpatterns.DuplicateMapKeys; @@ -940,6 +941,7 @@ public static ScannerSupplier warningChecks() { DoNotClaimAnnotations.class, DoNotMockAutoValue.class, DoubleCheckedLocking.class, + DuplicateAssertion.class, DuplicateBranches.class, DuplicateDateFormatField.class, EffectivelyPrivate.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DuplicateAssertionTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DuplicateAssertionTest.java new file mode 100644 index 00000000000..27531266ce5 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DuplicateAssertionTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2025 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.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class DuplicateAssertionTest { + public final CompilationTestHelper helper = + CompilationTestHelper.newInstance(DuplicateAssertion.class, getClass()); + + @Test + public void positive() { + helper + .addSourceLines( + "Test.java", + """ + import static com.google.common.truth.Truth.assertThat; + + import java.util.List; + + class Test { + public void test(List list) { + assertThat(list).contains(1); + // BUG: Diagnostic contains: + assertThat(list).contains(1); + } + } + """) + .doTest(); + } + + @Test + public void negative_notDuplicated() { + helper + .addSourceLines( + "Test.java", + """ + import static com.google.common.truth.Truth.assertThat; + + import java.util.List; + + class Test { + public void test(List list) { + assertThat(list).contains(1); + assertThat(list).contains(2); + } + } + """) + .doTest(); + } + + @Test + public void negative_notAssertion() { + helper + .addSourceLines( + "Test.java", + """ + class Test { + public void test() { + int x = 1; + x = 2; + x = 2; + } + } + """) + .doTest(); + } + + @Test + public void negative_impure() { + helper + .addSourceLines( + "Test.java", + """ + import java.util.Iterator; + import static com.google.common.truth.Truth.assertThat; + + import java.util.List; + + class Test { + public void test(List list) { + Iterator it = list.iterator(); + assertThat(it.next()).isEqualTo(1); + assertThat(it.next()).isEqualTo(1); + } + } + """) + .doTest(); + } +}