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

org.sonar.python.quickfix.TextEditUtils 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.quickfix;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.sonar.plugins.python.api.PythonLine;
import org.sonar.plugins.python.api.quickfix.PythonTextEdit;
import org.sonar.plugins.python.api.symbols.Symbol;
import org.sonar.plugins.python.api.symbols.Usage;
import org.sonar.plugins.python.api.tree.HasSymbol;
import org.sonar.plugins.python.api.tree.Statement;
import org.sonar.plugins.python.api.tree.StatementList;
import org.sonar.plugins.python.api.tree.Token;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.python.tree.TreeUtils;

public class TextEditUtils {

  private TextEditUtils() {}

  /**
   * Insert a line with the same offset as the given tree, before the given tree.
   * Offset is applied to multiline insertions.
   */
  public static PythonTextEdit insertLineBefore(Tree tree, String textToInsert) {
    String lineOffset = offset(tree);
    textToInsert += "\n";
    String textWithOffset = textToInsert.replace("\n", "\n" + lineOffset);
    return insertBefore(tree, textWithOffset);
  }

  /**
   * Insert a line with the same offset as the given tree, after the given tree.
   * Offset is applied to multiline insertions and calculated by the reference tree.
   */
  public static PythonTextEdit insertLineAfter(Tree tree, Tree indentReference, String textToInsert) {
    String lineOffset = offset(indentReference);
    String textWithOffset = "\n" + lineOffset + textToInsert.replace("\n", "\n" + lineOffset);
    return insertAfter(tree, textWithOffset);
  }

  private static String offset(Tree referenceTree) {
    return " ".repeat(referenceTree.firstToken().pythonColumn());
  }

  public static PythonTextEdit insertBefore(Tree tree, String textToInsert) {
    Token token = tree.firstToken();
    return insertAtPosition(token.pythonLine(), token.pythonColumn(), textToInsert);
  }

  public static PythonTextEdit insertAfter(Tree tree, String textToInsert) {
    Token token = tree.firstToken();
    int lengthToken = token.value().length();
    return insertAtPosition(token.pythonLine(), token.pythonColumn() + lengthToken, textToInsert);
  }

  public static PythonTextEdit insertAtPosition(PythonLine pythonLine, int column, String textToInsert) {
    return new PythonTextEdit(textToInsert, pythonLine.line(), column, pythonLine.line(), column);
  }

  public static PythonTextEdit replace(Tree toReplace, String replacementText) {
    return replaceRange(toReplace, toReplace, replacementText);
  }

  public static PythonTextEdit replaceRange(Tree start, Tree end, String replacementText) {
    Token first = start.firstToken();
    Token last = end.lastToken();
    return new PythonTextEdit(replacementText, first.line(), first.column(), last.line(), last.column() + last.value().length());
  }

  /**
   * Shift body statements to be on same level as the parent statement
   * Filter out text edits which apply on the same line which could show up with multiple statements on the same line
   */
  public static List shiftLeft(StatementList statementList) {
    int offset = statementList.firstToken().column() - statementList.parent().firstToken().column();
    return statementList.statements().stream()
      .map(statement -> shiftLeft(statement, offset))
      .flatMap(List::stream)
      .distinct()
      .toList();
  }

  /**
   * Shift single statement of a statement list by the given offset.
   * Take care about child statements by collecting all child tokens and shift each line once.
   */
  public static List shiftLeft(Tree tree, int offset) {
    return TreeUtils.tokens(tree).stream()
      .filter(token -> token.column() >= offset)
      .map(Token::pythonLine)
      .distinct()
      .map(line -> removeRange(line, 0, line, offset))
      .toList();
  }

  public static PythonTextEdit removeRange(PythonLine startLine, int startColumn, PythonLine endLine, int endColumn) {
    return new PythonTextEdit("", startLine.line(), startColumn, endLine.line(), endColumn);
  }

  /**
   * Remove range including the start token until the beginning of the end tree's first token.
   * This is useful to remove and shift multiple statement over multiple lines.
   */
  public static PythonTextEdit removeUntil(Tree start, Tree until) {
    return removeRange(start.firstToken().pythonLine(), start.firstToken().column(), until.firstToken().pythonLine(), until.firstToken().column());
  }

  public static PythonTextEdit removeStatement(Statement statement) {
    Token firstTokenOfStmt = statement.firstToken();
    Token lastTokenOfStmt = TreeUtils.getTreeSeparatorOrLastToken(statement);

    List siblings = statement.parent().children();
    int indexOfTree = siblings.indexOf(statement);
    Tree previous = indexOfTree > 0 ? siblings.get(indexOfTree - 1) : null;
    Tree next = indexOfTree < siblings.size() - 1 ? siblings.get(indexOfTree + 1) : null;

    // Statement is the single element in the block
    // Replace by `pass` keyword
    if (previous == null && next == null) {
      return replace(statement, "pass");
    }

    boolean hasPreviousSiblingOnLine = previous != null && firstTokenOfStmt.line() == TreeUtils.getTreeSeparatorOrLastToken(previous.lastToken()).line();
    boolean hasNextSiblingOnLine = next != null && lastTokenOfStmt.line() == next.firstToken().line();

    if (hasNextSiblingOnLine) {
      // Statement is first on the line or between at least two statements
      // Remove from first token to last toke of statement
      Token firstNextToken = next.firstToken();
      return removeRange(firstTokenOfStmt.pythonLine(), firstTokenOfStmt.column(), firstNextToken.pythonLine(), firstNextToken.column());
    } else if (hasPreviousSiblingOnLine) {
      // Statement is last on the line and has one or more previous statement on the line
      // Remove from last token or separator of previous statement to avoid trailing white spaces
      // Keep the line break to ensure elements on the next line don't get pushed to the current line
      Token lastPreviousToken = TreeUtils.getTreeSeparatorOrLastToken(previous);
      return removeRange(lastPreviousToken.pythonLine(), getEndColumn(lastPreviousToken), lastPreviousToken.pythonLine(), getEndColumn(lastTokenOfStmt) - 1);
    } else {
      // Statement is single on the line
      // Remove the entire line including indent and line break
      return removeRange(firstTokenOfStmt.pythonLine(), 0, lastTokenOfStmt.pythonLine(), getEndColumn(lastTokenOfStmt));
    }
  }

  /**
   * Token can be longer than a single character so we have to add the value length to determine the end column of the token
   */
  private static int getEndColumn(Token token) {
    return token.column() + token.value().length();
  }

  public static PythonTextEdit remove(Tree toRemove) {
    return replace(toRemove, "");
  }

  /**
   * Returns a list of all replacements for the symbol to new name.
   * If there is no usages empty list is returned.
   */
  public static List renameAllUsages(HasSymbol node, String newName) {
    Symbol symbol = node.symbol();
    List usages = symbol != null ? symbol.usages() : Collections.emptyList();
    List result = new LinkedList<>();
    for(Usage usage: usages) {
      PythonTextEdit text = replace(usage.tree().firstToken(), newName);
      result.add(text);
    }
    return result;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy