blob: 9b4c95f7d2b24401f0745d26b1f7485b79480cb2 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.refactoring;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.Function;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PythonStringUtil;
import com.jetbrains.python.inspections.PyStringFormatParser;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.PyTypeChecker;
import com.jetbrains.python.psi.types.PyTypeParser;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static com.jetbrains.python.PyTokenTypes.*;
import static com.jetbrains.python.inspections.PyStringFormatParser.*;
/**
* @author Dennis.Ushakov
*/
public class PyReplaceExpressionUtil implements PyElementTypes {
/**
* This marker is added in cases where valid selection nevertheless breaks existing expression.
* It can happen in cases like (here {@code <start> and <end>} represent selection boundaries):
* <ul>
* <li>Selection conflicts with operator precedence: {@code n = 1 * <start>2 + 3<end>}</li>
* <li>Selection conflicts with operator associativity: {@code n = 1 + <start>2 + 3<end>}</li>
* <li>Part of string literal is selected: {@code s = 'green <start>eggs<end> and ham'}</li>
* </ul>
*/
public static final Key<Pair<PsiElement, TextRange>> SELECTION_BREAKS_AST_NODE =
new Key<Pair<PsiElement, TextRange>>("python.selection.breaks.ast.node");
private PyReplaceExpressionUtil() {}
/**
* @param oldExpr old expression that will be substituted
* @param newExpr new expression to substitute with
* @return whether new expression should be wrapped in parenthesis to preserve original semantics
*/
public static boolean isNeedParenthesis(@NotNull final PyElement oldExpr, @NotNull final PyElement newExpr) {
final PyElement parentExpr = (PyElement)oldExpr.getParent();
if (parentExpr instanceof PyArgumentList) {
return newExpr instanceof PyTupleExpression;
}
if (parentExpr instanceof PyParenthesizedExpression || !(parentExpr instanceof PyExpression)) {
return false;
}
final int newPriority = getExpressionPriority(newExpr);
final int parentPriority = getExpressionPriority(parentExpr);
if (parentPriority > newPriority) {
return true;
}
else if (parentPriority == newPriority && parentPriority != 0 && parentExpr instanceof PyBinaryExpression) {
final PyBinaryExpression binaryExpression = (PyBinaryExpression)parentExpr;
if (isNotAssociative(binaryExpression) && oldExpr == getLeastPrioritySide(binaryExpression)) {
return true;
}
}
else if (newExpr instanceof PyConditionalExpression && parentExpr instanceof PyConditionalExpression) {
return true;
}
return false;
}
@Nullable
private static PyExpression getLeastPrioritySide(@NotNull PyBinaryExpression expression) {
if (expression.isOperator("**")) {
return expression.getLeftExpression();
}
else {
return expression.getRightExpression();
}
}
public static PsiElement replaceExpression(@NotNull final PsiElement oldExpression,
@NotNull final PsiElement newExpression) {
final Pair<PsiElement, TextRange> data = oldExpression.getUserData(SELECTION_BREAKS_AST_NODE);
if (data != null) {
final PsiElement element = data.first;
final TextRange textRange = data.second;
final String parentText = element.getText();
final String prefix = parentText.substring(0, textRange.getStartOffset());
final String suffix = parentText.substring(textRange.getEndOffset(), element.getTextLength());
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression);
if (element instanceof PyStringLiteralExpression) {
return replaceSubstringInStringLiteral((PyStringLiteralExpression)element, newExpression, textRange);
}
final PsiElement expression = generator.createFromText(languageLevel, element.getClass(), prefix + newExpression.getText() + suffix);
return element.replace(expression);
}
else {
return oldExpression.replace(newExpression);
}
}
@Nullable
private static PsiElement replaceSubstringInStringLiteral(@NotNull PyStringLiteralExpression oldExpression,
@NotNull PsiElement newExpression,
@NotNull TextRange textRange) {
final String fullText = oldExpression.getText();
final Pair<String, String> detectedQuotes = PythonStringUtil.getQuotes(fullText);
final Pair<String, String> quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'");
final String prefix = fullText.substring(0, textRange.getStartOffset());
final String suffix = fullText.substring(textRange.getEndOffset(), oldExpression.getTextLength());
final PyExpression formatValue = PyStringFormatParser.getFormatValueExpression(oldExpression);
final PyArgumentList newStyleFormatValue = PyStringFormatParser.getNewStyleFormatValueExpression(oldExpression);
final String newText = newExpression.getText();
final List<PyStringFormatParser.SubstitutionChunk> substitutions;
if (newStyleFormatValue != null) {
substitutions = filterSubstitutions(parseNewStyleFormat(fullText));
}
else {
substitutions = filterSubstitutions(parsePercentFormat(fullText));
}
final boolean hasSubstitutions = substitutions.size() > 0;
if (formatValue != null && !containsStringFormatting(substitutions, textRange)) {
if (formatValue instanceof PyTupleExpression) {
return replaceSubstringWithTupleFormatting(oldExpression, newExpression, textRange, prefix, suffix,
(PyTupleExpression)formatValue, substitutions);
}
else if (formatValue instanceof PyDictLiteralExpression) {
return replaceSubstringWithDictFormatting(oldExpression, quotes, prefix, suffix, formatValue, newText);
}
else {
final TypeEvalContext context = TypeEvalContext.userInitiated(oldExpression.getProject(), oldExpression.getContainingFile());
final PyType valueType = context.getType(formatValue);
final PyBuiltinCache builtinCache = PyBuiltinCache.getInstance(oldExpression);
final PyType tupleType = builtinCache.getTupleType();
final PyType mappingType = PyTypeParser.getTypeByName(null, "collections.Mapping");
if (!PyTypeChecker.match(tupleType, valueType, context) ||
(mappingType != null && !PyTypeChecker.match(mappingType, valueType, context))) {
return replaceSubstringWithSingleValueFormatting(oldExpression, textRange, prefix, suffix, formatValue, newText, substitutions);
}
}
}
if (newStyleFormatValue != null && hasSubstitutions && !containsStringFormatting(substitutions, textRange)) {
final PyExpression[] arguments = newStyleFormatValue.getArguments();
boolean hasStarArguments = false;
for (PyExpression argument : arguments) {
if (argument instanceof PyStarArgument) {
hasStarArguments = true;
}
}
if (!hasStarArguments) {
return replaceSubstringWithNewStyleFormatting(oldExpression, textRange, prefix, suffix, newStyleFormatValue, newText,
substitutions);
}
}
if (isConcatFormatting(oldExpression) || hasSubstitutions) {
return replaceSubstringWithConcatFormatting(oldExpression, quotes, prefix, suffix, newText, hasSubstitutions);
}
return replaceSubstringWithoutFormatting(oldExpression, prefix, suffix, newText);
}
private static PsiElement replaceSubstringWithSingleValueFormatting(PyStringLiteralExpression oldExpression,
TextRange textRange,
String prefix,
String suffix,
PyExpression formatValue,
String newText,
List<PyStringFormatParser.SubstitutionChunk> substitutions) {
// 'foo%s' % value if value is not tuple or mapping -> '%s%s' % (s, value)
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression);
final String newLiteralText = prefix + "%s" + suffix;
final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText);
oldExpression.replace(newLiteralExpression);
final StringBuilder builder = new StringBuilder();
builder.append("(");
final int i = getPositionInRanges(PyStringFormatParser.substitutionsToRanges(substitutions), textRange);
final int pos;
if (i == 0) {
pos = builder.toString().length();
builder.append(newText);
builder.append(",");
builder.append(formatValue.getText());
}
else {
builder.append(formatValue.getText());
builder.append(",");
pos = builder.toString().length();
builder.append(newText);
}
builder.append(")");
final PsiElement newElement = formatValue.replace(generator.createExpressionFromText(languageLevel, builder.toString()));
return newElement.findElementAt(pos);
}
private static PsiElement replaceSubstringWithDictFormatting(PyStringLiteralExpression oldExpression,
Pair<String, String> quotes,
String prefix,
String suffix,
PyExpression formatValue,
String newText) {
// 'foo%(x)s' % {'x': x} -> '%(s)s%(x)s' % {'x': x, 's': s}
// TODO: Support the dict() function
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression);
final String newLiteralText = prefix + "%(" + newText + ")s" + suffix;
final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText);
oldExpression.replace(newLiteralExpression);
final PyDictLiteralExpression dict = (PyDictLiteralExpression)formatValue;
final StringBuilder builder = new StringBuilder();
builder.append("{");
final PyKeyValueExpression[] elements = dict.getElements();
builder.append(StringUtil.join(elements, new Function<PyKeyValueExpression, String>() {
@Override
public String fun(PyKeyValueExpression expression) {
return expression.getText();
}
}, ","));
if (elements.length > 0) {
builder.append(",");
}
builder.append(quotes.getSecond());
builder.append(newText);
builder.append(quotes.getSecond());
builder.append(":");
final int pos = builder.toString().length();
builder.append(newText);
builder.append("}");
final PyExpression newDictLiteral = generator.createExpressionFromText(languageLevel, builder.toString());
final PsiElement newElement = formatValue.replace(newDictLiteral);
return newElement.findElementAt(pos);
}
private static PsiElement replaceSubstringWithTupleFormatting(PyStringLiteralExpression oldExpression,
PsiElement newExpression,
TextRange textRange,
String prefix,
String suffix,
PyTupleExpression tupleFormatValue,
List<PyStringFormatParser.SubstitutionChunk> substitutions) {
// 'foo%s' % (x,) -> '%s%s' % (s, x)
final String newLiteralText = prefix + "%s" + suffix;
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText);
oldExpression.replace(newLiteralExpression);
final PyExpression[] members = tupleFormatValue.getElements();
final int n = members.length;
final int i = Math.min(n, Math.max(0, getPositionInRanges(PyStringFormatParser.substitutionsToRanges(substitutions), textRange)));
final boolean last = i == n;
final ASTNode trailingComma = PyPsiUtils.getNextComma(members[n - 1].getNode());
if (trailingComma != null) {
tupleFormatValue.getNode().removeChild(trailingComma);
}
final PyExpression before = last ? null : members[i];
PyUtil.addListNode(tupleFormatValue, newExpression, before != null ? before.getNode() : null, i == 0 || !last, last, !last);
return newExpression;
}
private static PsiElement replaceSubstringWithoutFormatting(@NotNull PyStringLiteralExpression oldExpression,
@NotNull String prefix,
@NotNull String suffix,
@NotNull String newText) {
// 'foobar' -> '%sbar' % s
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression);
final PsiElement parent = oldExpression.getParent();
final boolean parensNeeded = parent instanceof PyExpression && !(parent instanceof PyParenthesizedExpression);
final StringBuilder builder = new StringBuilder();
if (parensNeeded) {
builder.append("(");
}
builder.append(prefix);
builder.append("%s");
builder.append(suffix);
builder.append(" % ");
final int pos = builder.toString().length();
builder.append(newText);
if (parensNeeded) {
builder.append(")");
}
final PyExpression expression = generator.createExpressionFromText(languageLevel, builder.toString());
final PsiElement newElement = oldExpression.replace(expression);
return newElement.findElementAt(pos);
}
private static PsiElement replaceSubstringWithConcatFormatting(@NotNull PyStringLiteralExpression oldExpression,
@NotNull Pair<String, String> quotes,
@NotNull String prefix,
@NotNull String suffix,
@NotNull String newText,
boolean hasSubstitutions) {
// 'foobar' + 'baz' -> s + 'bar' + 'baz'
// 'foobar%s' -> s + 'bar%s'
// 'f%soobar' % x -> (s + 'bar') % x
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression);
final String leftQuote = quotes.getFirst();
final String rightQuote = quotes.getSecond();
final StringBuilder builder = new StringBuilder();
if (hasSubstitutions) {
builder.append("(");
}
if (!leftQuote.endsWith(prefix)) {
builder.append(prefix + rightQuote + " + ");
}
final int pos = builder.toString().length();
builder.append(newText);
if (!rightQuote.startsWith(suffix)) {
builder.append(" + " + leftQuote + suffix);
}
if (hasSubstitutions) {
builder.append(")");
}
final PsiElement expression = generator.createExpressionFromText(languageLevel, builder.toString());
final PsiElement newElement = oldExpression.replace(expression);
return newElement.findElementAt(pos);
}
private static PsiElement replaceSubstringWithNewStyleFormatting(@NotNull PyStringLiteralExpression oldExpression,
@NotNull TextRange textRange,
@NotNull String prefix,
@NotNull String suffix,
@NotNull PyArgumentList newStyleFormatValue,
@NotNull String newText,
@NotNull List<PyStringFormatParser.SubstitutionChunk> substitutions) {
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression);
final PyExpression[] arguments = newStyleFormatValue.getArguments();
boolean hasKeywords = false;
int maxPosition = -1;
for (PyStringFormatParser.SubstitutionChunk substitution : substitutions) {
if (substitution.getMappingKey() != null) {
hasKeywords = true;
}
final Integer position = substitution.getPosition();
if (position != null && position > maxPosition) {
maxPosition = position;
}
}
if (hasKeywords) {
// 'foo{x}'.format(x='bar') -> '{s}oo{x}'.format(x='bar', s=s)
final String newLiteralText = prefix + "{" + newText + "}" + suffix;
final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText);
oldExpression.replace(newLiteralExpression);
final PyKeywordArgument kwarg = generator.createKeywordArgument(languageLevel, newText, newText);
newStyleFormatValue.addArgument(kwarg);
return kwarg.getValueExpression();
}
else if (maxPosition >= 0) {
// 'foo{0}'.format('bar') -> '{1}oo{0}'.format('bar', s)
final String newLiteralText = prefix + "{" + (maxPosition + 1) + "}" + suffix;
final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText);
oldExpression.replace(newLiteralExpression);
final PyExpression arg = generator.createExpressionFromText(languageLevel, newText);
newStyleFormatValue.addArgument(arg);
return arg;
}
else {
// 'foo{}'.format('bar') -> '{}oo{}'.format(s, 'bar')
final String newLiteralText = prefix + "{}" + suffix;
final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText);
oldExpression.replace(newLiteralExpression);
final int i = getPositionInRanges(PyStringFormatParser.substitutionsToRanges(substitutions), textRange);
final PyExpression arg = generator.createExpressionFromText(languageLevel, newText);
if (i == 0) {
newStyleFormatValue.addArgumentFirst(arg);
}
else if (i < arguments.length) {
newStyleFormatValue.addArgumentAfter(arg, arguments[i - 1]);
}
else {
newStyleFormatValue.addArgument(arg);
}
return arg;
}
}
private static int getPositionInRanges(@NotNull List<TextRange> ranges, @NotNull TextRange range) {
final int end = range.getEndOffset();
final int size = ranges.size();
for (int i = 0; i < size; i++) {
final TextRange r = ranges.get(i);
if (end < r.getStartOffset()) {
return i;
}
}
return size;
}
private static boolean containsStringFormatting(@NotNull List<PyStringFormatParser.SubstitutionChunk> substitutions,
@NotNull TextRange range) {
final List<TextRange> ranges = PyStringFormatParser.substitutionsToRanges(substitutions);
for (TextRange r : ranges) {
if (range.contains(r)) {
return true;
}
}
return false;
}
private static boolean isConcatFormatting(PyStringLiteralExpression element) {
final PsiElement parent = element.getParent();
return parent instanceof PyBinaryExpression && ((PyBinaryExpression)parent).isOperator("+");
}
private static boolean isNotAssociative(@NotNull final PyBinaryExpression binaryExpression) {
final IElementType opType = getOperationType(binaryExpression);
return COMPARISON_OPERATIONS.contains(opType) || binaryExpression instanceof PySliceExpression ||
opType == DIV || opType == FLOORDIV || opType == PERC || opType == EXP || opType == MINUS;
}
private static int getExpressionPriority(PyElement expr) {
int priority = 0;
if (expr instanceof PyReferenceExpression ||
expr instanceof PySubscriptionExpression ||
expr instanceof PySliceExpression ||
expr instanceof PyCallExpression) priority = 1;
else if (expr instanceof PyPrefixExpression) {
final IElementType opType = getOperationType(expr);
if (opType == PLUS || opType == MINUS || opType == TILDE) priority = 2;
if (opType == NOT_KEYWORD) priority = 11;
}
else if (expr instanceof PyBinaryExpression) {
final IElementType opType = getOperationType(expr);
if (opType == EXP) priority = 3;
if (opType == MULT || opType == DIV || opType == PERC || opType == FLOORDIV) priority = 4;
if (opType == PLUS || opType == MINUS) priority = 5;
if (opType == LTLT || opType == GTGT) priority = 6;
if (opType == AND) priority = 7;
if (opType == XOR) priority = 8;
if (opType == OR) priority = 9;
if (COMPARISON_OPERATIONS.contains(opType)) priority = 10;
if (opType == AND_KEYWORD) priority = 12;
if (opType == OR_KEYWORD) priority = 13;
}
else if (expr instanceof PyConditionalExpression) priority = 14;
else if (expr instanceof PyLambdaExpression) priority = 15;
return -priority;
}
@Nullable
private static IElementType getOperationType(@NotNull final PyElement expr) {
if (expr instanceof PyBinaryExpression) return ((PyBinaryExpression)expr).getOperator();
return ((PyPrefixExpression)expr).getOperator();
}
}