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

org.sonar.python.checks.ChildAndParentExceptionCaughtCheck Maven / Gradle / Ivy

The newest version!
/*
 * SonarQube Python Plugin
 * Copyright (C) 2011-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 Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * 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.python.checks;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
import org.sonar.plugins.python.api.symbols.ClassSymbol;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.tree.ExceptClause;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.Token;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.Tuple;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.quickfix.TextEditUtils;
import org.sonar.python.tree.TreeUtils;

@Rule(key = "S5713")
public class ChildAndParentExceptionCaughtCheck extends PythonSubscriptionCheck {
  public static final String QUICK_FIX_MESSAGE = "Remove the redundant Exception";

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.EXCEPT_CLAUSE, ChildAndParentExceptionCaughtCheck::checkExceptClause);
    context.registerSyntaxNodeConsumer(Tree.Kind.EXCEPT_GROUP_CLAUSE, ChildAndParentExceptionCaughtCheck::checkExceptClause);
  }

  private static void checkExceptClause(SubscriptionContext ctx) {
    ExceptClause exceptClause = (ExceptClause) ctx.syntaxNode();
    Map> caughtExceptionsBySymbol = new HashMap<>();
    Expression exceptionExpression = exceptClause.exception();
    if (exceptionExpression == null) {
      return;
    }
    TreeUtils.flattenTuples(exceptionExpression).forEach(e -> addExceptionExpression(e, caughtExceptionsBySymbol));
    checkCaughtExceptions(ctx, caughtExceptionsBySymbol);
  }

  private static void checkCaughtExceptions(SubscriptionContext ctx, Map> caughtExceptionsBySymbol) {
    caughtExceptionsBySymbol.forEach((currentSymbol, caughtExceptionsWithSameSymbol) -> {
      Expression currentException = caughtExceptionsWithSameSymbol.get(0);
      if (caughtExceptionsWithSameSymbol.size() > 1) {
        var issue = ctx.addIssue(currentException, "Remove this duplicate Exception class.");
        addQuickFix(issue, currentException);
        caughtExceptionsWithSameSymbol.stream().skip(1).forEach(e -> issue.secondary(e, "Duplicate."));
      }

      var caughtParentExceptions = caughtExceptionsBySymbol.entrySet()
        .stream()
        .filter(entry -> entry.getKey() != currentSymbol && currentSymbol.isOrExtends(entry.getKey()))
        .toList();

      if (!caughtParentExceptions.isEmpty()) {
        var issue = ctx.addIssue(currentException, "Remove this redundant Exception class; it derives from another which is already caught.");
        addQuickFix(issue, currentException);

        caughtParentExceptions.stream()
          .map(Map.Entry::getValue)
          .forEach(entries -> addSecondaryLocations(issue, entries));
      }
    });
  }

  private static void addQuickFix(PreciseIssue issue, Expression currentException) {
    var quickFix = createQuickFix(currentException);
    if (quickFix != null) {
      issue.addQuickFix(quickFix);
    }
  }

  private static List collectNamesFromTuple(Expression expression) {
    expression = Expressions.removeParentheses(expression);
    if (expression.is(Tree.Kind.TUPLE)) {
      var tuple = (Tuple) expression;
      return tuple.elements()
        .stream()
        .map(ChildAndParentExceptionCaughtCheck::collectNames)
        .flatMap(Collection::stream)
        .collect(Collectors.toList());
    }
    throw new IllegalArgumentException("Unsupported kind of tree element: " + expression.getKind().name());
  }
  private static List collectNames(Expression expression) {
    expression = Expressions.removeParentheses(expression);
    if (expression.is(Tree.Kind.NAME)) {
      var name = (Name) expression;
      return List.of(name.name());
    } else if (expression.is(Tree.Kind.QUALIFIED_EXPR)) {
      var name = TreeUtils.tokens(expression)
        .stream()
        .map(Token::value)
        .collect(Collectors.joining());
      return List.of(name);
    }
    throw new IllegalArgumentException("Unsupported kind of tree element: " + expression.getKind().name());
  }

  private static PythonQuickFix createQuickFix(Expression currentException) {
    try {
      var currentExceptionName = collectNames(currentException).get(0);

      return Optional.of(currentException)
        .map(exception -> TreeUtils.firstAncestorOfKind(exception, Tree.Kind.EXCEPT_CLAUSE))
        .map(ExceptClause.class::cast)
        .map(ExceptClause::exception)
        .map(exceptions -> {
          List names = collectNamesFromTuple(exceptions);
          names.remove(currentExceptionName);

          var text = names.size() == 1 ? names.get(0) : names.stream().collect(Collectors.joining(", ", "(", ")"));

          return PythonQuickFix.newQuickFix(QUICK_FIX_MESSAGE)
            .addTextEdit(TextEditUtils.replace(exceptions, text))
            .build();
        }).orElse(null);
    } catch (IllegalArgumentException e) {
      // expression contains subexpressions that are out of scope for quick fixing
      return null;
    }
  }

  private static void addExceptionExpression(Expression exceptionExpression, Map> caughtExceptionsByFQN) {
    if (exceptionExpression instanceof HasSymbol hasSymbol) {
      Symbol symbol = hasSymbol.symbol();
      if (symbol != null && symbol.kind().equals(Symbol.Kind.CLASS)) {
        ClassSymbol classSymbol = (ClassSymbol) symbol;
        caughtExceptionsByFQN.computeIfAbsent(classSymbol, k -> new ArrayList<>()).add(exceptionExpression);
      }
    }
  }

  private static void addSecondaryLocations(PreciseIssue issue, List others) {
    for (Expression other : others) {
      issue.secondary(other, "Parent class.");
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy