diff --git a/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java b/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java index 4b398df5e41a..34981829d251 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java +++ b/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java @@ -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> SELECTION_BREAKS_AST_NODE = - new Key>("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 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 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); diff --git a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java index 89bde2b737be..19d04cd4ab9f 100644 --- a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java +++ b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java @@ -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 myResult = new ArrayList(); + @NotNull private final String myLiteral; + @NotNull private final List myResult = new ArrayList(); 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 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 parseSubstitutions() { List result = new ArrayList(); for (FormatStringChunk chunk : parse()) { @@ -206,4 +224,86 @@ public class PyStringFormatParser { } return result; } + + @NotNull + public static List getPositionalSubstitutions(@NotNull List substitutions) { + final ArrayList result = new ArrayList(); + for (SubstitutionChunk s : substitutions) { + if (s.getMappingKey() == null) { + result.add(s); + } + } + return result; + } + + @NotNull + public static Map getKeywordSubstitutions(@NotNull List substitutions) { + final Map result = new HashMap(); + for (SubstitutionChunk s : substitutions) { + final String key = s.getMappingKey(); + if (key != null) { + result.put(key, s); + } + } + return result; + } + + + @NotNull + public static List substitutionsToRanges(@NotNull List substitutions) { + final List ranges = new ArrayList(); + 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 getEscapeRanges(@NotNull String s) { + final List ranges = new ArrayList(); + Matcher matcher = PyStringLiteralExpressionImpl.PATTERN_ESCAPE.matcher(s); + while (matcher.find()) { + ranges.add(TextRange.create(matcher.start(), matcher.end())); + } + return ranges; + } } diff --git a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java index 05cff9ee52b8..9004e384f896 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java @@ -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 escapeMap = initializeEscapeMap(); private String stringValue; diff --git a/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java b/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java index 03a61fa3817f..e3c9bba6c4dc 100644 --- a/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java +++ b/python/src/com/jetbrains/python/refactoring/PyRefactoringUtil.java @@ -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 selection = pattern.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + final Pair 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; diff --git a/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java b/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java index b4575c196ec1..3e649113d72a 100644 --- a/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java +++ b/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java @@ -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> SELECTION_BREAKS_AST_NODE = + new Key>("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 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 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 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 detectedQuotes = PythonStringUtil.getQuotes(fullText); + final Pair 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 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 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(); diff --git a/python/src/com/jetbrains/python/refactoring/extractmethod/PyExtractMethodUtil.java b/python/src/com/jetbrains/python/refactoring/extractmethod/PyExtractMethodUtil.java index c72833ce298b..b71249e7e40a 100644 --- a/python/src/com/jetbrains/python/refactoring/extractmethod/PyExtractMethodUtil.java +++ b/python/src/com/jetbrains/python/refactoring/extractmethod/PyExtractMethodUtil.java @@ -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 data = anchor.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + final Pair data = anchor.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE); if (data != null) { anchor = data.first; } diff --git a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java index 0e5a53ee7f27..0bf3f0d12e1f 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java @@ -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 selection = expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + final Pair 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 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 data = node.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + final Pair 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 data = expression.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + final Pair data = expression.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE); if (data == null) { return addDeclaration(expression, declaration, operation); } diff --git a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceValidator.java b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceValidator.java index 83b5c339d616..3d57ea6ed8bc 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceValidator.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceValidator.java @@ -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 data = psiElement.getUserData(PyPsiUtils.SELECTION_BREAKS_AST_NODE); + if (psiElement.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE) != null) { + final Pair data = psiElement.getUserData(PyReplaceExpressionUtil.SELECTION_BREAKS_AST_NODE); psiElement = data.first; } PsiElement context = PsiTreeUtil.getParentOfType(psiElement, PyFunction.class); diff --git a/python/src/com/jetbrains/python/refactoring/introduce/constant/PyIntroduceConstantHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/constant/PyIntroduceConstantHandler.java index 54358b4f87c8..2302f6d17659 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/constant/PyIntroduceConstantHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/constant/PyIntroduceConstantHandler.java @@ -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 diff --git a/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java index b15679205eb4..d1d717e048ae 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/field/PyIntroduceFieldHandler.java @@ -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); } diff --git a/python/src/com/jetbrains/python/refactoring/introduce/parameter/PyIntroduceParameterHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/parameter/PyIntroduceParameterHandler.java index 5fb57aea831d..a72dce5be7dd 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/parameter/PyIntroduceParameterHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/parameter/PyIntroduceParameterHandler.java @@ -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) { diff --git a/python/testData/refactoring/introduceVariable/bytesSubstring.after.py b/python/testData/refactoring/introduceVariable/bytesSubstring.after.py index 67a86c8749cf..1c0407336d2c 100644 --- a/python/testData/refactoring/introduceVariable/bytesSubstring.after.py +++ b/python/testData/refactoring/introduceVariable/bytesSubstring.after.py @@ -1,2 +1,2 @@ a = b'bar' -b'foo' + a + b'baz' \ No newline at end of file +b'foo' + a + b'baz' + suffix \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/bytesSubstring.py b/python/testData/refactoring/introduceVariable/bytesSubstring.py index 6f98337f4121..c42b25b2bc0a 100644 --- a/python/testData/refactoring/introduceVariable/bytesSubstring.py +++ b/python/testData/refactoring/introduceVariable/bytesSubstring.py @@ -1 +1 @@ -b'foobarbaz' \ No newline at end of file +b'foobarbaz' + suffix \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py index 66921e9c1aa1..f7f8e40e478f 100644 --- a/python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py +++ b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.after.py @@ -1,2 +1,2 @@ a = "hello" -print(a + " world") +print(a + " world" + "!") diff --git a/python/testData/refactoring/introduceVariable/leftQuoteSubstring.py b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.py index 4c4486bf7ccd..eb9ce68da4cc 100644 --- a/python/testData/refactoring/introduceVariable/leftQuoteSubstring.py +++ b/python/testData/refactoring/introduceVariable/leftQuoteSubstring.py @@ -1 +1 @@ -print("hello world") +print("hello world" + "!") diff --git a/python/testData/refactoring/introduceVariable/leftSubstring.after.py b/python/testData/refactoring/introduceVariable/leftSubstring.after.py index d21668605427..3856fe992a4d 100644 --- a/python/testData/refactoring/introduceVariable/leftSubstring.after.py +++ b/python/testData/refactoring/introduceVariable/leftSubstring.after.py @@ -1,2 +1,2 @@ a = "hello" -print(a + " world") \ No newline at end of file +print(a + " world" + "!") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/leftSubstring.py b/python/testData/refactoring/introduceVariable/leftSubstring.py index e981bd05c293..eefe21e9632c 100644 --- a/python/testData/refactoring/introduceVariable/leftSubstring.py +++ b/python/testData/refactoring/introduceVariable/leftSubstring.py @@ -1 +1 @@ -print("hello world") \ No newline at end of file +print("hello world" + "!") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/middleSubstring.after.py b/python/testData/refactoring/introduceVariable/middleSubstring.after.py index 148c9c774e8e..eebdb6e73937 100644 --- a/python/testData/refactoring/introduceVariable/middleSubstring.after.py +++ b/python/testData/refactoring/introduceVariable/middleSubstring.after.py @@ -1,2 +1,2 @@ a = "lo wor" -print("hel" + a + "ld") \ No newline at end of file +print(prefix + "hel" + a + "ld") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/middleSubstring.py b/python/testData/refactoring/introduceVariable/middleSubstring.py index 938db7bd1c1e..e6aad1276d2b 100644 --- a/python/testData/refactoring/introduceVariable/middleSubstring.py +++ b/python/testData/refactoring/introduceVariable/middleSubstring.py @@ -1 +1 @@ -print("hello world") \ No newline at end of file +print(prefix + "hello world") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/rightSubstring.after.py b/python/testData/refactoring/introduceVariable/rightSubstring.after.py index fa468b6341c7..13d798896fef 100644 --- a/python/testData/refactoring/introduceVariable/rightSubstring.after.py +++ b/python/testData/refactoring/introduceVariable/rightSubstring.after.py @@ -1,2 +1,2 @@ a = "world" -print("hello " + a) \ No newline at end of file +print("hello " + a + suffix) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/rightSubstring.py b/python/testData/refactoring/introduceVariable/rightSubstring.py index faaede5b7ddb..c748cbdcc8eb 100644 --- a/python/testData/refactoring/introduceVariable/rightSubstring.py +++ b/python/testData/refactoring/introduceVariable/rightSubstring.py @@ -1 +1 @@ -print("hello world") \ No newline at end of file +print("hello world" + suffix) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/simpleSubstring.after.py b/python/testData/refactoring/introduceVariable/simpleSubstring.after.py new file mode 100644 index 000000000000..5b9c0805f1e5 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/simpleSubstring.after.py @@ -0,0 +1,2 @@ +a = "hello" +print("%s world" % a) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/simpleSubstring.py b/python/testData/refactoring/introduceVariable/simpleSubstring.py new file mode 100644 index 000000000000..e981bd05c293 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/simpleSubstring.py @@ -0,0 +1 @@ +print("hello world") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringAfterFormatTuple.after.py b/python/testData/refactoring/introduceVariable/substringAfterFormatTuple.after.py new file mode 100644 index 000000000000..04e73d6ee14b --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringAfterFormatTuple.after.py @@ -0,0 +1,2 @@ +a = "World" +print("%s: %s %s" % ("Error", "Hello", a)) diff --git a/python/testData/refactoring/introduceVariable/substringAfterFormatTuple.py b/python/testData/refactoring/introduceVariable/substringAfterFormatTuple.py new file mode 100644 index 000000000000..6d3934bddc56 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringAfterFormatTuple.py @@ -0,0 +1 @@ +print("%s: %s World" % ("Error", "Hello")) diff --git a/python/testData/refactoring/introduceVariable/substringAfterFormatTupleWithComma.after.py b/python/testData/refactoring/introduceVariable/substringAfterFormatTupleWithComma.after.py new file mode 100644 index 000000000000..04e73d6ee14b --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringAfterFormatTupleWithComma.after.py @@ -0,0 +1,2 @@ +a = "World" +print("%s: %s %s" % ("Error", "Hello", a)) diff --git a/python/testData/refactoring/introduceVariable/substringAfterFormatTupleWithComma.py b/python/testData/refactoring/introduceVariable/substringAfterFormatTupleWithComma.py new file mode 100644 index 000000000000..ca34963a9884 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringAfterFormatTupleWithComma.py @@ -0,0 +1 @@ +print("%s: %s World" % ("Error", "Hello",)) diff --git a/python/testData/refactoring/introduceVariable/substringBeforeFormatTuple.after.py b/python/testData/refactoring/introduceVariable/substringBeforeFormatTuple.after.py new file mode 100644 index 000000000000..aef0b7c8a0ff --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringBeforeFormatTuple.after.py @@ -0,0 +1,2 @@ +a = "Hello" +print("%s %s" % (a, "World")) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringBeforeFormatTuple.py b/python/testData/refactoring/introduceVariable/substringBeforeFormatTuple.py new file mode 100644 index 000000000000..b70eecff3c9d --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringBeforeFormatTuple.py @@ -0,0 +1 @@ +print("Hello %s" % ("World",)) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringBreaksEscapes.py b/python/testData/refactoring/introduceVariable/substringBreaksEscapes.py new file mode 100644 index 000000000000..aa6d091d63cc --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringBreaksEscapes.py @@ -0,0 +1 @@ +print(u"Hello \u00d6sterreich\\!\n") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringBreaksFormatChars.py b/python/testData/refactoring/introduceVariable/substringBreaksFormatChars.py new file mode 100644 index 000000000000..591363105aa2 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringBreaksFormatChars.py @@ -0,0 +1 @@ +print("Hello %s!" % "World") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringContainsEscapes.after.py b/python/testData/refactoring/introduceVariable/substringContainsEscapes.after.py new file mode 100644 index 000000000000..b882c8770bc1 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringContainsEscapes.after.py @@ -0,0 +1,2 @@ +a = u"lo \u00d6sterreich\\!\n" +print(u"Hel%s\n" % a) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringContainsEscapes.py b/python/testData/refactoring/introduceVariable/substringContainsEscapes.py new file mode 100644 index 000000000000..51a6db074723 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringContainsEscapes.py @@ -0,0 +1 @@ +print(u"Hello \u00d6sterreich\\!\n\n") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringContainsFormatChars.after.py b/python/testData/refactoring/introduceVariable/substringContainsFormatChars.after.py new file mode 100644 index 000000000000..fbb420860efa --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringContainsFormatChars.after.py @@ -0,0 +1,2 @@ +a = "lo %s" +print(("Hel" + a + "!") % "World") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringContainsFormatChars.py b/python/testData/refactoring/introduceVariable/substringContainsFormatChars.py new file mode 100644 index 000000000000..ab8912594058 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringContainsFormatChars.py @@ -0,0 +1 @@ +print("Hello %s!" % "World") \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInExpression.after.py b/python/testData/refactoring/introduceVariable/substringInExpression.after.py index 3420cf9bf534..1788ab2e4dd5 100644 --- a/python/testData/refactoring/introduceVariable/substringInExpression.after.py +++ b/python/testData/refactoring/introduceVariable/substringInExpression.after.py @@ -1,2 +1,2 @@ a = 'foo' -print((a + 'bar').upper()) \ No newline at end of file +print(('%sbar' % a).upper()) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py b/python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py index c4b048f0ab3d..b49832ddc431 100644 --- a/python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py +++ b/python/testData/refactoring/introduceVariable/substringInExpressionStatement.after.py @@ -1,2 +1,2 @@ a = "two" -"one " + a + " three" \ No newline at end of file +"one %s three" % a \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInStatement.after.py b/python/testData/refactoring/introduceVariable/substringInStatement.after.py index 4027559cab28..86e613e3190e 100644 --- a/python/testData/refactoring/introduceVariable/substringInStatement.after.py +++ b/python/testData/refactoring/introduceVariable/substringInStatement.after.py @@ -1,2 +1,2 @@ a = 'foo' -x = a + 'bar' \ No newline at end of file +x = '%sbar' % a \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInsideFormatTuple.after.py b/python/testData/refactoring/introduceVariable/substringInsideFormatTuple.after.py new file mode 100644 index 000000000000..2aa1c25681d2 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInsideFormatTuple.after.py @@ -0,0 +1,2 @@ +a = "Hello" +print("%s: %s %s" % ("Error", a, "World")) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/substringInsideFormatTuple.py b/python/testData/refactoring/introduceVariable/substringInsideFormatTuple.py new file mode 100644 index 000000000000..9238fd7c8079 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/substringInsideFormatTuple.py @@ -0,0 +1 @@ +print("%s: Hello %s" % ("Error", "World")) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py index 5389de87a910..7234834dec3c 100644 --- a/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py +++ b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.after.py @@ -3,4 +3,4 @@ print(""""One two * """ + a + """ * Four -* Five""") \ No newline at end of file +* Five""" + suffix) \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py index 5972d20c474c..7406944ab5d2 100644 --- a/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py +++ b/python/testData/refactoring/introduceVariable/tripleQuotedSubstring.py @@ -2,4 +2,4 @@ print(""""One two * Three * Four -* Five""") \ No newline at end of file +* Five""" + suffix) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java index 5a28cd82f9f5..7cb8e3613b04 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java @@ -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 {