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

org.sonar.java.checks.spring.StatusCodesOnResponseCheck Maven / Gradle / Ivy

/*
 * SonarQube Java
 * Copyright (C) 2012-2024 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.spring;

import java.util.List;
import java.util.stream.Stream;
import org.sonar.check.Rule;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "S6863")
public class StatusCodesOnResponseCheck extends IssuableSubscriptionVisitor {

  public static final String RESPONSE_ENTITY = "org.springframework.http.ResponseEntity";
  public static final String ISSUE_MESSAGE = "Set a HttpStatus code reflective of the operation.";

  /*
   * Values for the okCodes list are extracted from:
   * https://docs.spring.io/spring-framework/docs/5.0.6.RELEASE/javadoc-api/index.html?org/springframework/http/HttpStatus.html
   * by taking all that return 1xx, 2xx, 3xx code.
   */
  private static final List OK_CODES = List.of("ACCEPTED",
    "ALREADY_REPORTED",
    "CHECKPOINT",
    "CONTINUE",
    "CREATED",
    "FOUND",
    "IM_USED",
    "MOVED_PERMANENTLY",
    "MULTIPLE_CHOICES",
    "MULTI_STATUS",
    "NON_AUTHORITATIVE_INFORMATION",
    "NOT_MODIFIED",
    "NO_CONTENT",
    "OK",
    "PARTIAL_CONTENT",
    "PERMANENT_REDIRECT",
    "PROCESSING",
    "RESET_CONTENT",
    "SEE_OTHER",
    "SWITCHING_PROTOCOLS",
    "TEMPORARY_REDIRECT");

  /*
   * Values for the errorCodes list are extracted from:
   * https://docs.spring.io/spring-framework/docs/5.0.6.RELEASE/javadoc-api/index.html?org/springframework/http/HttpStatus.html
   * by taking all that return 4xx, 5xx code.
   */
  private static final List ERROR_CODES = List.of("BAD_GATEWAY",
    "BAD_REQUEST",
    "BANDWIDTH_LIMIT_EXCEEDED",
    "CONFLICT",
    "EXPECTATION_FAILED",
    "FAILED_DEPENDENCY",
    "FORBIDDEN",
    "GATEWAY_TIMEOUT",
    "GONE",
    "HTTP_VERSION_NOT_SUPPORTED",
    "INSUFFICIENT_STORAGE",
    "INTERNAL_SERVER_ERROR",
    "I_AM_A_TEAPOT",
    "LENGTH_REQUIRED",
    "LOCKED",
    "LOOP_DETECTED",
    "METHOD_NOT_ALLOWED",
    "NETWORK_AUTHENTICATION_REQUIRED",
    "NOT_ACCEPTABLE",
    "NOT_EXTENDED",
    "NOT_FOUND",
    "NOT_IMPLEMENTED",
    "PAYLOAD_TOO_LARGE",
    "PAYMENT_REQUIRED",
    "PRECONDITION_FAILED",
    "PRECONDITION_REQUIRED",
    "PROXY_AUTHENTICATION_REQUIRED",
    "REQUESTED_RANGE_NOT_SATISFIABLE",
    "REQUEST_HEADER_FIELDS_TOO_LARGE",
    "REQUEST_TIMEOUT",
    "SERVICE_UNAVAILABLE",
    "TOO_MANY_REQUESTS",
    "UNAUTHORIZED",
    "UNAVAILABLE_FOR_LEGAL_REASONS",
    "UNPROCESSABLE_ENTITY",
    "UNSUPPORTED_MEDIA_TYPE",
    "UPGRADE_REQUIRED",
    "URI_TOO_LONG",
    "VARIANT_ALSO_NEGOTIATES");

  private static final MethodMatchers STATUS_METHOD_MATCHERS = MethodMatchers.create()
    .ofTypes(RESPONSE_ENTITY)
    .names("status")
    .addParametersMatcher("org.springframework.http.HttpStatus")
    .build();

  private static final MethodMatchers OK_METHOD_MATCHERS = MethodMatchers.create()
    .ofTypes(RESPONSE_ENTITY)
    .names("ok", "created", "accepted", "noContent")
    .withAnyParameters()
    .build();

  private static final MethodMatchers ERROR_METHODS_MATCHER = MethodMatchers.create()
    .ofTypes(RESPONSE_ENTITY)
    .names("badRequest", "notFound", "unprocessableEntity")
    .addWithoutParametersMatcher()
    .build();

  @Override
  public List nodesToVisit() {
    return List.of(Tree.Kind.CLASS);
  }

  @Override
  public void visitNode(Tree tree) {
    ClassTree classTree = (ClassTree) tree;
    if (!isClassController(classTree)) {
      return;
    }

    MethodInvocationVisitor methodInvocationVisitor = new MethodInvocationVisitor();
    classTree.accept(methodInvocationVisitor);
  }

  private class MethodInvocationVisitor extends BaseTreeVisitor {

    @Override
    public void visitMethodInvocation(MethodInvocationTree methodInvocationTree) {

      if (STATUS_METHOD_MATCHERS.matches(methodInvocationTree)) {
        checkTryCatch(methodInvocationTree);
        return;
      }

      if (OK_METHOD_MATCHERS.matches(methodInvocationTree)) {
        Tree catchParent = checkCatch(methodInvocationTree, false);
        if (catchParent == null) {
          checkTry(methodInvocationTree, true);
        }
        return;
      }

      if (ERROR_METHODS_MATCHER.matches(methodInvocationTree)) {
        Tree catchParent = checkCatch(methodInvocationTree, true);
        if (catchParent == null) {
          checkTry(methodInvocationTree, false);
        }
        return;
      }

      super.visitMethodInvocation(methodInvocationTree);
    }

    private void checkTryCatch(MethodInvocationTree methodInvocationTree) {
      Tree catchParent = ExpressionUtils.getParentOfType(methodInvocationTree, Tree.Kind.CATCH);
      boolean isError = isCodeInList(methodInvocationTree, ERROR_CODES);

      if (catchParent != null && !isError) {
        reportIssue(methodInvocationTree, ISSUE_MESSAGE);
        return;
      }

      if (catchParent == null) {
        Tree tryParent = ExpressionUtils.getParentOfType(methodInvocationTree, Tree.Kind.TRY_STATEMENT);
        boolean isOk = isCodeInList(methodInvocationTree, OK_CODES);

        if (tryParent != null && !isOk) {
          reportIssue(methodInvocationTree, ISSUE_MESSAGE);
        }
      }
    }

    private boolean isCodeInList(MethodInvocationTree methodInvocationTree, List codes) {
      ExpressionTree arg = methodInvocationTree.arguments().get(0);
      if (arg.is(Tree.Kind.MEMBER_SELECT)) {
        MemberSelectExpressionTree memberSelectExpressionTree = (MemberSelectExpressionTree) arg;
        return codes.contains(memberSelectExpressionTree.identifier().name());
      } else if (arg.is(Tree.Kind.IDENTIFIER)) {
        IdentifierTree identifierTree = (IdentifierTree) arg;
        return codes.contains(identifierTree.name());
      }
      return true;
    }

    private Tree checkCatch(MethodInvocationTree methodInvocationTree, boolean isError) {
      Tree catchParent = ExpressionUtils.getParentOfType(methodInvocationTree, Tree.Kind.CATCH);
      if (catchParent != null && !isError) {
        reportIssue(methodInvocationTree, ISSUE_MESSAGE);
      }
      return catchParent;
    }

    private Tree checkTry(MethodInvocationTree methodInvocationTree, boolean isOk) {
      Parents parents = getParentsTryAndIf(methodInvocationTree);
      if (!parents.foundIfWithinTry() && parents.foundTryStatement && !isOk) {
        reportIssue(methodInvocationTree, ISSUE_MESSAGE);
      }
      return null;
    }
  }

  private static Parents getParentsTryAndIf(Tree tree) {
    Tree parent = tree.parent();
    boolean foundIfStatement = false;
    boolean foundTryStatement = false;
    while (parent != null && !parent.is(Tree.Kind.METHOD)) {
      if (parent.is(Tree.Kind.IF_STATEMENT)) {
        foundIfStatement = true;
      }
      if (parent.is(Tree.Kind.TRY_STATEMENT)) {
        foundTryStatement = true;
        break;
      }
      parent = parent.parent();
    }
    return new Parents(foundIfStatement, foundTryStatement);
  }

  private static class Parents {
    boolean foundIfStatement;
    boolean foundTryStatement;

    public Parents(boolean foundIfStatement, boolean foundTryStatement) {
      this.foundIfStatement = foundIfStatement;
      this.foundTryStatement = foundTryStatement;
    }

    boolean foundIfWithinTry() {
      return foundIfStatement && foundTryStatement;
    }
  }

  private static boolean isClassController(ClassTree classTree) {
    return Stream.of("org.springframework.stereotype.Controller", "org.springframework.web.bind.annotation.RestController")
      .anyMatch(annotation -> classTree.symbol().metadata().isAnnotatedWith(annotation));
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy