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

org.sonar.python.tree.TreeUtils 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.tree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.plugins.python.api.LocationInFile;
import org.sonar.plugins.python.api.symbols.ClassSymbol;
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.tree.AnyParameter;
import org.sonar.plugins.python.api.tree.Argument;
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.Decorator;
import org.sonar.plugins.python.api.tree.DottedName;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.Parameter;
import org.sonar.plugins.python.api.tree.ParameterList;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.RegularArgument;
import org.sonar.plugins.python.api.tree.Statement;
import org.sonar.plugins.python.api.tree.Token;
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.python.TokenLocation;
import org.sonar.python.api.PythonTokenType;

public class TreeUtils {
  private TreeUtils() {
    // empty constructor
  }

  private static final Set WHITESPACE_TOKEN_TYPES = EnumSet.of(
    PythonTokenType.NEWLINE,
    PythonTokenType.INDENT,
    PythonTokenType.DEDENT);

  @CheckForNull
  public static Tree firstAncestor(Tree tree, Predicate predicate) {
    Tree currentParent = tree.parent();
    while (currentParent != null) {
      if (predicate.test(currentParent)) {
        return currentParent;
      }
      currentParent = currentParent.parent();
    }
    return null;
  }

  @CheckForNull
  public static Tree firstAncestorOfKind(Tree tree, Kind... kinds) {
    return firstAncestor(tree, t -> t.is(kinds));
  }

  public static Collector> groupAssignmentByParentStatementList() {
    return Collectors.toMap(tree -> TreeUtils.firstAncestor(tree, parent -> parent.is(Tree.Kind.STATEMENT_LIST)),
      Function.identity(),
      //Get just first element for each block
      (t1, t2) ->
        Stream.of(t1, t2).min(getTreeByPositionComparator()).get());
  }

  public static Comparator getTreeByPositionComparator() {
    return Comparator.comparing((Tree t) -> t.firstToken().pythonLine().line()).thenComparing((Tree t) -> t.firstToken().pythonColumn());
  }

  public static List tokens(Tree tree) {
    if (tree.is(Kind.TOKEN)) {
      return Collections.singletonList((Token) tree);
    } 
    List tokens = new ArrayList<>();
    for (Tree child : tree.children()) {
      if (child.is(Kind.TOKEN)) {
        tokens.add(((Token) child));
      } else {
        tokens.addAll(tokens(child));
      }
    }
    return tokens;
  }

  public static List nonWhitespaceTokens(Tree tree) {
    return TreeUtils.tokens(tree).stream()
      .filter(t -> !WHITESPACE_TOKEN_TYPES.contains(t.type()))
      .toList();
  }

  public static boolean hasDescendant(Tree tree, Predicate predicate) {
    return tree.children().stream().anyMatch(child -> predicate.test(child) || hasDescendant(child, predicate));
  }

  public static Stream flattenTuples(Expression expression) {
    if (expression.is(Kind.TUPLE)) {
      Tuple tuple = (Tuple) expression;
      return tuple.elements().stream().flatMap(TreeUtils::flattenTuples);
    } else {
      return Stream.of(expression);
    }
  }

  public static Optional getSymbolFromTree(@Nullable Tree tree) {
    if (tree instanceof HasSymbol hasSymbol) {
      return Optional.ofNullable(hasSymbol.symbol());
    }
    return Optional.empty();
  }

  @CheckForNull
  public static ClassSymbol getClassSymbolFromDef(@Nullable ClassDef classDef) {
    if (classDef == null) {
      return null;
    }

    Symbol classNameSymbol = classDef.name().symbol();
    if (classNameSymbol == null) {
      throw new IllegalStateException("A ClassDef should always have a non-null symbol!");
    }

    if (classNameSymbol.kind() == Symbol.Kind.CLASS) {
      return ((ClassSymbol) classNameSymbol);
    }

    return null;
  }

  public static List getParentClassesFQN(ClassDef classDef) {
    return getParentClasses(TreeUtils.getClassSymbolFromDef(classDef), new HashSet<>()).stream()
      .map(Symbol::fullyQualifiedName)
      .filter(Objects::nonNull)
      .toList();
  }

  private static List getParentClasses(@Nullable ClassSymbol classSymbol, Set visitedSymbols) {
    List superClasses = new ArrayList<>();
    if (classSymbol == null || visitedSymbols.contains(classSymbol)) {
      return superClasses;
    }
    visitedSymbols.add(classSymbol);
    for (Symbol symbol : classSymbol.superClasses()) {
      superClasses.add(symbol);
      if (symbol instanceof ClassSymbol superClassSymbol) {
        superClasses.addAll(getParentClasses(superClassSymbol, visitedSymbols));
      }
    }
    return superClasses;
  }

  @CheckForNull
  public static FunctionSymbol getFunctionSymbolFromDef(@Nullable FunctionDef functionDef) {
    if (functionDef == null) {
      return null;
    }

    Symbol functionNameSymbol = functionDef.name().symbol();
    if (functionNameSymbol == null) {
      throw new IllegalStateException("A FunctionDef should always have a non-null symbol!");
    }

    if (functionNameSymbol.kind() == Symbol.Kind.FUNCTION) {
      return ((FunctionSymbol) functionNameSymbol);
    }

    return null;
  }

  public static List nonTupleParameters(FunctionDef functionDef) {
    ParameterList parameterList = functionDef.parameters();
    if (parameterList == null) {
      return Collections.emptyList();
    }
    return parameterList.nonTuple();
  }

  public static List positionalParameters(FunctionDef functionDef) {
    ParameterList parameterList = functionDef.parameters();
    if (parameterList == null) {
      return Collections.emptyList();
    }

    List result = new ArrayList<>();
    for (AnyParameter anyParameter : parameterList.all()) {
      if (anyParameter instanceof Parameter parameter) {
        Token starToken = parameter.starToken();
        if (parameter.name() == null && starToken != null) {
          if ("*".equals(starToken.value())) {
            return result;
          }
          // Ignore the possible '/' parameter
        } else {
          result.add(parameter);
        }
      }
    }

    return result;
  }

  /**
   * Collects all top-level function definitions within a class def.
   * It is used to discover methods defined within "strange" constructs, such as
   * 
   *   class A:
   *       if p:
   *           def f(self): ...
   * 
   */
  public static List topLevelFunctionDefs(ClassDef classDef) {
    CollectFunctionDefsVisitor visitor = new CollectFunctionDefsVisitor();
    classDef.body().accept(visitor);

    return visitor.functionDefs;
  }

  public static int findIndentationSize(Tree tree) {
    var parent = tree.parent();

    if (parent == null) {
      return findIndentDownTree(tree);
    }

    var treeToken = tree.firstToken();
    var parentToken = parent.firstToken();

    if (treeToken.pythonLine().line() != parentToken.pythonLine().line()) {
      return treeToken.pythonColumn() - parentToken.pythonColumn();
    } else {
      return findIndentationSize(parent);
    }
  }

  private static int findIndentDownTree(Tree parent) {
    var parentToken = parent.firstToken();
    return parent.children()
      .stream()
      .map(child -> {

        var childToken = child.firstToken();
        if (childToken.pythonLine().line() > parentToken.pythonLine().line() && childToken.pythonColumn() > parentToken.pythonColumn()) {
          return childToken.pythonColumn() - parentToken.pythonColumn();
        } else  {
          return findIndentDownTree(child);
        }
      })
      .filter(i -> i > 0)
      .findFirst()
      .orElse(0);
  }

  private static class CollectFunctionDefsVisitor extends BaseTreeVisitor {
    private List functionDefs = new ArrayList<>();

    @Override
    public void visitClassDef(ClassDef pyClassDefTree) {
      // Do not descend into nested classes
    }

    @Override
    public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
      this.functionDefs.add(pyFunctionDefTree);
      // Do not descend into nested functions
    }
  }

  @CheckForNull
  public static RegularArgument argumentByKeyword(String keyword, List arguments) {
    for (int i = 0; i < arguments.size(); i++) {
      Argument argument = arguments.get(i);
      if (hasKeyword(argument, keyword)) {
        return ((RegularArgument) argument);
      }
    }
    return null;
  }

  @CheckForNull
  public static RegularArgument nthArgumentOrKeyword(int argPosition, String keyword, List arguments) {
    for (int i = 0; i < arguments.size(); i++) {
      Argument argument = arguments.get(i);
      if (hasKeyword(argument, keyword)) {
        return ((RegularArgument) argument);
      }
      if (argument.is(Kind.REGULAR_ARGUMENT)) {
        RegularArgument regularArgument = (RegularArgument) argument;
        if (regularArgument.keywordArgument() == null && argPosition == i) {
          return regularArgument;
        }
      }
    }
    return null;
  }

  private static boolean hasKeyword(Argument argument, String keyword) {
    if (argument.is(Kind.REGULAR_ARGUMENT)) {
      Name keywordArgument = ((RegularArgument) argument).keywordArgument();
      return keywordArgument != null && keywordArgument.name().equals(keyword);
    }
    return false;
  }

  public static boolean isBooleanLiteral(Tree tree) {
    if (tree.is(Kind.NAME)) {
      String name = ((Name) tree).name();
      return name.equals("True") || name.equals("False");
    }
    return false;
  }

  public static String nameFromExpression(Expression expression) {
    if (expression.is(Tree.Kind.NAME)) {
      return ((Name) expression).name();
    }
    return null;
  }

  public static Optional nameFromQualifiedOrCallExpression(Expression expression) {
    return Optional.ofNullable(TreeUtils.nameFromExpression(expression))
      .or(() -> TreeUtils.toOptionalInstanceOf(QualifiedExpression.class, expression)
        .map(TreeUtils::nameFromQualifiedExpression))
      .or(() -> TreeUtils.toOptionalInstanceOf(CallExpression.class, expression)
        .map(CallExpression::callee)
        .flatMap(TreeUtils::nameFromExpressionOrQualifiedExpression));
  }

  public static Optional nameFromExpressionOrQualifiedExpression(Expression expression) {
    return TreeUtils.toOptionalInstanceOf(QualifiedExpression.class, expression)
      .map(TreeUtils::nameFromQualifiedExpression)
      .or(() -> Optional.ofNullable(TreeUtils.nameFromExpression(expression)));
  }

  public static String nameFromQualifiedExpression(QualifiedExpression qualifiedExpression) {
    String exprName = qualifiedExpression.name().name();
    Expression qualifier = qualifiedExpression.qualifier();
    String nameOfQualifier = decoratorNameFromExpression(qualifier);
    if (nameOfQualifier != null) {
      exprName = nameOfQualifier + "." + exprName;
    } else {
      exprName = null;
    }
    return exprName;
  }

  @CheckForNull
  public static String decoratorNameFromExpression(Expression expression) {
    if (expression.is(Kind.NAME)) {
      return ((Name) expression).name();
    }
    if (expression.is(Kind.QUALIFIED_EXPR)) {
      return nameFromQualifiedExpression((QualifiedExpression) expression);
    }
    if (expression.is(Kind.CALL_EXPR)) {
      return decoratorNameFromExpression(((CallExpression) expression).callee());
    }
    return null;
  }


  public static boolean isFunctionWithGivenDecoratorFQN(Tree tree, String decoratorFQN) {
    if (!tree.is(Kind.FUNCDEF)) {
      return false;
    }
    return ((FunctionDef) tree).decorators().stream().anyMatch(d -> isDecoratorWithFQN(d, decoratorFQN));
  }

  public static boolean isDecoratorWithFQN(Decorator decorator, String fullyQualifiedName) {
    return Optional.of(decorator.expression())
      .flatMap(TreeUtils::getSymbolFromTree)
      .map(Symbol::fullyQualifiedName)
      .filter(fullyQualifiedName::equals)
      .isPresent();
  }

  public static Optional fullyQualifiedNameFromQualifiedExpression(QualifiedExpression qualifiedExpression) {
    String exprName = qualifiedExpression.name().name();
    Expression qualifier = qualifiedExpression.qualifier();
    return fullyQualifiedNameFromExpression(qualifier).map(nameOfQualifier -> nameOfQualifier + "." + exprName);
  }

  public static Optional fullyQualifiedNameFromExpression(Expression expression) {
    if (expression.is(Kind.NAME)) {
      Symbol symbol = ((Name) expression).symbol();
      return Optional.of(Optional.ofNullable(symbol).map(Symbol::fullyQualifiedName).orElse(((Name) expression).name()));
    }
    if (expression.is(Kind.QUALIFIED_EXPR)) {
      return fullyQualifiedNameFromQualifiedExpression((QualifiedExpression) expression);
    }
    if (expression.is(Kind.CALL_EXPR)) {
      return fullyQualifiedNameFromExpression(((CallExpression) expression).callee());
    }
    return Optional.empty();
  }

  @CheckForNull
  public static LocationInFile locationInFile(Tree tree, @Nullable String fileId) {
    if (fileId == null) {
      return null;
    }
    TokenLocation firstToken = new TokenLocation(tree.firstToken());
    TokenLocation lastToken = new TokenLocation(tree.lastToken());
    return new LocationInFile(fileId, firstToken.startLine(), firstToken.startLineOffset(), lastToken.endLine(), lastToken.endLineOffset());
  }

  /**
   * Statements can have a separator like semicolon. When handling ranges we want to take them into account.
   */
  public static Token getTreeSeparatorOrLastToken(Tree tree) {
    if (tree instanceof Statement statement) {
      Token separator = statement.separator();
      if (separator != null) {
        return separator;
      }
    }
    return tree.lastToken();
  }

  public static  Function toInstanceOfMapper(Class castToClass) {
    return toOptionalInstanceOfMapper(castToClass).andThen(t -> t.orElse(null));
  }

  public static  Function> toOptionalInstanceOfMapper(Class castToClass) {
    return tree -> toOptionalInstanceOf(castToClass, tree);
  }

  public static  Optional toOptionalInstanceOf(Class castToClass, @Nullable Tree tree) {
    return Optional.ofNullable(tree).filter(castToClass::isInstance).map(castToClass::cast);
  }

  public static  Function> toStreamInstanceOfMapper(Class castToClass) {
    return tree -> toOptionalInstanceOf(castToClass, tree).map(Stream::of).orElse(Stream.empty());
  }

  public static Optional firstChild(Tree tree, Predicate filter) {
    if (filter.test(tree)) {
      return Optional.of(tree);
    }
    return tree.children()
      .stream()
      .map(c -> firstChild(c, filter))
      .filter(Optional::isPresent)
      .findFirst()
      .map(Optional::get);
  }

  public static String treeToString(Tree tree, boolean renderMultiline) {
    if (!renderMultiline) {
      var firstLine = tree.firstToken().pythonLine().line();
      var lastLine = tree.lastToken().pythonLine().line();

      // We decided to not support multiline default parameters
      // because it requires indents calculation for place where the value should be copied.
      if (firstLine != lastLine) {
        return null;
      }
    }

    var tokens = TreeUtils.tokens(tree);

    var valueBuilder = new StringBuilder();
    for (int i = 0; i < tokens.size(); i++) {
      var token = tokens.get(i);
      if (i > 0) {
        var previous = tokens.get(i - 1);
        var spaceBetween = token.column() - previous.column() - previous.value().length();
        if (spaceBetween < 0) {
          spaceBetween = token.column();
        }
        valueBuilder.append(" ".repeat(spaceBetween));
      }
      valueBuilder.append(token.value());
    }
    return valueBuilder.toString();
  }


  public static List dottedNameToPartFqn(DottedName dottedName) {
    return dottedName.names()
      .stream()
      .map(Name::name)
      .toList();
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy