diff --git a/python/src/com/jetbrains/python/codeInsight/fstrings/FStringParser.java b/python/src/com/jetbrains/python/codeInsight/fstrings/FStringParser.java index eb093aeb7d27..ccba619b5832 100644 --- a/python/src/com/jetbrains/python/codeInsight/fstrings/FStringParser.java +++ b/python/src/com/jetbrains/python/codeInsight/fstrings/FStringParser.java @@ -18,7 +18,7 @@ package com.jetbrains.python.codeInsight.fstrings; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.containers.ContainerUtil; -import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; +import com.jetbrains.python.psi.PyStringLiteralUtil; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -44,7 +44,7 @@ public class FStringParser { private FStringParser(@NotNull String nodeText) { myNodeText = nodeText; - myNodeContentRange = PyStringLiteralExpressionImpl.getNodeTextRange(myNodeText); + myNodeContentRange = PyStringLiteralUtil.getContentRange(myNodeText); } private void parseTopLevel() { diff --git a/python/src/com/jetbrains/python/documentation/docstrings/DocStringUtil.java b/python/src/com/jetbrains/python/documentation/docstrings/DocStringUtil.java index fc4d7856ce47..2302a4228f82 100644 --- a/python/src/com/jetbrains/python/documentation/docstrings/DocStringUtil.java +++ b/python/src/com/jetbrains/python/documentation/docstrings/DocStringUtil.java @@ -32,7 +32,6 @@ import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.documentation.PyDocumentationSettings; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyPsiUtils; -import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; import com.jetbrains.python.toolbox.Substring; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -135,7 +134,7 @@ public class DocStringUtil { @NotNull private static Substring stripPrefixAndQuotes(@NotNull String text) { - final TextRange contentRange = PyStringLiteralExpressionImpl.getNodeTextRange(text); + final TextRange contentRange = PyStringLiteralUtil.getContentRange(text); return new Substring(text, contentRange.getStartOffset(), contentRange.getEndOffset()); } diff --git a/python/src/com/jetbrains/python/inspections/PyNewStyleStringFormatParser.java b/python/src/com/jetbrains/python/inspections/PyNewStyleStringFormatParser.java index 878035287edd..296f88481add 100644 --- a/python/src/com/jetbrains/python/inspections/PyNewStyleStringFormatParser.java +++ b/python/src/com/jetbrains/python/inspections/PyNewStyleStringFormatParser.java @@ -17,7 +17,7 @@ package com.jetbrains.python.inspections; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; -import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; +import com.jetbrains.python.psi.PyStringLiteralUtil; import gnu.trove.TIntArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -79,7 +79,7 @@ public class PyNewStyleStringFormatParser { private PyNewStyleStringFormatParser(@NotNull String nodeText) { myNodeText = nodeText; - myNodeContentRange = PyStringLiteralExpressionImpl.getNodeTextRange(nodeText); + myNodeContentRange = PyStringLiteralUtil.getContentRange(nodeText); } private void parseTopLevel() { diff --git a/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java b/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java index ced5cf7c1e1e..18d1a333b0de 100644 --- a/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java +++ b/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java @@ -16,6 +16,7 @@ package com.jetbrains.python.psi; import com.google.common.collect.ImmutableList; +import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; @@ -41,6 +42,8 @@ public class PyStringLiteralUtil { public static final int MAX_PREFIX_LENGTH = 3; private static final ImmutableList QUOTES = ImmutableList.of("'''", "\"\"\"", "'", "\""); + private static final Logger LOG = Logger.getInstance(PyStringLiteralUtil.class); + private PyStringLiteralUtil() { } @@ -89,15 +92,21 @@ public class PyStringLiteralUtil { } /** - * Handles unicode and raw strings - * - * @param text - * @return open and close quote (including raw/unicode prefixes), null if no quotes present in string - * 'string' -> (', ') - * UR"unicode raw string" -> (UR", ") + * Returns a pair where the first element is the prefix combined with the opening quote and the second is the closing quote. + *

+ * If the given string literal is not properly quoted, e.g. the closing quote has fewer quotes as opposed to the + * opening one, or it's missing altogether this method returns null. + *

+ * Examples: + *

+   *   ur"foo" -> ("ur, ")
+   *   ur'bar -> null
+   *   """baz""" -> (""", """)
+   *   '''quux' -> null
+   * 
*/ @Nullable - public static Pair getQuotes(@NotNull final String text) { + public static Pair getQuotes(@NotNull String text) { final String prefix = getPrefix(text); final String mainText = text.substring(prefix.length()); for (String quote : QUOTES) { @@ -109,6 +118,28 @@ public class PyStringLiteralUtil { return null; } + /** + * Returns the range of the string literal text between the opening quote and the closing one. + * If the closing quote is either missing or mismatched, this range spans until the end of the literal. + */ + @NotNull + public static TextRange getContentRange(@NotNull String text) { + LOG.assertTrue(isStringLiteralToken(text), "Text of a single string literal node expected: " + text); + int startOffset = getPrefixLength(text); + int delimiterLength = 1; + final String afterPrefix = text.substring(startOffset); + if (afterPrefix.startsWith("\"\"\"") || afterPrefix.startsWith("'''")) { + delimiterLength = 3; + } + final String delimiter = text.substring(startOffset, startOffset + delimiterLength); + startOffset += delimiterLength; + int endOffset = text.length(); + if (text.substring(startOffset).endsWith(delimiter)) { + endOffset -= delimiterLength; + } + return new TextRange(startOffset, endOffset); + } + /** * Finds the end offset of the string prefix starting from {@code startOffset} in the given char sequence. * String prefix may contain only up to {@link #MAX_PREFIX_LENGTH} characters from {@link #PREFIX_CHARACTERS} diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index 6efc94262edb..5d853bd92484 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -1921,7 +1921,7 @@ public class PyUtil { final String nodeText = node.getText(); final int prefixLength = PyStringLiteralUtil.getPrefixLength(nodeText); myPrefix = nodeText.substring(0, prefixLength); - myContentRange = PyStringLiteralExpressionImpl.getNodeTextRange(nodeText); + myContentRange = PyStringLiteralUtil.getContentRange(nodeText); myQuote = nodeText.substring(prefixLength, myContentRange.getStartOffset()); } diff --git a/python/src/com/jetbrains/python/psi/impl/PyLiteralStringNodeImpl.java b/python/src/com/jetbrains/python/psi/impl/PyLiteralStringNodeImpl.java index 80fa3d7a34b9..81315c556d59 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyLiteralStringNodeImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyLiteralStringNodeImpl.java @@ -55,7 +55,7 @@ public class PyLiteralStringNodeImpl extends LeafPsiElement implements PyLiteral @NotNull @Override public TextRange getContentRange() { - return PyStringLiteralExpressionImpl.getNodeTextRange(getText()); + return PyStringLiteralUtil.getContentRange(getText()); } @NotNull diff --git a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java index 75fe5710619c..d747757598dc 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java @@ -20,7 +20,6 @@ import com.intellij.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.navigation.ItemPresentation; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; @@ -51,7 +50,6 @@ import java.util.EnumSet; import java.util.List; public class PyStringLiteralExpressionImpl extends PyElementImpl implements PyStringLiteralExpression, RegExpLanguageHost, PsiLiteralValue { - private static final Logger LOG = Logger.getInstance(PyStringLiteralExpressionImpl.class); @Nullable private volatile String myStringValue; @Nullable private volatile List myValueTextRanges; @@ -93,24 +91,6 @@ public class PyStringLiteralExpressionImpl extends PyElementImpl implements PySt return result; } - // TODO replace all usages with PyStringLiteralUtil.getStringValue(String) - public static TextRange getNodeTextRange(final String text) { - LOG.assertTrue(PyStringLiteralUtil.isStringLiteralToken(text), "Text of a single string literal node expected: " + text); - int startOffset = PyStringLiteralUtil.getPrefixLength(text); - int delimiterLength = 1; - final String afterPrefix = text.substring(startOffset); - if (afterPrefix.startsWith("\"\"\"") || afterPrefix.startsWith("'''")) { - delimiterLength = 3; - } - final String delimiter = text.substring(startOffset, startOffset + delimiterLength); - startOffset += delimiterLength; - int endOffset = text.length(); - if (text.substring(startOffset).endsWith(delimiter)) { - endOffset -= delimiterLength; - } - return new TextRange(startOffset, endOffset); - } - @Override @NotNull public List> getDecodedFragments() {