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

org.sonar.python.semantic.SymbolUtils 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 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.python.semantic;

import java.io.File;
import java.net.URI;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
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.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.plugins.python.api.PythonFile;
import org.sonar.plugins.python.api.symbols.AmbiguousSymbol;
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.ArgList;
import org.sonar.plugins.python.api.tree.Argument;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.Expression;
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.ParenthesizedExpression;
import org.sonar.plugins.python.api.tree.RegularArgument;
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.tree.TreeUtils;
import org.sonar.python.types.InferredTypes;
import org.sonar.python.types.TypeShed;

import static org.sonar.plugins.python.api.symbols.Symbol.Kind.CLASS;
import static org.sonar.python.tree.TreeUtils.getSymbolFromTree;

public class SymbolUtils {

  private SymbolUtils() {
  }

  public static String getModuleFileName(String fileName) {
    int extensionIndex = fileName.lastIndexOf('.');
    return extensionIndex > 0
      ? fileName.substring(0, extensionIndex)
      : fileName;
  }

  public static String fullyQualifiedModuleName(String packageName, String fileName) {
    String moduleName = getModuleFileName(fileName);
    if (moduleName.equals("__init__")) {
      return packageName;
    }
    return packageName.isEmpty()
      ? moduleName
      : (packageName + "." + moduleName);
  }

  static void resolveTypeHierarchy(ClassDef classDef, @Nullable Symbol symbol, PythonFile pythonFile, Map symbolsByName) {
    if (symbol == null || !CLASS.equals(symbol.kind())) {
      return;
    }
    ClassSymbolImpl classSymbol = (ClassSymbolImpl) symbol;
    ArgList argList = classDef.args();
    if (argList == null) {
      return;
    }
    for (Argument argument : argList.arguments()) {
      if (!argument.is(Kind.REGULAR_ARGUMENT)) {
        classSymbol.setHasSuperClassWithoutSymbol();
      } else {
        addParentClass(pythonFile, symbolsByName, classSymbol, (RegularArgument) argument);
      }
    }
  }

  private static void addParentClass(PythonFile pythonFile, Map symbolsByName, ClassSymbolImpl classSymbol, RegularArgument regularArgument) {
    Name keyword = regularArgument.keywordArgument();
    if (keyword != null) {
      if (keyword.name().equals("metaclass")) {
        classSymbol.setHasMetaClass();
        getSymbolFromTree(regularArgument.expression())
          .map(Symbol::fullyQualifiedName)
          .ifPresent(classSymbol::setMetaclassFQN);
      }
      return;
    }
    Symbol argumentSymbol = getSymbolFromArgument(regularArgument);
    if (argumentSymbol == null) {
      classSymbol.setHasSuperClassWithoutSymbol();
    } else {
      if ("typing.Generic".equals(argumentSymbol.fullyQualifiedName())) {
        classSymbol.setSupportsGenerics(true);
      }
      classSymbol.addSuperClass(argumentSymbol);
    }
  }

  @CheckForNull
  private static Symbol getSymbolFromArgument(RegularArgument regularArgument) {
    Expression expression = regularArgument.expression();
    while (expression.is(Kind.SUBSCRIPTION)) {
      // to support using 'typing' symbols like 'List[str]'
      expression = ((SubscriptionExpression) expression).object();
    }
    if (expression instanceof HasSymbol hasSymbol) {
      return hasSymbol.symbol();
    }
    return null;
  }

  public static List assignmentsLhs(AssignmentStatement assignmentStatement) {
    return assignmentStatement.lhsExpressions().stream()
      .flatMap(exprList -> exprList.expressions().stream())
      .flatMap(TreeUtils::flattenTuples)
      .toList();
  }

  public static List boundNamesFromExpression(@CheckForNull Tree tree) {
    List names = new ArrayList<>();
    if (tree == null) {
      return names;
    }
    if (tree.is(Tree.Kind.NAME)) {
      names.add(((Name) tree));
    } else if (tree.is(Tree.Kind.TUPLE)) {
      ((Tuple) tree).elements().forEach(t -> names.addAll(boundNamesFromExpression(t)));
    } else if (tree.is(Kind.LIST_LITERAL)) {
      ((ListLiteral) tree).elements().expressions().forEach(t -> names.addAll(boundNamesFromExpression(t)));
    } else if (tree.is(Kind.PARENTHESIZED)) {
      names.addAll(boundNamesFromExpression(((ParenthesizedExpression) tree).expression()));
    } else if (tree.is(Kind.UNPACKING_EXPR)) {
      names.addAll(boundNamesFromExpression(((UnpackingExpression) tree).expression()));
    }
    return names;
  }

  public static String pythonPackageName(File file, String projectBaseDirAbsolutePath) {
    File currentDirectory = file.getParentFile();
    Deque packages = new ArrayDeque<>();
    while (!currentDirectory.getAbsolutePath().equals(projectBaseDirAbsolutePath)) {
      File initFile = new File(currentDirectory, "__init__.py");
      if (!initFile.exists()) {
        break;
      }
      packages.push(currentDirectory.getName());
      currentDirectory = currentDirectory.getParentFile();
    }
    return String.join(".", packages);
  }

  @CheckForNull
  public static Path pathOf(PythonFile pythonFile) {
    try {
      URI uri = pythonFile.uri();
      if ("file".equalsIgnoreCase(uri.getScheme())) {
        return Paths.get(uri);
      }
      return null;
    } catch (InvalidPathException e) {
      return null;
    }
  }

  /**
   * @return the offset between parameter position and argument position:
   *   0 if there is no implicit first parameter (self, cls, etc...)
   *   1 if there is an implicit first parameter
   *  -1 if unknown (intent is not clear from context)
   */
  public static int firstParameterOffset(FunctionSymbol functionSymbol, boolean isStaticCall) {
    List parameters = functionSymbol.parameters();
    if (parameters.isEmpty()) {
      return 0;
    }

    var hasUnknownFirstParameter = parameters.stream()
      .findFirst()
      .filter(p -> Optional.ofNullable(p.declaredType())
        .filter(Predicate.not(InferredTypes.anyType()::equals))
        .isEmpty()
      )
      .filter(p -> p.name() == null)
      .isPresent();

    if (hasUnknownFirstParameter) {
      // First parameter is defined as a tuple
      return -1;
    }
    List decoratorNames = functionSymbol.decorators();
    if (decoratorNames.size() > 1) {
      // We want to avoid FP if there are many decorators
      return -1;
    }
    if (!decoratorNames.isEmpty() && 
        !decoratorNames.get(0).endsWith("classmethod") && 
        !decoratorNames.get(0).endsWith("staticmethod") &&
        !decoratorNames.get(0).endsWith("abstractmethod")) {
      // Unknown decorator which might alter the behaviour of the method
      return -1;
    }
    if (functionSymbol.isInstanceMethod() && !isStaticCall) {
      // regular instance call, takes self as first implicit parameter
      return 1;
    }
    if (decoratorNames.size() == 1 && decoratorNames.get(0).endsWith("classmethod")) {
      // class method call, takes cls as first implicit parameter
      return 1;
    }
    // regular static call (function or method), no first implicit parameter
    return 0;
  }

  public static Optional getOverriddenMethod(FunctionSymbol functionSymbol) {
    return getOverriddenMethod(functionSymbol, symbols -> Optional.of(symbols)
      .filter(s -> s.size() == 1)
      .map(Collection::stream)
      .flatMap(Stream::findFirst)
    );
  }

  public static Optional getOverriddenMethod(FunctionSymbol functionSymbol,
    Function, Optional> symbolPicker) {
    return symbolPicker.apply(getOverriddenMethods(functionSymbol));
  }

  public static List getOverriddenMethods(FunctionSymbol functionSymbol) {
    Symbol owner = ((FunctionSymbolImpl) functionSymbol).owner();
    if (owner == null || owner.kind() != CLASS) {
      return List.of();
    }
    ClassSymbol classSymbol = (ClassSymbol) owner;

    return classSymbol.superClasses()
      .stream()
      .filter(ClassSymbol.class::isInstance)
      .map(ClassSymbol.class::cast)
      .map(c -> c.resolveMember(functionSymbol.name())
        .map(SymbolUtils::getFunctionSymbols)
        .orElseGet(List::of))
      .filter(Predicate.not(List::isEmpty))
      .findFirst()
      .orElseGet(List::of);
  }

  public static List getFunctionSymbols(@Nullable Symbol symbol) {
    if (symbol == null) {
      return List.of();
    }

    if (symbol.is(Symbol.Kind.FUNCTION)) {
      return List.of((FunctionSymbol) symbol);
    }

    if (symbol.is(Symbol.Kind.AMBIGUOUS)) {
      var ambiguousSymbol = (AmbiguousSymbol) symbol;

      var functionSymbols = ambiguousSymbol
        .alternatives()
        .stream()
        .filter(alternative -> alternative.is(Symbol.Kind.FUNCTION))
        .map(FunctionSymbol.class::cast)
        .toList();

      if (functionSymbols.size() != ambiguousSymbol.alternatives().size()) {
        return List.of();
      }

      return functionSymbols;
    }
    return List.of();
  }

  public static Optional getFirstAlternativeIfEqualArgumentNames(List alternatives) {
    return Optional.of(alternatives)
      .filter(SymbolUtils::isEqualParameterCountAndNames)
      .map(Collection::stream)
      .flatMap(Stream::findFirst);
  }

  public static boolean isEqualParameterCountAndNames(List alternatives) {
    return alternatives.stream()
      .map(FunctionSymbol::parameters)
      .filter(Objects::nonNull)
      .map(parameters -> parameters.stream()
        .map(parameter -> List.of(Objects.requireNonNullElse(parameter.name(), ""), parameter.isKeywordOnly(), parameter.isPositionalOnly()))
        .toList()
      ).distinct()
      .count() == 1;
  }


  public static boolean canBeAnOverridingMethod(@Nullable FunctionSymbol functionSymbol) {
    if (functionSymbol == null) return true;
    Symbol owner = ((FunctionSymbolImpl) functionSymbol).owner();
    if (owner == null || owner.kind() != CLASS) return false;
    ClassSymbol classSymbol = (ClassSymbol) owner;
    if (classSymbol.hasUnresolvedTypeHierarchy()) return true;
    for (Symbol superClass : classSymbol.superClasses()) {
      if (superClass.is(CLASS)) {
        boolean canHaveMember = ((ClassSymbol) superClass).canHaveMember(functionSymbol.name());
        if (canHaveMember) return true;
      }
    }
    return false;
  }

  public static Symbol typeshedSymbolWithFQN(String fullyQualifiedName) {
    String[] fqnSplitByDot = fullyQualifiedName.split("\\.");
    String localName = fqnSplitByDot[fqnSplitByDot.length - 1];
    Symbol symbol = TypeShed.symbolWithFQN(fullyQualifiedName);
    return symbol == null ? new SymbolImpl(localName, fullyQualifiedName) : ((SymbolImpl) symbol).copyWithoutUsages();
  }

  public static Set flattenAmbiguousSymbols(Set symbols) {
    Set alternatives = new HashSet<>();
    for (Symbol symbol : symbols) {
      if (symbol.is(Symbol.Kind.AMBIGUOUS)) {
        Set flattenedAlternatives = flattenAmbiguousSymbols(((AmbiguousSymbol) symbol).alternatives());
        alternatives.addAll(flattenedAlternatives);
      } else {
        alternatives.add(symbol);
      }
    }
    return alternatives;
  }

  public static boolean isPrivateName(String name) {
    return name.startsWith("_") && !name.startsWith("__");
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy