All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.java.checks.RedundantThrowsDeclarationCheck Maven / Gradle / Ivy

There is a newer version: 8.9.0.37768
Show newest version
/*
 * 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