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

org.sonar.python.semantic.SymbolTableBuilder 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.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.symbols.Usage;
import org.sonar.plugins.python.api.tree.AliasedName;
import org.sonar.plugins.python.api.tree.AnnotatedAssignment;
import org.sonar.plugins.python.api.tree.AnyParameter;
import org.sonar.plugins.python.api.tree.AssignmentExpression;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
import org.sonar.plugins.python.api.tree.CapturePattern;
import org.sonar.plugins.python.api.tree.ClassDef;
import org.sonar.plugins.python.api.tree.CompoundAssignmentStatement;
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
import org.sonar.plugins.python.api.tree.ComprehensionFor;
import org.sonar.plugins.python.api.tree.Decorator;
import org.sonar.plugins.python.api.tree.DictCompExpression;
import org.sonar.plugins.python.api.tree.DottedName;
import org.sonar.plugins.python.api.tree.ExceptClause;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.FileInput;
import org.sonar.plugins.python.api.tree.ForStatement;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.FunctionLike;
import org.sonar.plugins.python.api.tree.GlobalStatement;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.ImportFrom;
import org.sonar.plugins.python.api.tree.ImportName;
import org.sonar.plugins.python.api.tree.LambdaExpression;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.NonlocalStatement;
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.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.TupleParameter;
import org.sonar.plugins.python.api.tree.TypeAliasStatement;
import org.sonar.plugins.python.api.tree.TypeAnnotation;
import org.sonar.plugins.python.api.tree.TypeParams;
import org.sonar.plugins.python.api.tree.WithItem;
import org.sonar.python.tree.ClassDefImpl;
import org.sonar.python.tree.ComprehensionExpressionImpl;
import org.sonar.python.tree.DictCompExpressionImpl;
import org.sonar.python.tree.FileInputImpl;
import org.sonar.python.tree.FunctionDefImpl;
import org.sonar.python.tree.ImportFromImpl;
import org.sonar.python.tree.LambdaExpressionImpl;
import org.sonar.python.tree.TreeUtils;
import org.sonar.python.types.InferredTypes;
import org.sonar.python.types.TypeInference;
import org.sonar.python.types.TypeShed;

import static org.sonar.python.semantic.SymbolUtils.boundNamesFromExpression;
import static org.sonar.python.semantic.SymbolUtils.resolveTypeHierarchy;

// SymbolTable based on https://docs.python.org/3/reference/executionmodel.html#naming-and-binding
public class SymbolTableBuilder extends BaseTreeVisitor {
  private final String fullyQualifiedModuleName;
  private final List filePath;
  private final ProjectLevelSymbolTable projectLevelSymbolTable;
  private Map scopesByRootTree;
  private FileInput fileInput = null;
  private final Set assignmentLeftHandSides = new HashSet<>();
  private final PythonFile pythonFile;
  private final Set importedModulesFQN = new HashSet<>();

  public SymbolTableBuilder(PythonFile pythonFile) {
    fullyQualifiedModuleName = null;
    filePath = null;
    projectLevelSymbolTable = ProjectLevelSymbolTable.empty();
    this.pythonFile = pythonFile;
  }

  public Set importedModulesFQN() {
    return Collections.unmodifiableSet(importedModulesFQN);
  }

  public SymbolTableBuilder(String packageName, PythonFile pythonFile) {
    this(packageName, pythonFile, ProjectLevelSymbolTable.empty());
  }

  public SymbolTableBuilder(String packageName, PythonFile pythonFile, ProjectLevelSymbolTable projectLevelSymbolTable) {
    this.pythonFile = pythonFile;
    String fileName = pythonFile.fileName();
    fullyQualifiedModuleName = SymbolUtils.fullyQualifiedModuleName(packageName, fileName);
    filePath = new ArrayList<>(Arrays.asList(fullyQualifiedModuleName.split("\\.")));
    if (SymbolUtils.getModuleFileName(fileName).equals("__init__")) {
      filePath.add("");
    }
    this.projectLevelSymbolTable = projectLevelSymbolTable;
  }

  @Override
  public void visitFileInput(FileInput fileInput) {
    this.fileInput = fileInput;
    scopesByRootTree = new HashMap<>();
    fileInput.accept(new FirstPhaseVisitor());
    fileInput.accept(new SecondPhaseVisitor());
    createAmbiguousSymbols();
    addSymbolsToTree((FileInputImpl) fileInput);
    fileInput.accept(new ThirdPhaseVisitor());
    TypeInference.inferTypes(fileInput, pythonFile);
  }

  private static class SymbolToUpdate {
    final Symbol symbol;
    final AmbiguousSymbol ambiguousSymbol;

    SymbolToUpdate(Symbol symbol, AmbiguousSymbol ambiguousSymbol) {
      this.symbol = symbol;
      this.ambiguousSymbol = ambiguousSymbol;
    }
  }

  private void createAmbiguousSymbols() {
    for (Scope scope : scopesByRootTree.values()) {
      Set symbolsToUpdate = new HashSet<>();
      for (Symbol symbol : scope.symbols()) {
        if (symbol.kind() == Symbol.Kind.OTHER) {
          List bindingUsages = symbol.usages().stream().filter(Usage::isBindingUsage).toList();
          if (bindingUsages.size() > 1 &&
            bindingUsages.stream().anyMatch(usage -> usage.kind() == Usage.Kind.FUNC_DECLARATION || usage.kind() == Usage.Kind.CLASS_DECLARATION)) {

            Set alternativeDefinitions = getAlternativeDefinitions(symbol, bindingUsages);
            AmbiguousSymbol ambiguousSymbol = AmbiguousSymbolImpl.create(alternativeDefinitions);
            // update symbol and usage to newly created ambiguous symbol
            symbol.usages().forEach(usage -> ((SymbolImpl) ambiguousSymbol).addUsage(usage.tree(), usage.kind()));
            symbolsToUpdate.add(new SymbolToUpdate(symbol, ambiguousSymbol));
          }
        }
      }
      symbolsToUpdate.forEach(symbolToUpdate -> scope.replaceSymbolWithAmbiguousSymbol(symbolToUpdate.symbol, symbolToUpdate.ambiguousSymbol));
    }
  }

  private Set getAlternativeDefinitions(Symbol symbol, List bindingUsages) {
    Set alternativeDefinitions = new HashSet<>();
    for (Usage bindingUsage : bindingUsages) {
      switch (bindingUsage.kind()) {
        case FUNC_DECLARATION:
          FunctionDef functionDef = (FunctionDef) bindingUsage.tree().parent();
          FunctionSymbolImpl functionSymbol = new FunctionSymbolImpl(functionDef, symbol.fullyQualifiedName(), pythonFile);
          ((FunctionDefImpl) functionDef).setFunctionSymbol(functionSymbol);
          alternativeDefinitions.add(functionSymbol);
          break;
        case CLASS_DECLARATION:
          ClassDef classDef = (ClassDef) bindingUsage.tree().parent();
          ClassSymbolImpl classSymbol = new ClassSymbolImpl(classDef, symbol.fullyQualifiedName(), pythonFile);
          resolveTypeHierarchy(classDef, classSymbol, pythonFile, scopesByRootTree.get(fileInput).symbolsByName);
          Scope classScope = scopesByRootTree.get(classDef);
          classSymbol.addMembers(getClassMembers(classScope.symbolsByName, classScope.instanceAttributesByName));
          alternativeDefinitions.add(classSymbol);
          break;
        default:
          SymbolImpl alternativeSymbol = new SymbolImpl(symbol.name(), symbol.fullyQualifiedName());
          alternativeDefinitions.add(alternativeSymbol);
      }
    }
    return alternativeDefinitions;
  }

  private void addSymbolsToTree(FileInputImpl fileInput) {
    for (Scope scope : scopesByRootTree.values()) {
      if (scope.rootTree instanceof FunctionLike funcDef) {
        for (Symbol symbol : scope.symbols()) {
          if (funcDef.is(Kind.LAMBDA)) {
            ((LambdaExpressionImpl) funcDef).addLocalVariableSymbol(symbol);
          } else {
            ((FunctionDefImpl) funcDef).addLocalVariableSymbol(symbol);
          }
        }
      } else if (scope.rootTree.is(Kind.CLASSDEF)) {
        ClassDefImpl classDef = (ClassDefImpl) scope.rootTree;
        scope.symbols().forEach(classDef::addClassField);
        scope.instanceAttributesByName.values().forEach(classDef::addInstanceField);
        Symbol classSymbol = classDef.name().symbol();
        Optional.ofNullable(classSymbol)
          .filter(symbol -> symbol.kind() == Symbol.Kind.CLASS)
          .map(ClassSymbolImpl.class::cast)
          .ifPresent(symbol -> symbol.addMembers(getClassMembers(scope.symbolsByName, scope.instanceAttributesByName)));

      } else if (scope.rootTree.is(Kind.FILE_INPUT)) {
        scope.symbols().stream().filter(s -> !scope.builtinSymbols.contains(s)).forEach(fileInput::addGlobalVariables);
      } else if (scope.rootTree.is(Kind.DICT_COMPREHENSION)) {
        scope.symbols().forEach(((DictCompExpressionImpl) scope.rootTree)::addLocalVariableSymbol);
      } else if (scope.rootTree instanceof ComprehensionExpression) {
        scope.symbols().forEach(((ComprehensionExpressionImpl) scope.rootTree)::addLocalVariableSymbol);
      }
    }
  }

  private static Set getClassMembers(Map symbolsInClass, Map instanceAttributesByName) {
    Set members = new HashSet<>(symbolsInClass.values());
    for (SymbolImpl instanceAttribute : instanceAttributesByName.values()) {
      SymbolImpl member = (SymbolImpl) symbolsInClass.get(instanceAttribute.name());
      if (member != null) {
        for (Usage usage : instanceAttribute.usages()) {
          if (usage.isBindingUsage()) {
            // TODO: should be an Ambiguous Symbol
            member.setKind(Symbol.Kind.OTHER);
          }
          member.addUsage(usage.tree(), usage.kind());
        }
      } else {
        members.add(instanceAttribute);
      }
    }
    return members;
  }

  private class ScopeVisitor extends BaseTreeVisitor {

    private final Deque scopeRootTrees = new LinkedList<>();
    protected Scope moduleScope;

    Tree currentScopeRootTree() {
      return scopeRootTrees.peek();
    }

    void enterScope(Tree tree) {
      scopeRootTrees.push(tree);
    }

    Tree leaveScope() {
      return scopeRootTrees.pop();
    }

    Scope currentScope() {
      return scopesByRootTree.get(currentScopeRootTree());
    }
  }

  private class FirstPhaseVisitor extends ScopeVisitor {

    @Override
    public void visitFileInput(FileInput tree) {
      createScope(tree, null);
      enterScope(tree);
      moduleScope = currentScope();
      Map typeShedSymbols = TypeShed.builtinSymbols();
      for (String name : BuiltinSymbols.all()) {
        currentScope().createBuiltinSymbol(name, typeShedSymbols);
      }
      super.visitFileInput(tree);
    }

    @Override
    public void visitLambda(LambdaExpression pyLambdaExpressionTree) {
      createScope(pyLambdaExpressionTree, currentScope());
      enterScope(pyLambdaExpressionTree);
      createParameters(pyLambdaExpressionTree);
      super.visitLambda(pyLambdaExpressionTree);
      leaveScope();
    }

    @Override
    public void visitDictCompExpression(DictCompExpression tree) {
      createScope(tree, currentScope());
      enterScope(tree);
      super.visitDictCompExpression(tree);
      leaveScope();
    }

    /**
     * The scope of the decorator should be the parent scope of the function or class to which the decorator is assigned.
     * So we have to leave the function or class scope, visit the decorator and enter the previous scope.
     * See SONARPY-990
     */
    @Override
    public void visitDecorator(Decorator tree) {
      leaveScope();
      super.visitDecorator(tree);
      enterScope(tree.parent());
    }

    @Override
    public void visitPyListOrSetCompExpression(ComprehensionExpression tree) {
      createScope(tree, currentScope());
      enterScope(tree);
      super.visitPyListOrSetCompExpression(tree);
      leaveScope();
    }

    @Override
    public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
      String functionName = pyFunctionDefTree.name().name();
      String fullyQualifiedName = getFullyQualifiedName(functionName);
      currentScope().addFunctionSymbol(pyFunctionDefTree, fullyQualifiedName);
      createScope(pyFunctionDefTree, currentScope());
      enterScope(pyFunctionDefTree);
      createTypeParameters(pyFunctionDefTree.typeParams());
      createParameters(pyFunctionDefTree);
      super.visitFunctionDef(pyFunctionDefTree);
      leaveScope();
    }

    private void createTypeParameters(@Nullable TypeParams typeParams) {
      Optional.ofNullable(typeParams)
        .map(TypeParams::typeParamsList)
        .stream()
        .flatMap(Collection::stream)
        .forEach(typeParam -> addBindingUsage(typeParam.name(), Usage.Kind.TYPE_PARAM_DECLARATION));
    }

    @Override
    public void visitTypeAliasStatement(TypeAliasStatement typeAliasStatement) {
      addBindingUsage(typeAliasStatement.name(), Usage.Kind.TYPE_ALIAS_DECLARATION);
      super.visitTypeAliasStatement(typeAliasStatement);
    }

    @Override
    public void visitClassDef(ClassDef pyClassDefTree) {
      String className = pyClassDefTree.name().name();
      String fullyQualifiedName = getFullyQualifiedName(className);
      currentScope().addClassSymbol(pyClassDefTree, fullyQualifiedName);
      createScope(pyClassDefTree, currentScope());
      enterScope(pyClassDefTree);
      createTypeParameters(pyClassDefTree.typeParams());
      super.visitClassDef(pyClassDefTree);
      leaveScope();
    }

    @CheckForNull
    private String getFullyQualifiedName(String name) {
      String prefix = scopeQualifiedName();
      if (prefix != null) {
        return prefix.isEmpty() ? name : (prefix + "." + name);
      }
      return null;
    }

    private String scopeQualifiedName() {
      Tree scopeTree = currentScopeRootTree();
      if (scopeTree.is(Kind.CLASSDEF, Kind.FUNCDEF)) {
        Name name = scopeTree.is(Kind.CLASSDEF)
          ? ((ClassDef) scopeTree).name()
          : ((FunctionDef) scopeTree).name();
        return Optional.ofNullable(name.symbol()).map(Symbol::fullyQualifiedName).orElse(name.name());
      }
      return fullyQualifiedModuleName;
    }

    @Override
    public void visitImportName(ImportName pyImportNameTree) {
      createImportedNames(pyImportNameTree.modules(), null, Collections.emptyList());
      super.visitImportName(pyImportNameTree);
    }

    @Override
    public void visitImportFrom(ImportFrom importFrom) {
      DottedName moduleTree = importFrom.module();
      String moduleName = moduleTree != null
        ? moduleTree.names().stream().map(Name::name).collect(Collectors.joining("."))
        : null;
      if (importFrom.isWildcardImport()) {
        importedModulesFQN.add(moduleName);
        Set importedModuleSymbols = projectLevelSymbolTable.getSymbolsFromModule(moduleName);
        if (importedModuleSymbols == null && moduleName != null && !moduleName.equals(fullyQualifiedModuleName)) {
          importedModuleSymbols = TypeShed.symbolsForModule(moduleName).values().stream()
            .map(importedSymbol -> currentScope().copySymbol(importedSymbol.name(), importedSymbol)).collect(Collectors.toSet());
        }
        if (importedModuleSymbols != null && !importedModuleSymbols.isEmpty()) {
          currentScope().createSymbolsFromWildcardImport(importedModuleSymbols, importFrom);
          ((ImportFromImpl) importFrom).setHasUnresolvedWildcardImport(false);
        } else {
          ((ImportFromImpl) importFrom).setHasUnresolvedWildcardImport(true);
        }
      } else {
        createImportedNames(importFrom.importedNames(), moduleName, importFrom.dottedPrefixForModule());
      }
      super.visitImportFrom(importFrom);
    }

    private void createImportedNames(List importedNames, @Nullable String fromModuleName, List dottedPrefix) {
      importedNames.forEach(module -> {
        List dottedNames = module.dottedName().names();
        Name nameTree = dottedNames.get(0);
        String targetModuleName = fromModuleName;
        String fullyQualifiedName = targetModuleName != null
          ? (targetModuleName + "." + nameTree.name())
          : nameTree.name();
        if (!dottedPrefix.isEmpty()) {
          fullyQualifiedName = resolveFullyQualifiedNameBasedOnRelativeImport(dottedPrefix, fullyQualifiedName);
          targetModuleName = resolveFullyQualifiedNameBasedOnRelativeImport(dottedPrefix, targetModuleName);
        } else {
          importedModulesFQN.add(fullyQualifiedName);
        }
        Name alias = module.alias();
        if (targetModuleName != null) {
          currentScope().addImportedSymbol(alias == null ? nameTree : alias, fullyQualifiedName, targetModuleName);
        } else if (alias != null) {
          String fullName = dottedNames.stream().map(Name::name).collect(Collectors.joining("."));
          currentScope().addModuleSymbol(alias, fullName);
        } else if (dottedPrefix.isEmpty() && dottedNames.size() > 1) {
            // Submodule import
          dottedNames.stream().map(Name::name).reduce((a, b) -> String.join(".", a, b))
            .ifPresent(fqn -> currentScope().addSubmoduleSymbol(nameTree, fqn));
        } else {
          // It's a simple case - no "from" imports or aliasing
          currentScope().addModuleSymbol(nameTree, fullyQualifiedName);
        }
      });
    }

    @CheckForNull
    private String resolveFullyQualifiedNameBasedOnRelativeImport(List dottedPrefix, @Nullable String moduleName) {
      if (filePath == null || dottedPrefix.size() > filePath.size()) {
        return null;
      }
      String resolvedPackageName = String.join(".", filePath.subList(0, filePath.size() - dottedPrefix.size()));
      if (moduleName == null) {
        importedModulesFQN.add(resolvedPackageName);
        return resolvedPackageName;
      }
      if (resolvedPackageName.isEmpty()) {
        importedModulesFQN.add(moduleName);
        return moduleName;
      }
      String resolvedModuleFQN = resolvedPackageName + "." + moduleName;
      importedModulesFQN.add(resolvedModuleFQN);
      return resolvedModuleFQN;
    }

    @Override
    public void visitForStatement(ForStatement pyForStatementTree) {
      createLoopVariables(pyForStatementTree);
      super.visitForStatement(pyForStatementTree);
    }

    @Override
    public void visitComprehensionFor(ComprehensionFor tree) {
      addCompDeclarationParam(tree.loopExpression());
      super.visitComprehensionFor(tree);
    }

    private void addCompDeclarationParam(Tree tree) {
      boundNamesFromExpression(tree).forEach(name -> addBindingUsage(name, Usage.Kind.COMP_DECLARATION));
    }

    private void createLoopVariables(ForStatement loopTree) {
      loopTree.expressions().forEach(expr ->
        boundNamesFromExpression(expr).forEach(name -> addBindingUsage(name, Usage.Kind.LOOP_DECLARATION)));
    }

    private void createParameters(FunctionLike function) {
      ParameterList parameterList = function.parameters();
      if (parameterList == null || parameterList.all().isEmpty()) {
        return;
      }

      boolean hasSelf = false;
      if (function.isMethodDefinition()) {
        AnyParameter first = parameterList.all().get(0);
        if (first.is(Kind.PARAMETER)) {
          currentScope().createSelfParameter((Parameter) first);
          hasSelf = true;
        }
      }

      parameterList.nonTuple()
        .stream()
        .skip(hasSelf ? 1 : 0)
        .map(Parameter::name)
        .filter(Objects::nonNull)
        .forEach(param -> addBindingUsage(param, Usage.Kind.PARAMETER));

      parameterList.all().stream()
        .filter(param -> param.is(Kind.TUPLE_PARAMETER))
        .map(TupleParameter.class::cast)
        .forEach(this::addTupleParamElementsToBindingUsage);
    }

    private void addTupleParamElementsToBindingUsage(TupleParameter param) {
      param.parameters().stream()
        .filter(p -> p.is(Kind.PARAMETER))
        .map(p -> ((Parameter) p).name())
        .forEach(name -> addBindingUsage(name, Usage.Kind.PARAMETER));
      param.parameters().stream()
        .filter(p -> p.is(Kind.TUPLE_PARAMETER))
        .map(TupleParameter.class::cast)
        .forEach(this::addTupleParamElementsToBindingUsage);
    }

    @Override
    public void visitAssignmentStatement(AssignmentStatement pyAssignmentStatementTree) {
      List lhs = SymbolUtils.assignmentsLhs(pyAssignmentStatementTree);

      assignmentLeftHandSides.addAll(lhs);

      lhs.forEach(expression -> boundNamesFromExpression(expression).forEach(name -> addBindingUsage(name, Usage.Kind.ASSIGNMENT_LHS)));

      super.visitAssignmentStatement(pyAssignmentStatementTree);
    }

    @Override
    public void visitAnnotatedAssignment(AnnotatedAssignment annotatedAssignment) {
      if (annotatedAssignment.variable().is(Kind.NAME)) {
        Name variable = (Name) annotatedAssignment.variable();
        addBindingUsage(variable, Usage.Kind.ASSIGNMENT_LHS, getFullyQualifiedName(variable.name()));
      }
      super.visitAnnotatedAssignment(annotatedAssignment);
    }

    @Override
    public void visitCompoundAssignment(CompoundAssignmentStatement pyCompoundAssignmentStatementTree) {
      if (pyCompoundAssignmentStatementTree.lhsExpression().is(Kind.NAME)) {
        addBindingUsage((Name) pyCompoundAssignmentStatementTree.lhsExpression(), Usage.Kind.COMPOUND_ASSIGNMENT_LHS);
      }
      super.visitCompoundAssignment(pyCompoundAssignmentStatementTree);
    }

    @Override
    public void visitAssignmentExpression(AssignmentExpression assignmentExpression) {
      final Scope scope = currentScope();
      Tree scopeRootTree = scope.rootTree;
      if (scopeRootTree.is(Kind.GENERATOR_EXPR) || scopeRootTree instanceof ComprehensionExpression || scopeRootTree instanceof DictCompExpression) {
        scope.parent().addBindingUsage(assignmentExpression.lhsName(), Usage.Kind.ASSIGNMENT_LHS, null);
      } else {
        addBindingUsage(assignmentExpression.lhsName(), Usage.Kind.ASSIGNMENT_LHS);
      }
      super.visitAssignmentExpression(assignmentExpression);
    }

    @Override
    public void visitGlobalStatement(GlobalStatement pyGlobalStatementTree) {
      pyGlobalStatementTree.variables()
        .forEach(name -> {
          // Global statements are not binding usages, but we consider them as such for symbol creation
          moduleScope.addBindingUsage(name, Usage.Kind.GLOBAL_DECLARATION, null);
          currentScope().addGlobalName(name.name());
        });

      super.visitGlobalStatement(pyGlobalStatementTree);
    }

    @Override
    public void visitNonlocalStatement(NonlocalStatement pyNonlocalStatementTree) {
      pyNonlocalStatementTree.variables().stream()
        .map(Name::name)
        .forEach(name -> currentScope().addNonLocalName(name));
      super.visitNonlocalStatement(pyNonlocalStatementTree);
    }

    @Override
    public void visitExceptClause(ExceptClause exceptClause) {
      boundNamesFromExpression(exceptClause.exceptionInstance()).forEach(name -> addBindingUsage(name, Usage.Kind.EXCEPTION_INSTANCE));
      super.visitExceptClause(exceptClause);
    }

    @Override
    public void visitWithItem(WithItem withItem) {
      boundNamesFromExpression(withItem.expression()).forEach(name -> addBindingUsage(name, Usage.Kind.WITH_INSTANCE));
      super.visitWithItem(withItem);
    }

    @Override
    public void visitCapturePattern(CapturePattern capturePattern) {
      addBindingUsage(capturePattern.name(), Usage.Kind.PATTERN_DECLARATION);
      super.visitCapturePattern(capturePattern);
    }

    private void createScope(Tree tree, @Nullable Scope parent) {
      scopesByRootTree.put(tree, new Scope(parent, tree, pythonFile, fullyQualifiedModuleName, projectLevelSymbolTable));
    }

    private void addBindingUsage(Name nameTree, Usage.Kind usage) {
      addBindingUsage(nameTree, usage, null);
    }

    private void addBindingUsage(Name nameTree, Usage.Kind usage, @Nullable String fullyQualifiedName) {
      currentScope().addBindingUsage(nameTree, usage, fullyQualifiedName);
    }
  }

  /**
   * Read (i.e. non-binding) usages have to be visited in a second phase.
   * They can't be visited in the same phase as write (i.e. binding) usages,
   * since a read usage may appear in the syntax tree "before" it's declared (written).
   */
  private class SecondPhaseVisitor extends ScopeVisitor {

    @Override
    public void visitFileInput(FileInput tree) {
      enterScope(tree);
      super.visitFileInput(tree);
    }

    @Override
    public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
      scan(pyFunctionDefTree.decorators());
      enterScope(pyFunctionDefTree);
      scan(pyFunctionDefTree.name());
      scan(pyFunctionDefTree.typeParams());
      scan(pyFunctionDefTree.parameters());
      scan(pyFunctionDefTree.returnTypeAnnotation());
      scan(pyFunctionDefTree.body());
      leaveScope();
    }

    @Override
    public void visitParameter(Parameter parameter) {
      // parameter default value should not be in the function scope.
      Tree currentScopeTree = leaveScope();
      scan(parameter.defaultValue());
      enterScope(currentScopeTree);
      scan(parameter.name());
      scan(parameter.typeAnnotation());
    }

    @Override
    public void visitLambda(LambdaExpression pyLambdaExpressionTree) {
      enterScope(pyLambdaExpressionTree);
      super.visitLambda(pyLambdaExpressionTree);
      leaveScope();
    }

    @Override
    public void visitPyListOrSetCompExpression(ComprehensionExpression tree) {
      enterScope(tree);
      scan(tree.resultExpression());
      ComprehensionFor comprehensionFor = tree.comprehensionFor();
      scan(comprehensionFor.loopExpression());
      leaveScope();
      scan(comprehensionFor.iterable());
      enterScope(tree);
      scan(comprehensionFor.nestedClause());
      leaveScope();
    }

    @Override
    public void visitDictCompExpression(DictCompExpression tree) {
      enterScope(tree);
      scan(tree.keyExpression());
      scan(tree.valueExpression());
      ComprehensionFor comprehensionFor = tree.comprehensionFor();
      scan(comprehensionFor.loopExpression());
      leaveScope();
      scan(comprehensionFor.iterable());
      enterScope(tree);
      scan(comprehensionFor.nestedClause());
      leaveScope();
    }

    @Override
    public void visitTypeAnnotation(TypeAnnotation tree) {
      if (tree.is(Kind.PARAMETER_TYPE_ANNOTATION) || tree.is(Kind.RETURN_TYPE_ANNOTATION)) {
        // The scope of the type annotations on a function declaration should be the scope enclosing the function, and not the scope of
        // the function body itself. Note that this code assumes that we already entered the function body scope by visiting the type
        // annotations, so there should always be a scope to pop out and return to here.
        Tree currentScopeTree = leaveScope();
        super.visitTypeAnnotation(tree);
        enterScope(currentScopeTree);
        super.visitTypeAnnotation(tree);
      } else {
        super.visitTypeAnnotation(tree);
      }
    }

    @Override
    public void visitClassDef(ClassDef pyClassDefTree) {
      scan(pyClassDefTree.args());
      scan(pyClassDefTree.decorators());
      enterScope(pyClassDefTree);
      scan(pyClassDefTree.name());
      resolveTypeHierarchy(pyClassDefTree, pyClassDefTree.name().symbol(), pythonFile, scopesByRootTree.get(fileInput).symbolsByName);
      scan(pyClassDefTree.body());
      leaveScope();
    }

    @Override
    public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) {
      // We need to firstly create symbol for qualifier
      super.visitQualifiedExpression(qualifiedExpression);
      if (qualifiedExpression.qualifier() instanceof HasSymbol hasSymbol) {
        Symbol qualifierSymbol = hasSymbol.symbol();
        if (qualifierSymbol != null) {
          Usage.Kind usageKind = assignmentLeftHandSides.contains(qualifiedExpression) ? Usage.Kind.ASSIGNMENT_LHS : Usage.Kind.OTHER;
          ((SymbolImpl) qualifierSymbol).addOrCreateChildUsage(qualifiedExpression.name(), usageKind);
        }
      }
    }

    @Override
    public void visitName(Name pyNameTree) {
      if (!pyNameTree.isVariable()) {
        return;
      }
      addSymbolUsage(pyNameTree);
      super.visitName(pyNameTree);
    }

    private void addSymbolUsage(Name nameTree) {
      Scope scope = scopesByRootTree.get(currentScopeRootTree());
      SymbolImpl symbol = scope.resolve(nameTree.name());
      // TODO: use Set to improve performances
      if (symbol != null && symbol.usages().stream().noneMatch(usage -> usage.tree().equals(nameTree))) {
        symbol.addUsage(nameTree, Usage.Kind.OTHER);
      }
    }
  }

  private class ThirdPhaseVisitor extends BaseTreeVisitor {

    @Override
    public void visitFunctionDef(FunctionDef functionDef) {
      FunctionSymbol functionSymbol = ((FunctionDefImpl) functionDef).functionSymbol();
      ParameterList parameters = functionDef.parameters();
      if (functionSymbol != null) {
        FunctionSymbolImpl functionSymbolImpl = (FunctionSymbolImpl) functionSymbol;
        if (parameters != null) {
          functionSymbolImpl.setParametersWithType(parameters);
        }
        TypeAnnotation typeAnnotation = functionDef.returnTypeAnnotation();
        if (typeAnnotation != null) {
          functionSymbolImpl.setDeclaredReturnType(InferredTypes.fromTypeAnnotation(typeAnnotation));
        }
      }
      super.visitFunctionDef(functionDef);
    }

    @Override
    public void visitAnnotatedAssignment(AnnotatedAssignment annotatedAssignment) {
      if (annotatedAssignment.variable().is(Kind.NAME)) {
        Name variable = (Name) annotatedAssignment.variable();
        TypeAnnotation annotation = annotatedAssignment.annotation();
        Symbol symbol = variable.symbol();
        Optional.ofNullable(symbol)
          .ifPresent(s -> ((SymbolImpl) symbol).setInferredType(InferredTypes.fromTypeAnnotation(annotation)));
      }
      super.visitAnnotatedAssignment(annotatedAssignment);
    }

    /**
     * Handle class member usages like the following:
     * 
     *     class A:
     *       foo = 42
     *     print(A.foo)
     * 
*/ @Override public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) { super.visitQualifiedExpression(qualifiedExpression); TreeUtils.getSymbolFromTree(qualifiedExpression.qualifier()) .filter(symbol -> symbol.kind() == Symbol.Kind.CLASS) .map(ClassSymbol.class::cast) .flatMap(classSymbol -> classSymbol.resolveMember(qualifiedExpression.name().name())) .ifPresent(member -> { Usage.Kind usageKind = assignmentLeftHandSides.contains(qualifiedExpression) ? Usage.Kind.ASSIGNMENT_LHS : Usage.Kind.OTHER; ((SymbolImpl) member).addUsage(qualifiedExpression.name(), usageKind); }); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy