From 0d34a0e88ffdeaabc435d07f8f7e2de3d7998cc3 Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Tue, 26 Sep 2023 11:27:36 +0300 Subject: [PATCH] PY-59594 PEP 701: Make the intention for flipping string quotes aware of the syntax (cherry picked from commit a62f78cfdee24eccf3fcf2d341807565f26d2fbd) IJ-MR-115763 GitOrigin-RevId: 059a289a9006d592a88a677137647c0660c68fc2 --- .../jetbrains/python/psi/PyQuotesUtil.java | 11 ++++-- ...ForFStringContainingTripleQuotedString.py} | 0 ...ntainingQuotesOfParentFStringBefore312.py} | 0 ...sideFStringWithOppositeQuotesBefore312.py} | 0 ...fFStringContainingOtherStringsBefore312.py | 1 + ...ngContainingOtherStringsBefore312_after.py | 1 + ...esOfFStringContainingOtherStrings_after.py | 2 +- ...dFStringContainingOtherStringsBefore312.py | 3 ++ ...ngContainingOtherStringsBefore312_after.py | 3 ++ ...luedFStringContainingOtherStrings_after.py | 6 ++-- ...OfStringContainingQuotesOfParentFString.py | 1 + ...ngContainingQuotesOfParentFString_after.py | 1 + ...OfStringInsideFStringWithOppositeQuotes.py | 1 + ...ngInsideFStringWithOppositeQuotes_after.py | 1 + .../convertingRawFStringQuotesBefore312.py | 1 + ...nvertingRawFStringQuotesBefore312_after.py | 1 + .../convertingRawFStringQuotes_after.py | 2 +- .../python/intentions/PyIntentionTest.java | 35 ++++++++++++++++--- 18 files changed, 58 insertions(+), 12 deletions(-) rename python/testData/intentions/{convertingQuotesNotSuggestedForFStringContainingStringWithInconvertibleQuotes.py => convertingQuotesNotSuggestedForFStringContainingTripleQuotedString.py} (100%) rename python/testData/intentions/{convertingQuotesNotSuggestedForStringInsideFStringThatWouldRequireEscapingInsideFragment.py => convertingQuotesNotSuggestedForStringContainingQuotesOfParentFStringBefore312.py} (100%) rename python/testData/intentions/{convertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotes.py => convertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotesBefore312.py} (100%) create mode 100644 python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312.py create mode 100644 python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312_after.py create mode 100644 python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312.py create mode 100644 python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312_after.py create mode 100644 python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString.py create mode 100644 python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString_after.py create mode 100644 python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes.py create mode 100644 python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes_after.py create mode 100644 python/testData/intentions/convertingRawFStringQuotesBefore312.py create mode 100644 python/testData/intentions/convertingRawFStringQuotesBefore312_after.py diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/PyQuotesUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/PyQuotesUtil.java index 9a59dfbe1f51..696b9cac52f6 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/PyQuotesUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/PyQuotesUtil.java @@ -11,6 +11,7 @@ import java.util.Collection; import static com.jetbrains.python.psi.PyUtil.as; public final class PyQuotesUtil { + private PyQuotesUtil() { } @@ -20,7 +21,7 @@ public final class PyQuotesUtil { PyFormattedStringElement parentFString = PsiTreeUtil.getParentOfType(stringElement, PyFormattedStringElement.class, true, PyStatement.class); char targetQuote = PyStringLiteralUtil.flipQuote(stringElement.getQuote().charAt(0)); - if (parentFString != null) { + if (parentFString != null && LanguageLevel.forElement(stringElement).isOlderThan(LanguageLevel.PYTHON312)) { boolean parentFStringUsesTargetQuotes = parentFString.getQuote().equals(Character.toString(targetQuote)); if (parentFStringUsesTargetQuotes) return false; boolean conversionIntroducesBackslashEscapedQuote = stringElement.textContains(targetQuote); @@ -51,11 +52,17 @@ public final class PyQuotesUtil { processStringElementText(builder, stringElement.getText(), originalQuote); } else { + boolean fStringCanIncludeArbitraryStringLiterals = LanguageLevel.forElement(stringElement).isAtLeast(LanguageLevel.PYTHON312); stringElement.acceptChildren(new PyRecursiveElementVisitor() { @Override public void visitElement(@NotNull PsiElement element) { if (element instanceof PyStringElement) { - processStringElement(builder, (PyStringElement)element); + if (fStringCanIncludeArbitraryStringLiterals) { + builder.append(element.getText()); + } + else { + processStringElement(builder, (PyStringElement)element); + } } else if (PyTokenTypes.FSTRING_TOKENS.contains(element.getNode().getElementType())) { processStringElementText(builder, element.getText(), originalQuote); diff --git a/python/testData/intentions/convertingQuotesNotSuggestedForFStringContainingStringWithInconvertibleQuotes.py b/python/testData/intentions/convertingQuotesNotSuggestedForFStringContainingTripleQuotedString.py similarity index 100% rename from python/testData/intentions/convertingQuotesNotSuggestedForFStringContainingStringWithInconvertibleQuotes.py rename to python/testData/intentions/convertingQuotesNotSuggestedForFStringContainingTripleQuotedString.py diff --git a/python/testData/intentions/convertingQuotesNotSuggestedForStringInsideFStringThatWouldRequireEscapingInsideFragment.py b/python/testData/intentions/convertingQuotesNotSuggestedForStringContainingQuotesOfParentFStringBefore312.py similarity index 100% rename from python/testData/intentions/convertingQuotesNotSuggestedForStringInsideFStringThatWouldRequireEscapingInsideFragment.py rename to python/testData/intentions/convertingQuotesNotSuggestedForStringContainingQuotesOfParentFStringBefore312.py diff --git a/python/testData/intentions/convertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotes.py b/python/testData/intentions/convertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotesBefore312.py similarity index 100% rename from python/testData/intentions/convertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotes.py rename to python/testData/intentions/convertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotesBefore312.py diff --git a/python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312.py b/python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312.py new file mode 100644 index 000000000000..1bf51526f406 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312.py @@ -0,0 +1 @@ +s = f"foo{'bar'}baz" diff --git a/python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312_after.py b/python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312_after.py new file mode 100644 index 000000000000..dc0aa47f2a12 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfFStringContainingOtherStringsBefore312_after.py @@ -0,0 +1 @@ +s = f'foo{"bar"}baz' diff --git a/python/testData/intentions/convertingQuotesOfFStringContainingOtherStrings_after.py b/python/testData/intentions/convertingQuotesOfFStringContainingOtherStrings_after.py index dc0aa47f2a12..dd15701dfad8 100644 --- a/python/testData/intentions/convertingQuotesOfFStringContainingOtherStrings_after.py +++ b/python/testData/intentions/convertingQuotesOfFStringContainingOtherStrings_after.py @@ -1 +1 @@ -s = f'foo{"bar"}baz' +s = f'foo{'bar'}baz' diff --git a/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312.py b/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312.py new file mode 100644 index 000000000000..482221e350c1 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312.py @@ -0,0 +1,3 @@ +s = (f'{"foo"}' + f'{"bar"}' + f'{"baz"}') diff --git a/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312_after.py b/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312_after.py new file mode 100644 index 000000000000..6cd016321aa5 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStringsBefore312_after.py @@ -0,0 +1,3 @@ +s = (f"{'foo'}" + f"{'bar'}" + f"{'baz'}") diff --git a/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStrings_after.py b/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStrings_after.py index 6cd016321aa5..e26a0ceedf2e 100644 --- a/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStrings_after.py +++ b/python/testData/intentions/convertingQuotesOfGluedFStringContainingOtherStrings_after.py @@ -1,3 +1,3 @@ -s = (f"{'foo'}" - f"{'bar'}" - f"{'baz'}") +s = (f"{"foo"}" + f"{"bar"}" + f"{"baz"}") diff --git a/python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString.py b/python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString.py new file mode 100644 index 000000000000..4b29c9bc849d --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString.py @@ -0,0 +1 @@ +s = f"""foo{'ba"r'}baz""" diff --git a/python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString_after.py b/python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString_after.py new file mode 100644 index 000000000000..728b02b5e913 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfStringContainingQuotesOfParentFString_after.py @@ -0,0 +1 @@ +s = f"""foo{"ba\"r"}baz""" diff --git a/python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes.py b/python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes.py new file mode 100644 index 000000000000..a9195ac9457c --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes.py @@ -0,0 +1 @@ +s = f"foo{'bar'}baz" diff --git a/python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes_after.py b/python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes_after.py new file mode 100644 index 000000000000..c522121e2866 --- /dev/null +++ b/python/testData/intentions/convertingQuotesOfStringInsideFStringWithOppositeQuotes_after.py @@ -0,0 +1 @@ +s = f"foo{"bar"}baz" diff --git a/python/testData/intentions/convertingRawFStringQuotesBefore312.py b/python/testData/intentions/convertingRawFStringQuotesBefore312.py new file mode 100644 index 000000000000..90cb3cfb25aa --- /dev/null +++ b/python/testData/intentions/convertingRawFStringQuotesBefore312.py @@ -0,0 +1 @@ +s = rf'foo{"bar"}' diff --git a/python/testData/intentions/convertingRawFStringQuotesBefore312_after.py b/python/testData/intentions/convertingRawFStringQuotesBefore312_after.py new file mode 100644 index 000000000000..f41231ee47aa --- /dev/null +++ b/python/testData/intentions/convertingRawFStringQuotesBefore312_after.py @@ -0,0 +1 @@ +s = rf"foo{'bar'}" diff --git a/python/testData/intentions/convertingRawFStringQuotes_after.py b/python/testData/intentions/convertingRawFStringQuotes_after.py index f41231ee47aa..b5992e514c66 100644 --- a/python/testData/intentions/convertingRawFStringQuotes_after.py +++ b/python/testData/intentions/convertingRawFStringQuotes_after.py @@ -1 +1 @@ -s = rf"foo{'bar'}" +s = rf"foo{"bar"}" diff --git a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java index d456222e2c3a..221f7e7d2c4f 100644 --- a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java +++ b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java @@ -224,22 +224,37 @@ public class PyIntentionTest extends PyTestCase { } // PY-30798 + public void testConvertingRawFStringQuotesBefore312() { + runWithLanguageLevel(LanguageLevel.PYTHON311, () -> doTest(PyPsiBundle.message("INTN.quoted.string.single.to.double"))); + } + + // PY-59594 public void testConvertingRawFStringQuotes() { doTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); } // PY-30798 - public void testConvertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotes() { - doNegativeTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); + public void testConvertingQuotesNotSuggestedForStringInsideFStringWithOppositeQuotesBefore312() { + runWithLanguageLevel(LanguageLevel.PYTHON311, () -> doNegativeTest(PyPsiBundle.message("INTN.quoted.string.single.to.double"))); + } + + // PY-59594 + public void testConvertingQuotesOfStringInsideFStringWithOppositeQuotes() { + doTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); } // PY-30798 - public void testConvertingQuotesNotSuggestedForStringInsideFStringThatWouldRequireEscapingInsideFragment() { - doNegativeTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); + public void testConvertingQuotesNotSuggestedForStringContainingQuotesOfParentFStringBefore312() { + runWithLanguageLevel(LanguageLevel.PYTHON311, () -> doNegativeTest(PyPsiBundle.message("INTN.quoted.string.single.to.double"))); + } + + // PY-59594 + public void testConvertingQuotesOfStringContainingQuotesOfParentFString() { + doTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); } // PY-30798 - public void testConvertingQuotesNotSuggestedForFStringContainingStringWithInconvertibleQuotes() { + public void testConvertingQuotesNotSuggestedForFStringContainingTripleQuotedString() { doNegativeTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); } @@ -249,6 +264,11 @@ public class PyIntentionTest extends PyTestCase { } // PY-30798 + public void testConvertingQuotesOfFStringContainingOtherStringsBefore312() { + runWithLanguageLevel(LanguageLevel.PYTHON311, () -> doTest(PyPsiBundle.message("INTN.quoted.string.double.to.single"))); + } + + // PY-59594 public void testConvertingQuotesOfFStringContainingOtherStrings() { doTest(PyPsiBundle.message("INTN.quoted.string.double.to.single")); } @@ -259,6 +279,11 @@ public class PyIntentionTest extends PyTestCase { } // PY-30798 + public void testConvertingQuotesOfGluedFStringContainingOtherStringsBefore312() { + runWithLanguageLevel(LanguageLevel.PYTHON311, () -> doTest(PyPsiBundle.message("INTN.quoted.string.single.to.double"))); + } + + // PY-59594 public void testConvertingQuotesOfGluedFStringContainingOtherStrings() { doTest(PyPsiBundle.message("INTN.quoted.string.single.to.double")); }