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

org.sonar.python.checks.utils.Expressions 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.utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.symbols.Usage;
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.DictionaryLiteral;
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.ListLiteral;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.NumericLiteral;
import org.sonar.plugins.python.api.tree.ParenthesizedExpression;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.SliceExpression;
import org.sonar.plugins.python.api.tree.StringElement;
import org.sonar.plugins.python.api.tree.StringLiteral;
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.Tree.Kind;
import org.sonar.plugins.python.api.tree.Tuple;
import org.sonar.plugins.python.api.tree.UnpackingExpression;
import org.sonar.python.semantic.SymbolUtils;
import org.sonar.python.tree.TreeUtils;

public class Expressions {

  private static final Set ZERO_VALUES = new HashSet<>(Arrays.asList("0", "0.0", "0j"));
  private  static final String TYPE_VAR_FQN = "typing.TypeVar";

  private Expressions() {
  }

  // https://docs.python.org/3/library/stdtypes.html#truth-value-testing
  public static boolean isFalsy(@Nullable Expression expression) {
    if (expression == null) {
      return false;
    }
    return switch (expression.getKind()) {
      case NAME -> "False".equals(((Name) expression).name());
      case NONE -> true;
      case STRING_LITERAL -> unescape((StringLiteral) expression).isEmpty();
      case NUMERIC_LITERAL -> ZERO_VALUES.contains(((NumericLiteral) expression).valueAsString());
      case LIST_LITERAL -> ((ListLiteral) expression).elements().expressions().isEmpty();
      case TUPLE -> ((Tuple) expression).elements().isEmpty();
      case DICTIONARY_LITERAL -> ((DictionaryLiteral) expression).elements().isEmpty();
      default -> false;
    };
  }

  // https://docs.python.org/3/library/stdtypes.html#truth-value-testing
  public static boolean isTruthy(@Nullable Expression expression) {
    if (expression == null) {
      return false;
    }
    return switch (expression.getKind()) {
      case NAME -> "True".equals(((Name) expression).name());
      case STRING_LITERAL, NUMERIC_LITERAL, LIST_LITERAL, TUPLE, SET_LITERAL, DICTIONARY_LITERAL -> !isFalsy(expression);
      default -> false;
    };
  }

  @CheckForNull
  public static Expression singleAssignedValue(Name name) {
    return singleAssignedValue(name, new HashSet<>());
  }

  @CheckForNull
  public static Expression singleAssignedValue(Name name, Set visited) {
    if (visited.contains(name)) {
      return null;
    }
    visited.add(name);
    Symbol symbol = name.symbol();
    if (symbol == null) {
      return null;
    }
    Expression result = null;
    for (Usage usage : symbol.usages()) {
      if (usage.kind() == Usage.Kind.ASSIGNMENT_LHS) {
        if (result != null) {
          return null;
        }
        Tree parent = usage.tree().parent();
        if (parent.is(Kind.EXPRESSION_LIST) &&
          ((ExpressionList) parent).expressions().size() == 1 &&
          parent.parent().is(Kind.ASSIGNMENT_STMT)) {

          result = ((AssignmentStatement) parent.parent()).assignedValue();
        } else {
          return null;
        }
      } else if (usage.isBindingUsage()) {
        return null;
      }
    }
    return result;
  }

  public static Expression removeParentheses(Expression expression) {
    Expression res = expression;
    while (res.is(Kind.PARENTHESIZED)) {
      res = ((ParenthesizedExpression) res).expression();
    }
    return res;
  }

  public static Optional singleAssignedNonNameValue(Name name) {
    Set visited = new HashSet<>();
    Expression result = singleAssignedValue(name, visited);
    while (result != null && result.is(Kind.NAME)) {
      result = singleAssignedValue((Name) result, visited);
    }
    return Optional.ofNullable(result);
  }

  public static Optional ifNameGetSingleAssignedNonNameValue(Expression expression) {
    if (expression.is(Tree.Kind.NAME)) {
      return Expressions.singleAssignedNonNameValue((Name) expression);
    }
    return Optional.of(expression);
  }

  public static List expressionsFromListOrTuple(Expression expression) {
    return TreeUtils.toOptionalInstanceOf(ListLiteral.class, expression)
      .map(ListLiteral::elements)
      .map(ExpressionList::expressions)
      .or(() -> TreeUtils.toOptionalInstanceOf(Tuple.class, expression)
        .map(Tuple::elements))
      .orElseGet(Collections::emptyList);
  }

  /**
   * @return concatenation of all underlying StringElement text values without quotes and with escape sequences replacement.
   * @see 2.4.1. String and Bytes literals
   */
  public static String unescape(StringLiteral stringLiteral) {
    List elements = stringLiteral.stringElements();
    if (elements.size() == 1) {
      return unescape(elements.get(0));
    }
    return elements.stream()
      .map(Expressions::unescape)
      .collect(Collectors.joining());
  }

  /**
   * @return the string content of the given StringElement text values without quotes and with escape sequences replacement.
   * @see 2.4.1. String and Bytes literals
   */
  public static String unescape(StringElement stringElement) {
    String lowerCasePrefix = stringElement.prefix().toLowerCase(Locale.ROOT);
    String valueWithoutQuotes = stringElement.trimmedQuotesValue();
    boolean isEscaped = lowerCasePrefix.indexOf('r') == -1;
    boolean isBytesLiteral = lowerCasePrefix.indexOf('b') != -1;
    if (isEscaped) {
      valueWithoutQuotes = unescapeString(valueWithoutQuotes, isBytesLiteral);
    }
    return valueWithoutQuotes;
  }

  /**
   * @param value to unescape according to python string and bytes literals conventions
   * @param isBytesLiteral knowing if it's a string, or an array of bytes is important because
   *                       python string uses 16 bits characters and support backslash u and U escape sequences
   *                       '\u0061' == 'a'
   *                       python bytes array uses 8 bits values and does not support and unescape backslash u and U escape sequences
   *                       b'\u0061' != b'a'
   *                       b'\u0061' == b'\\u0061'
   * @return unescaped value
   */
  // Visible for testing
  public static String unescapeString(String value, boolean isBytesLiteral) {
    if (value.indexOf('\\') == -1) {
      return value;
    }
    int length = value.length();
    StringBuilder sb = new StringBuilder(length);
    int i = 0;
    while (i < length) {
      char ch = value.charAt(i);
      if (ch != '\\') {
        sb.append(ch);
        i++;
      } else {
        EscapeSequence escapeSequence = EscapeSequence.extract(value, i, isBytesLiteral);
        sb.append(escapeSequence.unescapedValue);
        i += escapeSequence.escapedLength;
      }
    }
    return sb.toString();
  }

  private static class EscapeSequence {

    private static final int HEXADECIMAL_RADIX = 16;

    private static final EscapeSequence IGNORE = new EscapeSequence(1, "\\");

    private static final char[] UNESCAPED_CHAR = new char['v' + 1];
    static {
      UNESCAPED_CHAR['\\'] = '\\';
      UNESCAPED_CHAR['\''] = '\'';
      UNESCAPED_CHAR['\"'] = '\"';
      UNESCAPED_CHAR['a'] = '\u0007';
      UNESCAPED_CHAR['b'] = '\b';
      UNESCAPED_CHAR['f'] = '\f';
      UNESCAPED_CHAR['n'] = '\n';
      UNESCAPED_CHAR['r'] = '\r';
      UNESCAPED_CHAR['t'] = '\t';
      UNESCAPED_CHAR['v'] = '\u000b';
    }

    private final int escapedLength;
    private final String unescapedValue;

    private EscapeSequence(int escapedLength, String unescapedValue) {
      this.escapedLength = escapedLength;
      this.unescapedValue = unescapedValue;
    }

    private static EscapeSequence extract(String value, int i, boolean isBytesLiteral) {
      if (i == value.length() - 1) {
        return IGNORE;
      }
      char nextChar = value.charAt(i + 1);
      char unescaped = nextChar < UNESCAPED_CHAR.length ? UNESCAPED_CHAR[nextChar] : '\0';
      if (unescaped != '\0') {
        return new EscapeSequence(2, String.valueOf(unescaped));
      } else if (nextChar == '\n') {
        // ignored line break (linux end of line)
        return new EscapeSequence(2, "");
      } else if (nextChar == '\r') {
        // ignored line break (windows and mac end of line)
        return new EscapeSequence(i + 2 < value.length() && value.charAt(i + 2) == '\n' ? 3 : 2, "");
      } else if (nextChar == 'x') {
        return extractHexadecimal(value, i, 2);
      } else if (nextChar == 'u' && !isBytesLiteral) {
        return extractHexadecimal(value, i, 4);
      } else if (nextChar == 'U' && !isBytesLiteral) {
        return extractHexadecimal(value, i, 8);
      } else if (nextChar == 'N') {
        // escape sequence by unicode name is not supported, require java 9 to benefit from Character.codePointOf
        return IGNORE;
      } else {
        return extractOctal(value, i);
      }
    }

    private static EscapeSequence extractHexadecimal(String value, int i, int length) {
      if (i + 1 + length < value.length()) {
        try {
          int hexValue = Integer.parseInt(value.substring(i + 2, i + 2 + length), HEXADECIMAL_RADIX);
          return new EscapeSequence(2 + length, String.valueOf((char) hexValue));
        } catch (NumberFormatException ex) {
          return IGNORE;
        }
      } else {
        return IGNORE;
      }
    }

    private static EscapeSequence extractOctal(String value, int i) {
      // octal
      int octal = 0;
      int octalStart = (value.charAt(i + 1) == 'o') ? (i + 2) : (i + 1);
      int len = 0;
      int j = octalStart;
      while (len < 3 && j < value.length() && value.charAt(j) >= '0' && value.charAt(j) <= '7') {
        octal = octal * 8 + (value.charAt(j) - '0');
        j++;
        len++;
      }
      if (len > 0) {
        return new EscapeSequence(j - i, String.valueOf((char) octal));
      } else {
        return IGNORE;
      }
    }
  }

  public static Optional getAssignedName(Expression expression) {
    return getAssignedName(expression, 0);
  }

  private static Optional getAssignedName(Expression expression, int recursionCount) {
    if(recursionCount > 4){
      return Optional.empty();
    }
    if (expression.is(Tree.Kind.NAME)) {
      return Optional.of((Name) expression);
    }
    if (expression.is(Tree.Kind.QUALIFIED_EXPR)) {
      return Optional.of(((QualifiedExpression) expression).name());
    }
    if(expression.is(Tree.Kind.SUBSCRIPTION)){
      expression = ((SubscriptionExpression) expression).object();
    } 
    if(expression.is(Tree.Kind.SLICE_EXPR)){
      expression = ((SliceExpression) expression).object();
    }

    var maybeAssignment = TreeUtils.firstAncestorOfKind(expression, Tree.Kind.ASSIGNMENT_STMT);
    if (maybeAssignment == null) {
      return Optional.empty();
    }
    
    var assignment = (AssignmentStatement) maybeAssignment;
    var expressions = SymbolUtils.assignmentsLhs(assignment);

    if (expressions.size() != 1 ) {
      List rhsExpressions = getExpressionsFromRhs(assignment.assignedValue());
      var rhsIndex = rhsExpressions.stream().flatMap(TreeUtils::flattenTuples).toList().indexOf(expression);
      if (rhsIndex != -1 && rhsIndex < expressions.size()) {
        return getAssignedName(expressions.get(rhsIndex), recursionCount + 1);
      } else {
        return Optional.empty();
      }
    }
    return getAssignedName(expressions.get(0), recursionCount + 1);
  }

  public static List getExpressionsFromRhs(Expression rhs) {
    List expressions = new ArrayList<>();
    if (rhs.is(Tree.Kind.TUPLE)) {
      expressions.addAll(((Tuple) rhs).elements());
    } else if (rhs.is(Tree.Kind.LIST_LITERAL)) {
      expressions.addAll(((ListLiteral) rhs).elements().expressions());
    } else if (rhs.is(Tree.Kind.UNPACKING_EXPR)) {
      return getExpressionsFromRhs(((UnpackingExpression) rhs).expression());
    }
    return expressions;
  }

  public static boolean isGenericTypeAnnotation(Expression expression) {
    return Optional.of(expression)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(Name.class))
      .map(Expressions::singleAssignedValue)
      .flatMap(TreeUtils.toOptionalInstanceOfMapper(CallExpression.class))
      .map(CallExpression::callee)
      .filter(HasSymbol.class::isInstance)
      .map(HasSymbol.class::cast)
      .map(HasSymbol::symbol)
      .map(Symbol::fullyQualifiedName)
      .filter(TYPE_VAR_FQN::equals)
      .isPresent();
  }

  public static boolean containsSpreadOperator(List arguments) {
    return arguments.stream().anyMatch(UnpackingExpression.class::isInstance);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy