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

org.sonar.python.checks.hotspots.DebugModeCheck Maven / Gradle / Ivy

There is a newer version: 4.23.0.17664
Show 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.hotspots;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import javax.annotation.CheckForNull;
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.symbols.ClassSymbol;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.tree.Argument;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.ExpressionList;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.RegularArgument;
import org.sonar.plugins.python.api.tree.StringLiteral;
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
import org.sonar.plugins.python.api.tree.Tree.Kind;
import org.sonar.python.checks.utils.Expressions;
import org.sonar.python.tree.TreeUtils;

@Rule(key = DebugModeCheck.CHECK_KEY)
public class DebugModeCheck extends PythonSubscriptionCheck {
  public static final String CHECK_KEY = "S4507";
  public static final String DJANGO_CONFIGURE_FQN = "django.conf.settings.configure";
  private static final String FLASK_RUN_FQN = "flask.app.Flask.run";
  private static final String FLASK_APP_CONFIG_FQN = "flask.app.Flask.config";
  public static final String FLASK_APP_DEBUG_FQN = "flask.app.Flask.debug";
  public static final String FLASK_GRAPHQL_VIEW_AS_VIEW_FQN = "flask_graphql.GraphQLView.as_view";
  private static final String MESSAGE = "Make sure this debug feature is deactivated before delivering the code in production.";
  private static final List debugProperties = Arrays.asList("DEBUG", "DEBUG_PROPAGATE_EXCEPTIONS");
  private static final List settingFiles = Arrays.asList("global_settings.py", "settings.py");

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Kind.CALL_EXPR, DebugModeCheck::callExpressionConsumer);
    context.registerSyntaxNodeConsumer(Kind.ASSIGNMENT_STMT, DebugModeCheck::assignmentStatementConsumer);
  }

  private static void callExpressionConsumer(SubscriptionContext ctx) {
    CallExpression callExpression = (CallExpression) ctx.syntaxNode();
    List arguments = callExpression.arguments();
    if (!(callExpression.callee() instanceof QualifiedExpression) || arguments.isEmpty()) {
      return;
    }

    var qualifiedName = getQualifiedName(callExpression);

    if (DJANGO_CONFIGURE_FQN.equals(qualifiedName)) {
      arguments.stream().filter(DebugModeCheck::isDebugArgument).forEach(arg -> ctx.addIssue(arg, MESSAGE));
    }

    if (FLASK_RUN_FQN.equals(qualifiedName)) {
      RegularArgument debugArgument = TreeUtils.nthArgumentOrKeyword(2, "debug", arguments);
      Optional.ofNullable(debugArgument)
        .map(RegularArgument::expression)
        .filter(DebugModeCheck::isTrue)
        .ifPresent(name -> ctx.addIssue(debugArgument, MESSAGE));
    }

    if (isFlaskGraphqlViewAsViewMethodCall(callExpression)) {
      var argument = TreeUtils.nthArgumentOrKeyword(-1, "graphiql", arguments);
      Optional.ofNullable(argument)
        .map(RegularArgument::expression)
        .filter(DebugModeCheck::isTrue)
        .ifPresent(name -> ctx.addIssue(argument, MESSAGE));
    }
  }

  private static boolean isFlaskGraphqlViewAsViewMethodCall(CallExpression callExpression) {
    var qualifiedName = getQualifiedName(callExpression);
    if (FLASK_GRAPHQL_VIEW_AS_VIEW_FQN.equals(qualifiedName)) {
      return true;
    }
    boolean isAsViewMethodCall = Optional.of(callExpression)
      .map(CallExpression::calleeSymbol)
      .map(Symbol::name)
      .filter("as_view"::equals)
      .isPresent();

    if (isAsViewMethodCall) {
      return Optional.of(callExpression)
        .map(CallExpression::callee)
        .flatMap(TreeUtils.toOptionalInstanceOfMapper(QualifiedExpression.class))
        .map(QualifiedExpression::qualifier)
        .filter(HasSymbol.class::isInstance)
        .map(HasSymbol.class::cast)
        .map(HasSymbol::symbol)
        .filter(ClassSymbol.class::isInstance)
        .map(ClassSymbol.class::cast)
        .filter(s -> s.isOrExtends("flask_graphql.GraphQLView"))
        .isPresent();
    }
    return false;
  }

  private static void assignmentStatementConsumer(SubscriptionContext ctx) {
    Optional.of(ctx.pythonFile().fileName())
      .filter((settingFiles::contains))
      .ifPresentOrElse(
        fileName -> assignmentStatementCheck(ctx, DebugModeCheck::hasDjangoOrFlaskDebugProperties),
        () -> assignmentStatementCheck(ctx, DebugModeCheck::hasFlaskDebugProperties));
  }

  private static void assignmentStatementCheck(SubscriptionContext ctx, Predicate isDebugProperty) {
    AssignmentStatement assignmentStatementTree = (AssignmentStatement) ctx.syntaxNode();
    for (ExpressionList lhsExpression : assignmentStatementTree.lhsExpressions()) {
      if (isDebugProperty.test(lhsExpression) && isTrue(assignmentStatementTree.assignedValue())) {
        ctx.addIssue(assignmentStatementTree, MESSAGE);
      }
    }
  }

  private static boolean hasDjangoOrFlaskDebugProperties(ExpressionList expressionList) {
    return expressionList.expressions().stream().anyMatch(DebugModeCheck::isDebugIdentifier) || hasFlaskDebugProperties(expressionList);
  }

  private static boolean hasFlaskDebugProperties(ExpressionList expressionList) {
    return expressionList.expressions().stream().anyMatch(DebugModeCheck::isModifyingFlaskDebugProperty);

  }

  private static boolean isModifyingFlaskDebugProperty(Expression expression) {
    if (expression.is(Kind.QUALIFIED_EXPR)) {
      return Optional.of((QualifiedExpression) expression)
        .map(QualifiedExpression::symbol)
        .map(Symbol::fullyQualifiedName)
        .filter(FLASK_APP_DEBUG_FQN::equals)
        .isPresent();
    } else if (expression.is(Kind.SUBSCRIPTION)) {
      SubscriptionExpression subscriptionExpression = (SubscriptionExpression) expression;
      return isFlaskAppConfiguration(subscriptionExpression)
        && isMakingDebugParameterTrue(subscriptionExpression);
    }
    return false;
  }

  private static boolean isMakingDebugParameterTrue(SubscriptionExpression subscriptionExpression) {
    return Optional.of(subscriptionExpression.subscripts())
      .map(ExpressionList::expressions)
      .filter(list -> list.size() == 1)
      .map(list -> list.get(0))
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(StringLiteral.class))
      .map(StringLiteral::trimmedQuotesValue)
      .filter("DEBUG"::equals)
      .isPresent();
  }

  private static boolean isFlaskAppConfiguration(SubscriptionExpression subscriptionExpression) {
    return Optional.of(subscriptionExpression)
      .map(SubscriptionExpression::object)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(QualifiedExpression.class))
      .map(QualifiedExpression::symbol)
      .map(Symbol::fullyQualifiedName)
      .filter(FLASK_APP_CONFIG_FQN::equals)
      .isPresent();
  }

  private static boolean isDebugIdentifier(Expression expr) {
    return expr.is(Kind.NAME) && debugProperties.contains(((Name) expr).name());
  }

  private static boolean isTrue(Expression expr) {
    return Expressions.isTruthy(expr) || TreeUtils.toOptionalInstanceOf(Name.class, expr)
      .map(Expressions::singleAssignedValue)
      .filter(Expressions::isTruthy)
      .isPresent();
  }

  private static boolean isDebugArgument(Argument argument) {
    Name keywordArgument = argument.is(Kind.REGULAR_ARGUMENT) ? ((RegularArgument) argument).keywordArgument() : null;
    if (keywordArgument != null && debugProperties.contains((keywordArgument).name())) {
      return isTrue(((RegularArgument) argument).expression());
    }
    return false;
  }

  @CheckForNull
  private static String getQualifiedName(CallExpression callExpression) {
    Symbol symbol = callExpression.calleeSymbol();
    return symbol != null ? symbol.fullyQualifiedName() : "";
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy