diff --git a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java index a99705f04f50..82a56b4323f1 100644 --- a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java +++ b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java @@ -1,12 +1,16 @@ 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; /** @@ -221,6 +225,68 @@ 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; + } + + /** + * 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(); diff --git a/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java b/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java index fef68543392d..1cc2147a55b5 100644 --- a/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java +++ b/python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java @@ -7,10 +7,13 @@ 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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; + import static com.jetbrains.python.PyTokenTypes.*; /** @@ -72,34 +75,68 @@ public class PyReplaceExpressionUtil implements PyElementTypes { @NotNull PsiElement newExpression, @NotNull TextRange textRange) { final String fullText = oldExpression.getText(); - final PyElementGenerator generator = PyElementGenerator.getInstance(oldExpression.getProject()); - final LanguageLevel languageLevel = LanguageLevel.forElement(oldExpression); final String prefix = fullText.substring(0, textRange.getStartOffset()); final String suffix = fullText.substring(textRange.getEndOffset(), oldExpression.getTextLength()); - final Pair detectedQuotes = PythonStringUtil.getQuotes(fullText); - final Pair quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'"); - final PsiElement parent = oldExpression.getParent(); - final boolean parensNeeded = parent instanceof PyExpression && !(parent instanceof PyParenthesizedExpression); - final String leftQuote = quotes.getFirst(); - final String rightQuote = quotes.getSecond(); - final StringBuilder builder = new StringBuilder(); - if (parensNeeded) { - builder.append("("); + 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(); + + // TODO: Handle %-formatted strings + + if (isConcatFormatting(oldExpression) || substitutions.size() > 0) { + // 'foobar' + 'baz' -> s + 'bar' + 'baz' + // 'foobar%s' -> s + 'bar%s' + 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); } - if (!leftQuote.endsWith(prefix)) { - builder.append(prefix + rightQuote + " + "); + else { + // 'foobar' -> '%sbar' % s + 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); } - 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 = oldExpression.replace(expression); - return newElement.findElementAt(pos); + } + + 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) { 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/substringContainsEscapes.after.py b/python/testData/refactoring/introduceVariable/substringContainsEscapes.after.py index b264edbf0264..b882c8770bc1 100644 --- a/python/testData/refactoring/introduceVariable/substringContainsEscapes.after.py +++ b/python/testData/refactoring/introduceVariable/substringContainsEscapes.after.py @@ -1,2 +1,2 @@ a = u"lo \u00d6sterreich\\!\n" -print(u"Hel" + a + u"\n") \ No newline at end of file +print(u"Hel%s\n" % a) \ 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/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 269877156204..7dce38b1fb63 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();