org.sonar.java.checks.RedundantThrowsDeclarationCheck Maven / Gradle / Ivy
/*
* SonarQube Java
* Copyright (C) 2012-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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 GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.java.checks;
import com.google.common.collect.ImmutableList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.RspecKey;
import org.sonar.java.checks.helpers.Javadoc;
import org.sonar.java.checks.serialization.SerializableContract;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.java.model.ModifiersUtils;
import org.sonar.java.resolve.JavaSymbol;
import org.sonar.java.resolve.JavaType;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.BlockTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.LambdaExpressionTree;
import org.sonar.plugins.java.api.tree.ListTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.Modifier;
import org.sonar.plugins.java.api.tree.ModifiersTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
import org.sonar.plugins.java.api.tree.StatementTree;
import org.sonar.plugins.java.api.tree.ThrowStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TryStatementTree;
import org.sonar.plugins.java.api.tree.TypeTree;
@Rule(key = "RedundantThrowsDeclarationCheck")
@RspecKey("S1130")
public class RedundantThrowsDeclarationCheck extends IssuableSubscriptionVisitor {
@Override
public List nodesToVisit() {
return ImmutableList.of(Tree.Kind.METHOD, Tree.Kind.CONSTRUCTOR);
}
@Override
public void visitNode(Tree tree) {
ListTree thrownList = ((MethodTree) tree).throwsClauses();
if (!hasSemantic() || thrownList.isEmpty()) {
return;
}
checkMethodThrownList((MethodTree) tree, thrownList);
}
private void checkMethodThrownList(MethodTree methodTree, ListTree thrownList) {
Set thrownExceptions = thrownExceptionsFromBody(methodTree);
boolean hasTryWithResourceInBody = hasTryWithResourceInBody(methodTree);
boolean isOverridableMethod = ((JavaSymbol.MethodJavaSymbol) methodTree.symbol()).isOverridable();
List undocumentedExceptionNames = new Javadoc(methodTree).undocumentedThrownExceptions();
Set reported = new HashSet<>();
for (TypeTree typeTree : thrownList) {
Type exceptionType = typeTree.symbolType();
if (hasTryWithResourceInBody && (exceptionType.is("java.io.IOException") || exceptionType.is("java.lang.Exception"))) {
// method 'close()' from 'java.lang.AutoCloseable' interface throws 'java.lang.Exception'
// method 'close()' from 'java.io.Closeable' interface throws 'java.io.IOException"
continue;
}
String fullyQualifiedName = exceptionType.fullyQualifiedName();
if (!reported.contains(fullyQualifiedName)) {
String superTypeName = isSubclassOfAny(exceptionType, thrownList);
if (superTypeName != null) {
reportIssue(typeTree, String.format("Remove the declaration of thrown exception '%s' which is a subclass of '%s'.", fullyQualifiedName, superTypeName));
} else if (exceptionType.isSubtypeOf("java.lang.RuntimeException")) {
reportIssue(typeTree, String.format("Remove the declaration of thrown exception '%s' which is a runtime exception.", fullyQualifiedName));
} else if (declaredMoreThanOnce(fullyQualifiedName, thrownList)) {
reportIssue(typeTree, String.format("Remove the redundant '%s' thrown exception declaration(s).", fullyQualifiedName));
} else if (canNotBeThrown(methodTree, exceptionType, thrownExceptions) && (!isOverridableMethod || undocumentedExceptionNames.contains(exceptionType.name()))) {
reportIssue(typeTree, String.format("Remove the declaration of thrown exception '%s', as it cannot be thrown from %s's body.", fullyQualifiedName,
methodTreeType(methodTree)));
}
reported.add(fullyQualifiedName);
}
}
}
private static String methodTreeType(MethodTree tree) {
return tree.is(Tree.Kind.CONSTRUCTOR) ? "constructor" : "method";
}
private static boolean hasTryWithResourceInBody(MethodTree methodTree) {
BlockTree block = methodTree.block();
if (block == null) {
return false;
}
TryWithResourcesVisitor visitor = new TryWithResourcesVisitor();
block.accept(visitor);
return visitor.hasTryWithResource;
}
private static class TryWithResourcesVisitor extends BaseTreeVisitor {
private boolean hasTryWithResource = false;
@Override
public void visitTryStatement(TryStatementTree tree) {
if (!tree.resourceList().isEmpty()) {
hasTryWithResource = true;
}
super.visitTryStatement(tree);
}
@Override
public void visitClass(ClassTree tree) {
// skip anonymous classes
}
@Override
public void visitLambdaExpression(LambdaExpressionTree lambdaExpressionTree) {
// skip lambdas
}
}
private static boolean canNotBeThrown(MethodTree methodTree, Type exceptionType, @Nullable Set thrownExceptions) {
if (isOverridingOrDesignedForExtension(methodTree)
|| !exceptionType.isSubtypeOf("java.lang.Exception")
|| exceptionType.isSubtypeOf("java.lang.RuntimeException")
|| thrownExceptions == null) {
return false;
}
if (thrownExceptions.stream().anyMatch(t -> ((JavaType) t).isTagged(JavaType.TYPEVAR))) {
// should be handled by SONARJAVA-1778 - type substitution not applied on thrown type when parameterized on parametric methods
return false;
}
return thrownExceptions.stream().noneMatch(t -> t.isSubtypeOf(exceptionType));
}
private static boolean isOverridingOrDesignedForExtension(MethodTree methodTree) {
// we need to be sure that it's not an override
return !Boolean.FALSE.equals(methodTree.isOverriding())
|| SerializableContract.SERIALIZABLE_CONTRACT_METHODS.contains(methodTree.simpleName().name())
|| isDesignedForExtension(methodTree);
}
private static boolean isDesignedForExtension(MethodTree methodTree) {
ModifiersTree modifiers = methodTree.modifiers();
if (ModifiersUtils.hasModifier(modifiers, Modifier.PRIVATE)) {
return false;
}
return ModifiersUtils.hasModifier(modifiers, Modifier.DEFAULT)
|| emptyBody(methodTree)
|| onlyReturnLiteralsOrThrowException(methodTree);
}
private static boolean onlyReturnLiteralsOrThrowException(MethodTree methodTree) {
BlockTree block = methodTree.block();
if (block == null) {
return false;
}
List body = block.body();
if (body.size() != 1) {
return false;
}
StatementTree singleStatement = body.get(0);
return singleStatement.is(Tree.Kind.THROW_STATEMENT) || returnStatementWithLiteral(singleStatement);
}
private static boolean returnStatementWithLiteral(StatementTree statement) {
if (statement.is(Tree.Kind.RETURN_STATEMENT)) {
ExpressionTree expression = ((ReturnStatementTree) statement).expression();
return expression == null || ExpressionUtils.skipParentheses(expression).is(
Tree.Kind.NULL_LITERAL,
Tree.Kind.STRING_LITERAL,
Tree.Kind.BOOLEAN_LITERAL,
Tree.Kind.CHAR_LITERAL,
Tree.Kind.DOUBLE_LITERAL,
Tree.Kind.FLOAT_LITERAL,
Tree.Kind.LONG_LITERAL,
Tree.Kind.INT_LITERAL);
}
return false;
}
private static boolean emptyBody(MethodTree methodTree) {
BlockTree block = methodTree.block();
return block != null && block.body().isEmpty();
}
@Nullable
private static Set thrownExceptionsFromBody(MethodTree methodTree) {
BlockTree block = methodTree.block();
if (block != null) {
MethodInvocationVisitor visitor = new MethodInvocationVisitor();
block.accept(visitor);
return visitor.thrownExceptions();
}
return null;
}
private static class MethodInvocationVisitor extends BaseTreeVisitor {
private Set thrownExceptions = new HashSet<>();
private boolean visitedUnknown = false;
@Nullable
public Set thrownExceptions() {
if (visitedUnknown || thrownExceptions.stream().anyMatch(Type::isUnknown)) {
// as soon as there is an unknown type, we discard any attempt to find an issue
return null;
}
return thrownExceptions;
}
@Override
public void visitMethodInvocation(MethodInvocationTree tree) {
addThrownTypes(tree.symbol());
super.visitMethodInvocation(tree);
}
@Override
public void visitNewClass(NewClassTree tree) {
addThrownTypes(tree.constructorSymbol());
super.visitNewClass(tree);
}
private void addThrownTypes(Symbol methodSymbol) {
if (!visitedUnknown) {
if (methodSymbol.isUnknown() || !methodSymbol.isMethodSymbol()) {
visitedUnknown = true;
} else {
thrownExceptions.addAll(((Symbol.MethodSymbol) methodSymbol).thrownTypes());
}
}
}
@Override
public void visitThrowStatement(ThrowStatementTree tree) {
Type exceptionType = tree.expression().symbolType();
thrownExceptions.add(exceptionType);
super.visitThrowStatement(tree);
}
@Override
public void visitClass(ClassTree tree) {
// skip anonymous classes
}
@Override
public void visitLambdaExpression(LambdaExpressionTree lambdaExpressionTree) {
// skip lambdas
}
}
private static boolean declaredMoreThanOnce(String fullyQualifiedName, ListTree thrown) {
boolean firstOccurrenceFound = false;
for (TypeTree typeTree : thrown) {
if (typeTree.symbolType().is(fullyQualifiedName)) {
if (firstOccurrenceFound) {
return true;
} else {
firstOccurrenceFound = true;
}
}
}
return false;
}
private static String isSubclassOfAny(Type type, ListTree thrownList) {
for (TypeTree thrown : thrownList) {
String name = thrown.symbolType().fullyQualifiedName();
if (!type.is(name) && type.isSubtypeOf(name)) {
return name;
}
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy