Merge branch 'introduce-for-substrings'

This commit is contained in:
Andrey Vlasovskikh
2012-12-27 23:02:10 +04:00
43 changed files with 404 additions and 112 deletions
@@ -4,9 +4,6 @@ import com.google.common.collect.Lists;
import com.intellij.extapi.psi.ASTDelegatePsiElement;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
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.*;
import com.intellij.psi.stubs.StubElement;
@@ -15,7 +12,6 @@ import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PythonStringUtil;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -29,8 +25,6 @@ import java.util.List;
* @author max
*/
public class PyPsiUtils {
public static final Key<Pair<PsiElement, TextRange>> SELECTION_BREAKS_AST_NODE =
new Key<Pair<PsiElement, TextRange>>("python.selection.breaks.ast.node");
private static final Logger LOG = Logger.getInstance(PyPsiUtils.class.getName());
private PyPsiUtils() {
@@ -68,52 +62,6 @@ public class PyPsiUtils {
return node;
}
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) {
final Pair<String, String> quotes = PythonStringUtil.getQuotes(parentText);
final PsiElement parent = element.getParent();
final boolean parensNeeded = parent instanceof PyExpression && !(parent instanceof PyParenthesizedExpression);
if (quotes != null) {
final String leftQuote = quotes.getFirst();
final String rightQuote = quotes.getSecond();
final StringBuilder builder = new StringBuilder();
if (parensNeeded) {
builder.append("(");
}
if (!leftQuote.endsWith(prefix)) {
builder.append(prefix + rightQuote + " + ");
}
final int pos = builder.toString().length();
builder.append(newExpression.getText());
if (!rightQuote.startsWith(suffix)) {
builder.append(" + " + leftQuote + suffix);
}
if (parensNeeded) {
builder.append(")");
}
final PsiElement expression = generator.createExpressionFromText(languageLevel, builder.toString());
final PsiElement newElement = element.replace(expression);
return newElement.findElementAt(pos);
}
}
final PsiElement expression = generator.createFromText(languageLevel, element.getClass(), prefix + newExpression.getText() + suffix);
return element.replace(expression);
}
else {
return oldExpression.replace(newExpression);
}
}
public static void addBeforeInParent(@NotNull final PsiElement anchor, @NotNull final PsiElement... newElements) {
final ASTNode anchorNode = anchor.getNode();
LOG.assertTrue(anchorNode != null);
@@ -1,7 +1,17 @@
package com.jetbrains.python.inspections;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.HashMap;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
/**
* @author yole
@@ -33,10 +43,10 @@ public class PyStringFormatParser {
}
public static class SubstitutionChunk extends FormatStringChunk {
private String myMappingKey;
private String myConversionFlags;
private String myWidth;
private String myPrecision;
@Nullable private String myMappingKey;
@Nullable private String myConversionFlags;
@Nullable private String myWidth;
@Nullable private String myPrecision;
private char myLengthModifier;
private char myConversionType;
private boolean myUnclosedMapping;
@@ -45,7 +55,7 @@ public class PyStringFormatParser {
super(startIndex, startIndex);
}
public void setEndIndex(int endIndex) {
private void setEndIndex(int endIndex) {
myEndIndex = endIndex;
}
@@ -53,39 +63,43 @@ public class PyStringFormatParser {
return myConversionType;
}
public void setConversionType(char conversionType) {
private void setConversionType(char conversionType) {
myConversionType = conversionType;
}
@Nullable
public String getMappingKey() {
return myMappingKey;
}
public void setMappingKey(String mappingKey) {
private void setMappingKey(@Nullable String mappingKey) {
myMappingKey = mappingKey;
}
@Nullable
public String getConversionFlags() {
return myConversionFlags;
}
public void setConversionFlags(String conversionFlags) {
private void setConversionFlags(@Nullable String conversionFlags) {
myConversionFlags = conversionFlags;
}
@Nullable
public String getWidth() {
return myWidth;
}
public void setWidth(String width) {
private void setWidth(@Nullable String width) {
myWidth = width;
}
@Nullable
public String getPrecision() {
return myPrecision;
}
public void setPrecision(String precision) {
private void setPrecision(@Nullable String precision) {
myPrecision = precision;
}
@@ -93,7 +107,7 @@ public class PyStringFormatParser {
return myLengthModifier;
}
public void setLengthModifier(char lengthModifier) {
private void setLengthModifier(char lengthModifier) {
myLengthModifier = lengthModifier;
}
@@ -101,13 +115,13 @@ public class PyStringFormatParser {
return myUnclosedMapping;
}
public void setUnclosedMapping(boolean unclosedMapping) {
private void setUnclosedMapping(boolean unclosedMapping) {
myUnclosedMapping = unclosedMapping;
}
}
private final String myLiteral;
private final List<FormatStringChunk> myResult = new ArrayList<FormatStringChunk>();
@NotNull private final String myLiteral;
@NotNull private final List<FormatStringChunk> myResult = new ArrayList<FormatStringChunk>();
private int myPos;
private static final String CONVERSION_FLAGS = "#0- +";
@@ -115,10 +129,11 @@ public class PyStringFormatParser {
private static final String LENGTH_MODIFIERS = "hlL";
private static final String VALID_CONVERSION_TYPES = "diouxXeEfFgGcrs";
public PyStringFormatParser(String literal) {
public PyStringFormatParser(@NotNull String literal) {
myLiteral = literal;
}
@NotNull
public List<FormatStringChunk> parse() {
myPos = 0;
while(myPos < myLiteral.length()) {
@@ -173,7 +188,7 @@ public class PyStringFormatParser {
chunk.setEndIndex(myPos);
}
private boolean isAtSet(final String characterSet) {
private boolean isAtSet(@NotNull final String characterSet) {
return myPos < myLiteral.length() && characterSet.indexOf(myLiteral.charAt(myPos)) >= 0;
}
@@ -181,6 +196,7 @@ public class PyStringFormatParser {
return myPos < myLiteral.length() && myLiteral.charAt(myPos) == c;
}
@NotNull
private String parseWidth() {
if (isAt('*')) {
myPos++;
@@ -189,7 +205,8 @@ public class PyStringFormatParser {
return parseWhileCharacterInSet(DIGITS);
}
private String parseWhileCharacterInSet(final String characterSet) {
@NotNull
private String parseWhileCharacterInSet(@NotNull final String characterSet) {
int flagStart = myPos;
while(isAtSet(characterSet)) {
myPos++;
@@ -197,6 +214,7 @@ public class PyStringFormatParser {
return myLiteral.substring(flagStart, myPos);
}
@NotNull
public List<SubstitutionChunk> parseSubstitutions() {
List<SubstitutionChunk> result = new ArrayList<SubstitutionChunk>();
for (FormatStringChunk chunk : parse()) {
@@ -206,4 +224,86 @@ public class PyStringFormatParser {
}
return result;
}
@NotNull
public static List<SubstitutionChunk> getPositionalSubstitutions(@NotNull List<SubstitutionChunk> substitutions) {
final ArrayList<SubstitutionChunk> result = new ArrayList<SubstitutionChunk>();
for (SubstitutionChunk s : substitutions) {
if (s.getMappingKey() == null) {
result.add(s);
}
}
return result;
}
@NotNull
public static Map<String, SubstitutionChunk> getKeywordSubstitutions(@NotNull List<SubstitutionChunk> substitutions) {
final Map<String, SubstitutionChunk> result = new HashMap<String, SubstitutionChunk>();
for (SubstitutionChunk s : substitutions) {
final String key = s.getMappingKey();
if (key != null) {
result.put(key, s);
}
}
return result;
}
@NotNull
public static List<TextRange> substitutionsToRanges(@NotNull List<SubstitutionChunk> substitutions) {
final List<TextRange> ranges = new ArrayList<TextRange>();
for (SubstitutionChunk substitution : substitutions) {
ranges.add(TextRange.create(substitution.getStartIndex(), substitution.getEndIndex()));
}
return ranges;
}
/**
* Return the RHS operand of %-based string literal format expression.
*/
@Nullable
public static PyExpression getFormatValueExpression(@NotNull PyStringLiteralExpression element) {
final PsiElement parent = element.getParent();
if (parent instanceof PyBinaryExpression) {
final PyBinaryExpression binaryExpr = (PyBinaryExpression)parent;
if (binaryExpr.isOperator("%")) {
PyExpression expr = binaryExpr.getRightExpression();
while (expr instanceof PyParenthesizedExpression) {
expr = ((PyParenthesizedExpression)expr).getContainedExpression();
}
return expr;
}
}
return null;
}
/**
* Return the argument list of the str.format() literal format expression.
*/
@Nullable
public static PyArgumentList getNewStyleFormatValueExpression(@NotNull PyStringLiteralExpression element) {
final PsiElement parent = element.getParent();
if (parent instanceof PyQualifiedExpression) {
final PyQualifiedExpression qualifiedExpr = (PyQualifiedExpression)parent;
final String name = qualifiedExpr.getReferencedName();
if ("format".equals(name)) {
final PsiElement parent2 = qualifiedExpr.getParent();
if (parent2 instanceof PyCallExpression) {
final PyCallExpression callExpr = (PyCallExpression)parent2;
return callExpr.getArgumentList();
}
}
}
return null;
}
@NotNull
public static List<TextRange> getEscapeRanges(@NotNull String s) {
final List<TextRange> ranges = new ArrayList<TextRange>();
Matcher matcher = PyStringLiteralExpressionImpl.PATTERN_ESCAPE.matcher(s);
while (matcher.find()) {
ranges.add(TextRange.create(matcher.start(), matcher.end()));
}
return ranges;
}
}
@@ -25,7 +25,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PyStringLiteralExpressionImpl extends PyElementImpl implements PyStringLiteralExpression, RegExpLanguageHost {
private static final Pattern PATTERN_ESCAPE = Pattern
public static final Pattern PATTERN_ESCAPE = Pattern
.compile("\\\\(\n|\\\\|'|\"|a|b|f|n|r|t|v|([0-7]{1,3})|x([0-9a-fA-F]{1,2})" + "|N(\\{.*?\\})|u([0-9a-fA-F]){4}|U([0-9a-fA-F]{8}))");
private static final Map<String, String> escapeMap = initializeEscapeMap();
private String stringValue;
@@ -14,7 +14,6 @@ import com.intellij.util.Processor;
import com.intellij.util.containers.HashSet;
import com.jetbrains.python.findUsages.PyFindUsagesHandlerFactory;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -43,7 +42,7 @@ public class PyRefactoringUtil {
return;
}
if (element instanceof PyStringLiteralExpression) {
final Pair<PsiElement, TextRange> selection = pattern.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE);
final Pair<PsiElement, TextRange> selection = pattern.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (selection != null) {
final String substring = selection.getSecond().substring(pattern.getText());
final PyStringLiteralExpression expr = (PyStringLiteralExpression)element;
@@ -51,7 +50,7 @@ public class PyRefactoringUtil {
if (text != null && expr.getStringNodes().size() == 1) {
final int start = text.indexOf(substring);
if (start >= 0) {
element.putUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE, Pair.create(element, TextRange.from(start, substring.length())));
element.putUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE, Pair.create(element, TextRange.from(start, substring.length())));
occurrences.add(element);
return;
}
@@ -113,7 +112,7 @@ public class PyRefactoringUtil {
return null;
}
expression.putUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE, Pair.create(parent, textRange));
expression.putUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE, Pair.create(parent, textRange));
return expression;
}
return null;
@@ -1,9 +1,20 @@
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.psi.PsiElement;
import com.intellij.psi.tree.IElementType;
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.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static com.jetbrains.python.PyTokenTypes.*;
@@ -11,6 +22,8 @@ import static com.jetbrains.python.PyTokenTypes.*;
* @author Dennis.Ushakov
*/
public class PyReplaceExpressionUtil implements PyElementTypes {
public static final Key<Pair<PsiElement, TextRange>> SELECTION_BREAKS_AST_NODE =
new Key<Pair<PsiElement, TextRange>>("python.selection.breaks.ast.node");
private PyReplaceExpressionUtil() {}
@@ -37,6 +50,144 @@ public class PyReplaceExpressionUtil implements PyElementTypes {
return false;
}
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 String prefix = fullText.substring(0, textRange.getStartOffset());
final String suffix = fullText.substring(textRange.getEndOffset(), oldExpression.getTextLength());
final PyExpression valueExpression = PyStringFormatParser.getFormatValueExpression(oldExpression);
final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject());
final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression);
final List<PyStringFormatParser.SubstitutionChunk> substitutions = new PyStringFormatParser(fullText).parseSubstitutions();
if (valueExpression instanceof PyTupleExpression && !containsStringFormatting(fullText, textRange)) {
// 'foo%s' % (x,) -> '%s%s' % (s, x)
// TODO: Support dict literals and dict() function
// TODO: It is possible to resolve to a tuple or dict literal and modify them
final String newLiteralText = prefix + "%s" + suffix;
final PyStringLiteralExpression newLiteralExpression = generator.createStringLiteralAlreadyEscaped(newLiteralText);
oldExpression.replace(newLiteralExpression);
final PyTupleExpression tuple = (PyTupleExpression)valueExpression;
final PyExpression[] members = tuple.getElements();
final List<PyStringFormatParser.SubstitutionChunk> positional = PyStringFormatParser.getPositionalSubstitutions(substitutions);
final int i = getPositionInRanges(PyStringFormatParser.substitutionsToRanges(positional), textRange);
final int n = members.length;
if (n > 0 && i <= n) {
final boolean last = i == n;
final ASTNode trailingComma = PyPsiUtils.getNextComma(members[n - 1].getNode());
if (trailingComma != null) {
tuple.getNode().removeChild(trailingComma);
}
final PyExpression before = last ? null : members[i];
PyUtil.addListNode(tuple, newExpression, before != null ? before.getNode() : null, i == 0 || !last, last, !last);
return newExpression;
}
return null;
}
else if (isConcatFormatting(oldExpression) || substitutions.size() > 0) {
// 'foobar' + 'baz' -> s + 'bar' + 'baz'
// 'foobar%s' -> s + 'bar%s'
// 'f%soobar' % x -> (s + 'bar') % x
final Pair<String, String> detectedQuotes = PythonStringUtil.getQuotes(fullText);
final Pair<String, String> quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'");
final String leftQuote = quotes.getFirst();
final String rightQuote = quotes.getSecond();
final StringBuilder builder = new StringBuilder();
if (valueExpression != null) {
builder.append("(");
}
if (!leftQuote.endsWith(prefix)) {
builder.append(prefix + rightQuote + " + ");
}
final int pos = builder.toString().length();
builder.append(newExpression.getText());
if (!rightQuote.startsWith(suffix)) {
builder.append(" + " + leftQuote + suffix);
}
if (valueExpression != null) {
builder.append(")");
}
final PsiElement expression = generator.createExpressionFromText(languageLevel, builder.toString());
final PsiElement newElement = oldExpression.replace(expression);
return newElement.findElementAt(pos);
}
else {
// 'foobar' -> '%sbar' % s
// TODO: Handle extracting substring from a string with new-style formatting
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(newExpression.getText());
if (parensNeeded) {
builder.append(")");
}
final PyExpression expression = generator.createExpressionFromText(languageLevel, builder.toString());
final PsiElement newElement = oldExpression.replace(expression);
return newElement.findElementAt(pos);
}
}
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 String s, @NotNull TextRange range) {
final List<TextRange> ranges = PyStringFormatParser.substitutionsToRanges(new PyStringFormatParser(s).parseSubstitutions());
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 ||
@@ -68,7 +219,7 @@ public class PyReplaceExpressionUtil implements PyElementTypes {
return -priority;
}
@NotNull
@Nullable
private static IElementType getOperationType(@NotNull final PyElement expr) {
if (expr instanceof PyBinaryExpression) return ((PyBinaryExpression)expr).getOperator();
return ((PyPrefixExpression)expr).getOperator();
@@ -41,6 +41,7 @@ import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyFunctionBuilder;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -308,7 +309,7 @@ public class PyExtractMethodUtil {
// replace statements with call
if (callElement != null) {
callElement = PyPsiUtils.replaceExpression(expression, callElement);
callElement = PyReplaceExpressionUtil.replaceExpression(expression, callElement);
}
// Set editor
@@ -397,7 +398,7 @@ public class PyExtractMethodUtil {
}
private static PyFunction insertGeneratedMethod(PsiElement anchor, final PyFunction generatedMethod) {
final Pair<PsiElement, TextRange> data = anchor.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE);
final Pair<PsiElement, TextRange> data = anchor.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (data != null) {
anchor = data.first;
}
@@ -31,14 +31,15 @@ import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonStringUtil;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.inspections.PyStringFormatParser;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.types.PyNoneType;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.TypeEvalContext;
import com.jetbrains.python.refactoring.NameSuggesterUtil;
import com.jetbrains.python.refactoring.PyRefactoringUtil;
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -115,12 +116,12 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
protected PsiElement replaceExpression(PsiElement expression, PyExpression newExpression, IntroduceOperation operation) {
PyExpressionStatement statement = PsiTreeUtil.getParentOfType(expression, PyExpressionStatement.class);
if (statement != null) {
if (statement.getExpression() == expression && expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE) == null) {
if (statement.getExpression() == expression && expression.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE) == null) {
statement.delete();
return null;
}
}
return PyPsiUtils.replaceExpression(expression, newExpression);
return PyReplaceExpressionUtil.replaceExpression(expression, newExpression);
}
private final IntroduceValidator myValidator;
@@ -172,7 +173,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
}
};
String text = expression.getText();
final Pair<PsiElement, TextRange> selection = expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE);
final Pair<PsiElement, TextRange> selection = expression.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (selection != null) {
text = selection.getSecond().substring(text);
}
@@ -271,10 +272,8 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
}
if (singleElementSelection && element1 instanceof PyStringLiteralExpression) {
// TODO: Protect against escapes
// TODO: Protect against substrings with format characters
// TODO: Handle extracting substring from a string with formatting
final PyStringLiteralExpression literal = (PyStringLiteralExpression)element1;
// Currently introduce for substrings of a multi-part string literals is not supported
if (literal.getStringNodes().size() > 1) {
showCannotPerformError(project, editor);
return;
@@ -286,7 +285,13 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
final TextRange innerRange = literal.getStringValueTextRange();
final TextRange intersection = selectionRange.shiftRight(-offset).intersection(innerRange);
final TextRange finalRange = intersection != null ? intersection : selectionRange;
element1.putUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE, Pair.create(element1, finalRange));
final String text = literal.getText();
// TODO: Protect against substrings with new-style format characters
if (breaksStringFormatting(text, finalRange) || breaksStringEscaping(text, finalRange)) {
showCannotPerformError(project, editor);
return;
}
element1.putUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE, Pair.create(element1, finalRange));
}
}
@@ -297,6 +302,26 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
performActionOnElement(operation);
}
private boolean breaksStringFormatting(@NotNull String s, @NotNull TextRange range) {
return breaksRanges(PyStringFormatParser.substitutionsToRanges(new PyStringFormatParser(s).parseSubstitutions()), range);
}
private boolean breaksStringEscaping(@NotNull String s, @NotNull TextRange range) {
return breaksRanges(PyStringFormatParser.getEscapeRanges(s), range);
}
private boolean breaksRanges(@NotNull List<TextRange> ranges, @NotNull TextRange range) {
for (TextRange r : ranges) {
if (range.contains(r)) {
continue;
}
if (range.intersectsStrict(r)) {
return true;
}
}
return false;
}
private void showCannotPerformError(Project project, Editor editor) {
CommonRefactoringUtil.showErrorHint(project, editor, PyBundle.message("refactoring.introduce.selection.error"), myDialogTitle,
"refactoring.extractMethod");
@@ -473,7 +498,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
@Override
public void visitPyStringLiteralExpression(PyStringLiteralExpression node) {
final Pair<PsiElement, TextRange> data = node.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE);
final Pair<PsiElement, TextRange> data = node.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (data != null) {
final PsiElement parent = data.getFirst();
final String text = parent.getText();
@@ -566,7 +591,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
@Nullable
public PsiElement addDeclaration(IntroduceOperation operation, PsiElement declaration) {
final PsiElement expression = operation.getInitializer();
final Pair<PsiElement, TextRange> data = expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE);
final Pair<PsiElement, TextRange> data = expression.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
if (data == null) {
return addDeclaration(expression, declaration, operation);
}
@@ -12,8 +12,8 @@ import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyExpression;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.refactoring.PyRefactoringUtil;
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -40,8 +40,8 @@ public abstract class IntroduceValidator {
public abstract String check(String name, PsiElement psiElement);
public static boolean isDefinedInScope(String name, PsiElement psiElement) {
if (psiElement.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE) != null) {
final Pair<PsiElement,TextRange> data = psiElement.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE);
if (psiElement.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE) != null) {
final Pair<PsiElement,TextRange> data = psiElement.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE);
psiElement = data.first;
}
PsiElement context = PsiTreeUtil.getParentOfType(psiElement, PyFunction.class);
@@ -9,7 +9,7 @@ import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.imports.AddImportHelper;
import com.jetbrains.python.psi.PyExpression;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil;
import com.jetbrains.python.refactoring.introduce.IntroduceHandler;
import com.jetbrains.python.refactoring.introduce.IntroduceOperation;
import org.jetbrains.annotations.NotNull;
@@ -29,7 +29,7 @@ public class PyIntroduceConstantHandler extends IntroduceHandler {
if (PsiTreeUtil.getParentOfType(expression, ScopeOwner.class) instanceof PyFile) {
return super.replaceExpression(expression, newExpression, operation);
}
return PyPsiUtils.replaceExpression(expression, newExpression);
return PyReplaceExpressionUtil.replaceExpression(expression, newExpression);
}
@Override
@@ -25,7 +25,7 @@ import com.jetbrains.python.inspections.quickfix.AddFieldQuickFix;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyFunctionBuilder;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil;
import com.jetbrains.python.refactoring.introduce.IntroduceHandler;
import com.jetbrains.python.refactoring.introduce.IntroduceOperation;
import com.jetbrains.python.refactoring.introduce.variable.PyIntroduceVariableHandler;
@@ -78,7 +78,7 @@ public class PyIntroduceFieldHandler extends IntroduceHandler {
@Override
protected PsiElement replaceExpression(PsiElement expression, PyExpression newExpression, IntroduceOperation operation) {
if (operation.getInitPlace() != InitPlace.SAME_METHOD) {
return PyPsiUtils.replaceExpression(expression, newExpression);
return PyReplaceExpressionUtil.replaceExpression(expression, newExpression);
}
return super.replaceExpression(expression, newExpression, operation);
}
@@ -10,7 +10,7 @@ import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.refactoring.PyReplaceExpressionUtil;
import com.jetbrains.python.refactoring.introduce.IntroduceHandler;
import com.jetbrains.python.refactoring.introduce.IntroduceOperation;
import com.jetbrains.python.refactoring.introduce.variable.VariableValidator;
@@ -56,7 +56,7 @@ public class PyIntroduceParameterHandler extends IntroduceHandler {
@Nullable
@Override
protected PsiElement replaceExpression(PsiElement expression, PyExpression newExpression, IntroduceOperation operation) {
return PyPsiUtils.replaceExpression(expression, newExpression);
return PyReplaceExpressionUtil.replaceExpression(expression, newExpression);
}
protected boolean isValidIntroduceContext(PsiElement element) {
@@ -1,2 +1,2 @@
a = b'bar'
b'foo' + a + b'baz'
b'foo' + a + b'baz' + suffix
@@ -1 +1 @@
b'foo<selection>bar</selection>baz'
b'foo<selection>bar</selection>baz' + suffix
@@ -1,2 +1,2 @@
a = "hello"
print(a + " world")
print(a + " world" + "!")
@@ -1 +1 @@
print(<selection>"hello</selection> world")
print(<selection>"hello</selection> world" + "!")
@@ -1,2 +1,2 @@
a = "hello"
print(a + " world")
print(a + " world" + "!")
@@ -1 +1 @@
print("<selection>hello</selection> world")
print("<selection>hello</selection> world" + "!")
@@ -1,2 +1,2 @@
a = "lo wor"
print("hel" + a + "ld")
print(prefix + "hel" + a + "ld")
@@ -1 +1 @@
print("hel<selection>lo wor</selection>ld")
print(prefix + "hel<selection>lo wor</selection>ld")
@@ -1,2 +1,2 @@
a = "world"
print("hello " + a)
print("hello " + a + suffix)
@@ -1 +1 @@
print("hello <selection>world</selection>")
print("hello <selection>world</selection>" + suffix)
@@ -0,0 +1,2 @@
a = "hello"
print("%s world" % a)
@@ -0,0 +1 @@
print("<selection>hello</selection> world")
@@ -0,0 +1,2 @@
a = "World"
print("%s: %s %s" % ("Error", "Hello", a))
@@ -0,0 +1 @@
print("%s: %s <selection>World</selection>" % ("Error", "Hello"))
@@ -0,0 +1,2 @@
a = "World"
print("%s: %s %s" % ("Error", "Hello", a))
@@ -0,0 +1 @@
print("%s: %s <selection>World</selection>" % ("Error", "Hello",))
@@ -0,0 +1,2 @@
a = "Hello"
print("%s %s" % (a, "World"))
@@ -0,0 +1 @@
print("<selection>Hello</selection> %s" % ("World",))
@@ -0,0 +1 @@
print(u"Hel<selection>lo \u00d6sterreich\\!\</selection>n")
@@ -0,0 +1 @@
print("Hel<selection>lo %</selection>s!" % "World")
@@ -0,0 +1,2 @@
a = u"lo \u00d6sterreich\\!\n"
print(u"Hel%s\n" % a)
@@ -0,0 +1 @@
print(u"Hel<selection>lo \u00d6sterreich\\!\n</selection>\n")
@@ -0,0 +1,2 @@
a = "lo %s"
print(("Hel" + a + "!") % "World")
@@ -0,0 +1 @@
print("Hel<selection>lo %s</selection>!" % "World")
@@ -1,2 +1,2 @@
a = 'foo'
print((a + 'bar').upper())
print(('%sbar' % a).upper())
@@ -1,2 +1,2 @@
a = "two"
"one " + a + " three"
"one %s three" % a
@@ -1,2 +1,2 @@
a = 'foo'
x = a + 'bar'
x = '%sbar' % a
@@ -0,0 +1,2 @@
a = "Hello"
print("%s: %s %s" % ("Error", a, "World"))
@@ -0,0 +1 @@
print("%s: <selection>Hello</selection> %s" % ("Error", "World"))
@@ -3,4 +3,4 @@ print(""""One two
* """ + a + """
* Four
* Five""")
* Five""" + suffix)
@@ -2,4 +2,4 @@ print(""""One two
* <selection>Three</selection>
* Four
* Five""")
* Five""" + suffix)
@@ -88,6 +88,11 @@ public class PyIntroduceVariableTest extends PyIntroduceTestCase {
doTest();
}
// PY-3654
public void testSimpleSubstring() {
doTest();
}
// PY-3654
public void testLeftSubstring() {
doTest();
@@ -128,11 +133,51 @@ public class PyIntroduceVariableTest extends PyIntroduceTestCase {
doTest();
}
// PY-6354
// PY-3654
public void testBytesSubstring() {
doTest();
}
// PY-3654
public void testSubstringContainsFormatChars() {
doTest();
}
// PY-3654
public void testSubstringBreaksFormatChars() {
doTestCannotPerform();
}
// PY-3654
public void testSubstringContainsEscapes() {
doTest();
}
// PY-3654
public void testSubstringBreaksEscapes() {
doTestCannotPerform();
}
// PY-3654
public void testSubstringBeforeFormatTuple() {
doTest();
}
// PY-3654
public void testSubstringInsideFormatTuple() {
doTest();
}
// PY-3654
public void testSubstringAfterFormatTuple() {
doTest();
}
// PY-3654
public void testSubstringAfterFormatTupleWithComma() {
doTest();
}
private void doTestCannotPerform() {
boolean thrownExpectedException = false;
try {