Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.BlockTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionStatementTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.StatementTree;
import org.sonar.plugins.java.api.tree.Tree;

import static java.util.Arrays.asList;
Expand All @@ -42,7 +46,7 @@ public final class UnitTestUtils {
private static final String ORG_JUNIT_TEST = "org.junit.Test";
public static final Pattern ASSERTION_METHODS_PATTERN = Pattern.compile(
"(assert|verify|fail|should|check|expect|validate|andExpect|approve).*" +
// Eclipse Vert.x with JUnit 5 (VertxTestContext)
// Eclipse Vert.x with JUnit 5 (VertxTestContext)
"|laxCheckpoint|succeedingThenComplete");
private static final Pattern TEST_METHODS_PATTERN = Pattern.compile("test.*|.*Test");

Expand All @@ -63,8 +67,8 @@ public final class UnitTestUtils {
"io.restassured.response.ValidatableResponseOptions", // restassured 3.x and 4.x
"io.restassured.module.mockmvc.response.ValidatableMockMvcResponse" // spring mock mvc extending the io.restassured library
)
.name(name -> "body".equals(name) ||
"time".equals(name) ||
.name(name -> "body" .equals(name) ||
"time" .equals(name) ||
name.startsWith("time") ||
name.startsWith("content") ||
name.startsWith("status") ||
Expand Down Expand Up @@ -93,21 +97,21 @@ public final class UnitTestUtils {

public static final MethodMatchers FAIL_METHOD_MATCHER = MethodMatchers.or(
MethodMatchers.create().ofTypes(
// JUnit 5
"org.junit.jupiter.api.Assertions",
// JUnit 4
"org.junit.Assert",
// JUnit 3
"junit.framework.Assert",
// Fest assert
"org.fest.assertions.Fail",
// AssertJ
"org.assertj.core.api.Fail",
"org.assertj.core.api.Assertions")
// JUnit 5
"org.junit.jupiter.api.Assertions",
// JUnit 4
"org.junit.Assert",
// JUnit 3
"junit.framework.Assert",
// Fest assert
"org.fest.assertions.Fail",
// AssertJ
"org.assertj.core.api.Fail",
"org.assertj.core.api.Assertions")
.names("fail").withAnyParameters().build(),
MethodMatchers.create().ofTypes(
// AssertJ
"org.assertj.core.api.Assertions")
// AssertJ
"org.assertj.core.api.Assertions")
.names("failBecauseExceptionWasNotThrown").withAnyParameters().build());

public static final MethodMatchers ASSERTIONS_METHOD_MATCHER = MethodMatchers.or(
Expand Down Expand Up @@ -209,8 +213,8 @@ private static boolean isOrOverridesJunit4TestMethod(MethodTree methodTree) {
return symbol.metadata().isAnnotatedWith(ORG_JUNIT_TEST)
// JUnit 4 considers as test any method overriding another annotated with @Test
|| symbol.overriddenSymbols().stream()
.map(Symbol::metadata)
.anyMatch(meta -> meta.isAnnotatedWith(ORG_JUNIT_TEST));
.map(Symbol::metadata)
.anyMatch(meta -> meta.isAnnotatedWith(ORG_JUNIT_TEST));
}

public static boolean isTestClass(ClassTree classTree) {
Expand Down Expand Up @@ -243,10 +247,24 @@ public static boolean methodNameMatchesAssertionMethodPattern(String methodName,
if (TEST_METHODS_PATTERN.matcher(methodName).matches()) {
return !REACTIVE_X_TEST_METHODS.matches(methodSymbol);
}
if ("verify".equals(methodName) || "failing".equals(methodName)) {
if ("verify" .equals(methodName) || "failing" .equals(methodName)) {
return !VERTX_TEST_CONTEXT_METHODS.matches(methodSymbol);
}

return ASSERTION_METHODS_PATTERN.matcher(methodName).matches();
}

public static boolean isTryCatchFail(BlockTree block) {
List<StatementTree> statements = block.body();
if (statements.isEmpty()) {
return false;
}
StatementTree lastStatement = statements.get(statements.size() - 1);
if (lastStatement.is(Tree.Kind.EXPRESSION_STATEMENT)) {
ExpressionTree expression = ((ExpressionStatementTree) lastStatement).expression();
return expression.is(Tree.Kind.METHOD_INVOCATION) && FAIL_METHOD_MATCHER.matches((MethodInvocationTree) expression);
}
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,15 @@
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.ExpressionStatementTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.LambdaExpressionTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.StatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TryStatementTree;

import static org.sonar.java.checks.helpers.UnitTestUtils.FAIL_METHOD_MATCHER;
import static org.sonar.java.checks.helpers.UnitTestUtils.isTryCatchFail;

public abstract class AbstractOneExpectedExceptionRule extends IssuableSubscriptionVisitor {

Expand Down Expand Up @@ -122,7 +120,7 @@ private void visitMethodInvocation(MethodInvocationTree mit) {
}

private void visitTryStatement(TryStatementTree tryStatementTree) {
if (isTryCatchFail(tryStatementTree)) {
if (isTryCatchFail(tryStatementTree.block())) {
List<Type> expectedTypes = tryStatementTree.catches().stream().map(c -> c.parameter().type().symbolType()).toList();
reportMultipleCallInTree(expectedTypes, tryStatementTree.block(), tryStatementTree.tryKeyword(), "body of this try/catch");
}
Expand Down Expand Up @@ -159,20 +157,6 @@ private static Optional<IdentifierTree> getExpectedException(ExpressionTree expe
return Optional.empty();
}

private static boolean isTryCatchFail(TryStatementTree tree) {
List<StatementTree> statementTrees = tree.block().body();
if (!statementTrees.isEmpty()) {
StatementTree lastElement = statementTrees.get(statementTrees.size() - 1);
if (lastElement.is(Tree.Kind.EXPRESSION_STATEMENT)) {
ExpressionTree expressionTree = ((ExpressionStatementTree) lastElement).expression();
if (expressionTree.is(Tree.Kind.METHOD_INVOCATION)) {
return FAIL_METHOD_MATCHER.matches((MethodInvocationTree) expressionTree);
}
}
}
return false;
}

abstract void reportMultipleCallInTree(List<Type> expectedExceptions, Tree treeToVisit, Tree reportLocation, String placeToRefactor);

static boolean isChecked(Type type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* SonarQube Java
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.filters;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.sonar.java.checks.InstantConversionsCheck;
import org.sonar.java.checks.helpers.MethodTreeUtils;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.plugins.java.api.JavaCheck;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.CatchTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.NewArrayTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TryStatementTree;
import org.sonar.plugins.java.api.tree.TypeTree;
import org.sonar.plugins.java.api.tree.UnionTypeTree;

import static org.sonar.java.checks.helpers.UnitTestUtils.isTryCatchFail;

public class ExpectedExceptionFilter extends BaseTreeVisitorIssueFilter {

private static final String ASSERTJ_ASSERTIONS = "org.assertj.core.api.Assertions";

private static final MethodMatchers ASSERT_THROWS_MATCHER = MethodMatchers.create()
.ofTypes("org.junit.Assert", "org.junit.jupiter.api.Assertions", "org.testng.Assert", "org.testng.AssertJUnit")
.names("assertThrows", "assertThrowsExactly", "expectThrows")
.withAnyParameters()
.build();

private static final MethodMatchers ASSERTJ_CATCH_THROWABLE_OF_TYPE = MethodMatchers.create()
.ofTypes(ASSERTJ_ASSERTIONS)
.names("catchThrowableOfType")
.addParametersMatcher("org.assertj.core.api.ThrowableAssert$ThrowingCallable", "java.lang.Class")
.build();

private static final MethodMatchers ASSERTJ_ASSERT_CODE = MethodMatchers.create()
.ofTypes(ASSERTJ_ASSERTIONS)
.names("assertThatCode", "assertThatThrownBy")
.withAnyParameters()
.build();

private static final MethodMatchers ASSERTJ_EXCEPTION_OF_TYPE = MethodMatchers.create()
.ofTypes(ASSERTJ_ASSERTIONS, "org.assertj.core.api.BDDAssertions")
.names("assertThatExceptionOfType", "thenExceptionOfType")
.addParametersMatcher("java.lang.Class")
.build();

private static final MethodMatchers ASSERTJ_TYPED_EXCEPTION = MethodMatchers.create()
.ofTypes(ASSERTJ_ASSERTIONS, "org.assertj.core.api.BDDAssertions")
.names("assertThatException", "assertThatRuntimeException", "thenException", "thenRuntimeException")
.withAnyParameters()
.build();

private static final MethodMatchers ASSERTJ_IS_THROWN_BY = MethodMatchers.create()
.ofTypes("org.assertj.core.api.ThrowableTypeAssert")
.names("isThrownBy")
.addParametersMatcher("org.assertj.core.api.ThrowableAssert$ThrowingCallable")
.build();

private static final MethodMatchers ASSERTJ_INSTANCE_OF_PREDICATES = MethodMatchers.create()
.ofSubTypes("org.assertj.core.api.Assert")
.names("isInstanceOf", "isInstanceOfAny")
.withAnyParameters()
.build();

private static final MethodMatchers ASSERTJ_EXACT_INSTANCE_OF_PREDICATES = MethodMatchers.create()
.ofSubTypes("org.assertj.core.api.Assert")
.names("isExactlyInstanceOf", "isOfAnyClassIn")
.withAnyParameters()
.build();

private static final String DATE_TIME_EXCEPTION = "java.time.DateTimeException";

private static final Set<String> DATE_TIME_EXCEPTION_SUPERTYPES = Set.of(
"java.lang.RuntimeException",
"java.lang.Exception",
"java.lang.Throwable"
);

@Override
public Set<Class<? extends JavaCheck>> filteredRules() {
return Set.of(InstantConversionsCheck.class);
}

@Override
public void visitMethod(MethodTree tree) {
if (hasExpectedDateTimeExceptionAnnotation(tree.modifiers().annotations())) {
excludeLines(tree, InstantConversionsCheck.class);
}
super.visitMethod(tree);
}

@Override
public void visitTryStatement(TryStatementTree tree) {
if (isTryCatchFailExpectingDateTimeException(tree)) {
excludeLines(tree.block(), InstantConversionsCheck.class);
}
super.visitTryStatement(tree);
}

@Override
public void visitMethodInvocation(MethodInvocationTree tree) {
excludeExpectedDateTimeExceptionIssues(tree);
super.visitMethodInvocation(tree);
}

private void excludeExpectedDateTimeExceptionIssues(MethodInvocationTree mit) {
if (ASSERT_THROWS_MATCHER.matches(mit)) {
excludeAssertThrowsExecutableForDateTimeException(mit);
} else if (ASSERTJ_CATCH_THROWABLE_OF_TYPE.matches(mit) && isDateTimeExceptionClass(mit.arguments().get(1), false)) {
excludeLines(mit.arguments().get(0), InstantConversionsCheck.class);
} else if (ASSERTJ_ASSERT_CODE.matches(mit)) {
MethodTreeUtils.subsequentMethodInvocation(mit, ASSERTJ_INSTANCE_OF_PREDICATES).ifPresent(subsequentMit -> {
if (anyHasDateTimeExceptionType(subsequentMit.arguments(), false)) {
excludeLines(mit.arguments().get(0), InstantConversionsCheck.class);
}
});
MethodTreeUtils.subsequentMethodInvocation(mit, ASSERTJ_EXACT_INSTANCE_OF_PREDICATES).ifPresent(subsequentMit -> {
if (anyHasDateTimeExceptionType(subsequentMit.arguments(), true)) {
excludeLines(mit.arguments().get(0), InstantConversionsCheck.class);
}
});
} else if (ASSERTJ_TYPED_EXCEPTION.matches(mit) || (ASSERTJ_EXCEPTION_OF_TYPE.matches(mit) && isDateTimeExceptionClass(mit.arguments().get(0), false))) {
MethodTreeUtils.subsequentMethodInvocation(mit, ASSERTJ_IS_THROWN_BY).ifPresent(subsequentMit ->
excludeLines(subsequentMit.arguments().get(0), InstantConversionsCheck.class)
);
}
}

private void excludeAssertThrowsExecutableForDateTimeException(MethodInvocationTree mit) {
Arguments arguments = mit.arguments();
if (arguments.size() < 2) {
return;
}
int expectedTypeIndex = firstArgumentIsMessage(arguments) ? 1 : 0;
int executableIndex = expectedTypeIndex + 1;
if (arguments.size() > executableIndex && isDateTimeExceptionClass(arguments.get(expectedTypeIndex), "assertThrowsExactly".equals(mit.methodSymbol().name()))) {
excludeLines(arguments.get(executableIndex), InstantConversionsCheck.class);
}
}

private static boolean firstArgumentIsMessage(Arguments arguments) {
return arguments.size() >= 3 && arguments.get(0).symbolType().is("java.lang.String");
}

private static boolean isTryCatchFailExpectingDateTimeException(TryStatementTree tryStatement) {
return isTryCatchFail(tryStatement.block()) && tryStatement.catches().stream().anyMatch(ExpectedExceptionFilter::isDateTimeExceptionCatch);
}

private static boolean isDateTimeException(Type type, boolean exact) {
return type.isSubtypeOf(DATE_TIME_EXCEPTION) || (!exact && DATE_TIME_EXCEPTION_SUPERTYPES.stream().anyMatch(type::is));
}

private static boolean isDateTimeExceptionClass(ExpressionTree expression, boolean exact) {
if (expression.is(Tree.Kind.NEW_ARRAY)) {
return ((NewArrayTree) expression).initializers().stream()
.anyMatch(initializer -> isDateTimeExceptionClass(initializer, exact));
}
return classLiteralType(expression)
.map(type -> isDateTimeException(type, exact))
.orElse(false);
}

private static Optional<Type> classLiteralType(ExpressionTree expression) {
if (expression.is(Tree.Kind.MEMBER_SELECT)) {
MemberSelectExpressionTree memberSelect = (MemberSelectExpressionTree) expression;
if ("class" .equals(memberSelect.identifier().name())) {
return Optional.of(memberSelect.expression().symbolType());
}
}
return Optional.empty();
}

private static boolean isDateTimeExceptionCatch(CatchTree catchTree) {
return exceptionTypes(catchTree.parameter().type()).stream()
.anyMatch(type -> isDateTimeException(type.symbolType(), false));
}

private static List<TypeTree> exceptionTypes(TypeTree typeTree) {
if (typeTree.is(Tree.Kind.UNION_TYPE)) {
return ((UnionTypeTree) typeTree).typeAlternatives();
}
return List.of(typeTree);
}

private static boolean hasExpectedDateTimeExceptionAnnotation(List<AnnotationTree> annotations) {
return annotations.stream().anyMatch(annotation -> {
Type annotationType = annotation.annotationType().symbolType();
if (annotationType.is("org.junit.Test")) {
return hasExpectedDateTimeExceptionArgument(annotation, "expected");
} else if (annotationType.is("org.testng.annotations.Test")) {
return hasExpectedDateTimeExceptionArgument(annotation, "expectedExceptions");
}
return false;
});
}

private static boolean hasExpectedDateTimeExceptionArgument(AnnotationTree annotation, String attributeName) {
return annotation.arguments().stream()
.filter(argument -> attributeName.equals(ExpressionUtils.annotationAttributeName(argument)))
.anyMatch(argument -> isDateTimeExceptionClass(annotationValue(argument), false));
}

private static ExpressionTree annotationValue(ExpressionTree expression) {
return expression.is(Tree.Kind.ASSIGNMENT) ? ((AssignmentExpressionTree) expression).expression() : expression;
}

private static boolean anyHasDateTimeExceptionType(List<ExpressionTree> expressions, boolean exact) {
return expressions.stream().anyMatch(expression -> isDateTimeExceptionClass(expression, exact));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ List<JavaIssueFilter> issueFilters() {
new GoogleAutoFilter(),
new SuppressWarningFilter(),
new GeneratedCodeFilter(),
new ExpectedExceptionFilter(),
new SpringFilter()));
}
return issueFilters;
Expand Down
Loading
Loading