Extracted methods from PythonStringUtil into dedicated util class PyStringLiteralUtil
authorMikhail Golubev <mikhail.golubev@jetbrains.com>
Mon, 10 Oct 2016 14:28:24 +0000 (17:28 +0300)
committerMikhail Golubev <mikhail.golubev@jetbrains.com>
Mon, 10 Oct 2016 14:59:02 +0000 (17:59 +0300)
20 files changed:
python/src/com/jetbrains/python/PyStringLiteralReference.java
python/src/com/jetbrains/python/PythonFoldingBuilder.java
python/src/com/jetbrains/python/PythonStringUtil.java
python/src/com/jetbrains/python/actions/PyFillParagraphHandler.java
python/src/com/jetbrains/python/codeInsight/editorActions/moveUpDown/PyStatementMover.java
python/src/com/jetbrains/python/codeInsight/intentions/ConvertVariadicParamIntention.java
python/src/com/jetbrains/python/codeInsight/intentions/PyStringConcatenationToFormatIntention.java
python/src/com/jetbrains/python/documentation/docstrings/DocStringParameterReference.java
python/src/com/jetbrains/python/documentation/doctest/PyDocstringLanguageInjector.java
python/src/com/jetbrains/python/editor/BaseQuoteHandler.java
python/src/com/jetbrains/python/lexer/PyStringLiteralLexer.java
python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java [new file with mode: 0644]
python/src/com/jetbrains/python/psi/PyUtil.java
python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java
python/src/com/jetbrains/python/psi/impl/PyNamedParameterImpl.java
python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionImpl.java
python/src/com/jetbrains/python/psi/impl/PyStringLiteralExpressionManipulator.java
python/src/com/jetbrains/python/refactoring/PyReplaceExpressionUtil.java
python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java
python/src/com/jetbrains/python/spellchecker/PythonSpellcheckerStrategy.java

index 46fd61466ac9d7d346ded9128a5c5a7e0bb25ee9..524197817f0ff574977448c5e8c2a59a042cab9c 100644 (file)
@@ -18,6 +18,7 @@ package com.jetbrains.python;
 import com.intellij.openapi.util.TextRange;
 import com.intellij.psi.PsiElement;
 import com.jetbrains.python.psi.PyElementGenerator;
+import com.jetbrains.python.psi.PyStringLiteralUtil;
 import com.jetbrains.python.psi.StringLiteralExpression;
 import org.jetbrains.annotations.NotNull;
 
@@ -38,7 +39,7 @@ public abstract class PyStringLiteralReference extends BaseReference {
   @SuppressWarnings("RefusedBequest") // 1 instead of 1 in range and "-1" at the end because we do not need quotes
   @Override
   public final TextRange getRangeInElement() {
-    return PythonStringUtil.getTextRange(myElement);
+    return PyStringLiteralUtil.getTextRange(myElement);
   }
 
   @Override
index f3cffaabf67e621cbbd7737618e05a85177f7651..7978fd5deead3c24a16cb7a233369c0fdd451913 100644 (file)
@@ -223,7 +223,7 @@ public class PythonFoldingBuilder extends CustomFoldingBuilder implements DumbAw
 
   private static String getLanguagePlaceholderForString(PyStringLiteralExpression stringLiteralExpression) {
     String stringText = stringLiteralExpression.getText();
-    Pair<String, String> quotes = PythonStringUtil.getQuotes(stringText);
+    Pair<String, String> quotes = PyStringLiteralUtil.getQuotes(stringText);
     if (quotes != null) {
       return quotes.second + "..." + quotes.second;
     }
index 3f0df093112dddf10afd247d339ef8eb9fea047b..0b4ee0e2540ab8b37b9eaf0892b06920760b6a3c 100644 (file)
  */
 package com.jetbrains.python;
 
-import com.google.common.collect.ImmutableList;
 import com.intellij.openapi.util.Pair;
 import com.intellij.openapi.util.SystemInfo;
 import com.intellij.openapi.util.TextRange;
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.psi.PsiElement;
 import com.intellij.psi.util.QualifiedName;
 import com.intellij.util.ObjectUtils;
 import com.intellij.util.PathUtil;
-import com.jetbrains.python.psi.PyExpression;
-import com.jetbrains.python.psi.PyStringLiteralExpression;
+import com.jetbrains.python.psi.PyStringLiteralUtil;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -34,49 +31,15 @@ import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import static com.intellij.openapi.util.text.StringUtil.endsWith;
-import static com.intellij.openapi.util.text.StringUtil.startsWith;
-
 /**
  * @author Alexei Orischenko
  * @author vlan
  */
 public class PythonStringUtil {
-  private static final ImmutableList<String> QUOTES = ImmutableList.of("'''", "\"\"\"", "'", "\"");
-  /**
-   * Valid string prefix characters (lowercased) as defined in Python lexer.
-   */
-  public static final String PREFIX_CHARACTERS = "ubcrf";
-  
-  /**
-   * Maximum length of a string prefix as defined in Python lexer.
-   */
-  public static final int MAX_PREFIX_LENGTH = 3;
 
   private PythonStringUtil() {
   }
 
-  /**
-   * 'text' => text
-   * "text" => text
-   * text => text
-   * "text => "text
-   *
-   * @return string without heading and trailing pair of ' or "
-   */
-  @NotNull
-  public static String getStringValue(@NotNull String s) {
-    return getStringValueTextRange(s).substring(s);
-  }
-
-
-  public static TextRange getStringValueTextRange(@NotNull String s) {
-    final Pair<String, String> quotes = getQuotes(s);
-    if (quotes != null) {
-      return TextRange.create(quotes.getFirst().length(), s.length() - quotes.getSecond().length());
-    }
-    return TextRange.allOf(s);
-  }
 
   @NotNull
   public static String removeFirstPrefix(@Nullable String s, String separator) {
@@ -159,9 +122,9 @@ public class PythonStringUtil {
   public static String replaceLastSuffix(String s, String separator, String newElementName) {
 
     Pair<String, String> quotes = null;
-    if (isQuoted(s)) {
-      quotes = getQuotes(s);
-      s = stripQuotesAroundValue(s);
+    if (PyStringLiteralUtil.isQuoted(s)) {
+      quotes = PyStringLiteralUtil.getQuotes(s);
+      s = PyStringLiteralUtil.stripQuotesAroundValue(s);
     }
 
     s = removeLastSuffix(s, separator);
@@ -184,112 +147,6 @@ public class PythonStringUtil {
   }
 
 
-  /**
-   * Handles unicode and raw strings
-   *
-   * @param text
-   * @return false if no quotes found, true otherwise
-   *         sdfs -> false
-   *         ur'x' -> true
-   *         "string" -> true
-   */
-
-  public static boolean isQuoted(@Nullable String text) {
-    return text != null && getQuotes(text) != null;
-  }
-
-  /**
-   * 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", ")
-   */
-  @Nullable
-  public static Pair<String, String> getQuotes(@NotNull final String text) {
-    final String prefix = getPrefix(text);
-    final String mainText = text.substring(prefix.length());
-    for (String quote : QUOTES) {
-      final Pair<String, String> quotes = getQuotes(mainText, prefix, quote);
-      if (quotes != null) {
-        return quotes;
-      }
-    }
-    return null;
-  }
-
-  /**
-   * 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}
-   * (case insensitively).  
-   * 
-   * @return end offset of found string prefix
-   */
-  public static int getPrefixEndOffset(@NotNull CharSequence text, int startOffset) {
-    int offset;
-    for (offset = startOffset; offset < Math.min(startOffset + MAX_PREFIX_LENGTH, text.length()); offset++) {
-      if (PREFIX_CHARACTERS.indexOf(Character.toLowerCase(text.charAt(offset))) < 0) {
-        break;
-      }
-    }
-    return offset;
-  }
-
-  @NotNull
-  public static String getPrefix(@NotNull CharSequence text) {
-    return getPrefix(text, 0);
-  }
-
-  /**
-   * Extracts string prefix from the given char sequence using {@link #getPrefixEndOffset(CharSequence, int)}.
-   *
-   * @return extracted string prefix
-   * @see #getPrefixEndOffset(CharSequence, int)
-   */
-  @NotNull
-  public static String getPrefix(@NotNull CharSequence text, int startOffset) {
-    return text.subSequence(startOffset, getPrefixEndOffset(text, startOffset)).toString();
-  }
-
-  /**
-   * @return whether the given prefix contains either 'u' or 'U' character
-   */
-  public static boolean isUnicodePrefix(@NotNull String prefix) {
-    return StringUtil.indexOfIgnoreCase(prefix, 'u', 0) >= 0;
-  }
-
-  /**
-   * @return whether the given prefix contains either 'b' or 'B' character
-   */
-  public static boolean isBytesPrefix(@NotNull String prefix) {
-    return StringUtil.indexOfIgnoreCase(prefix, 'b', 0) >= 0;
-  }
-
-  /**
-   * @return whether the given prefix contains either 'r' or 'R' character
-   */
-  public static boolean isRawPrefix(@NotNull String prefix) {
-    return StringUtil.indexOfIgnoreCase(prefix, 'r', 0) >= 0;
-  }
-
-  /**
-   * @return whether the given prefix contains either 'f' or 'F' character
-   */
-  public static boolean isFormattedPrefix(@NotNull String prefix) {
-    return StringUtil.indexOfIgnoreCase(prefix, 'f', 0) >= 0;
-  }
-
-  @Nullable
-  private static Pair<String, String> getQuotes(@NotNull String text, @NotNull String prefix, @NotNull String quote) {
-    final int length = text.length();
-    final int n = quote.length();
-    if (length >= 2 * n && text.startsWith(quote) && text.endsWith(quote)) {
-      return Pair.create(prefix + text.substring(0, n), text.substring(length - n));
-    }
-    return null;
-  }
-
   @Nullable
   public static String intersect(String fullName, String elementStringValue) {
     QualifiedName fullQName = QualifiedName.fromDottedString(fullName);
@@ -323,58 +180,4 @@ public class PythonStringUtil {
 
     return null;
   }
-
-  public static TextRange getTextRange(PsiElement element) {
-    if (element instanceof PyStringLiteralExpression) {
-      final List<TextRange> ranges = ((PyStringLiteralExpression)element).getStringValueTextRanges();
-      return ranges.get(0);
-    }
-    else {
-      return new TextRange(0, element.getTextLength());
-    }
-  }
-
-  @Nullable
-  public static String getText(@Nullable PyExpression ex) {
-    if (ex == null) {
-      return null;
-    }
-    else {
-      return ex.getText();
-    }
-  }
-
-  @Nullable
-  public static String getStringValue(@Nullable PsiElement o) {
-    if (o == null) {
-      return null;
-    }
-    if (o instanceof PyStringLiteralExpression) {
-      PyStringLiteralExpression literalExpression = (PyStringLiteralExpression)o;
-      return literalExpression.getStringValue();
-    }
-    else {
-      return o.getText();
-    }
-  }
-
-  public static String stripQuotesAroundValue(String text) {
-    Pair<String, String> quotes = getQuotes(text);
-    if (quotes == null) {
-      return text;
-    }
-
-    return text.substring(quotes.first.length(), text.length() - quotes.second.length());
-  }
-
-  public static boolean isRawString(String text) {
-    text = text.toLowerCase();
-    text = StringUtil.trimStart(text, "u");
-    return isStringPrefixedBy(text.toLowerCase(), "r");
-  }
-
-
-  private static boolean isStringPrefixedBy(String text, String prefix) {
-    return (startsWith(text, prefix + "\"") && endsWith(text, "\"")) || (startsWith(text, prefix + "\'") && endsWith(text, "\'"));
-  }
 }
index 53f58865784e22e6f05c25a3806f6efca58d4b68..e347361a0ca4c7f7bdf5696b78ad54932569cc2f 100644 (file)
@@ -24,11 +24,7 @@ import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.PsiWhiteSpace;
 import com.intellij.psi.util.PsiTreeUtil;
-import com.jetbrains.python.PythonStringUtil;
-import com.jetbrains.python.psi.PyDocStringOwner;
-import com.jetbrains.python.psi.PyFile;
-import com.jetbrains.python.psi.PyStatementList;
-import com.jetbrains.python.psi.PyStringLiteralExpression;
+import com.jetbrains.python.psi.*;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -46,7 +42,7 @@ public class PyFillParagraphHandler extends ParagraphFillHandler {
     if (stringLiteralExpression != null) {
       final String text = stringLiteralExpression.getText();
       final Pair<String,String> quotes =
-        PythonStringUtil.getQuotes(text);
+        PyStringLiteralUtil.getQuotes(text);
       final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(stringLiteralExpression, PyDocStringOwner.class);
       if (docStringOwner != null && stringLiteralExpression.equals(docStringOwner.getDocStringExpression())) {
         String indent = getIndent(stringLiteralExpression);
@@ -90,7 +86,7 @@ public class PyFillParagraphHandler extends ParagraphFillHandler {
     if (stringLiteralExpression != null) {
       final String text = stringLiteralExpression.getText();
       final Pair<String,String> quotes =
-        PythonStringUtil.getQuotes(text);
+        PyStringLiteralUtil.getQuotes(text);
       final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(stringLiteralExpression, PyDocStringOwner.class);
       if (docStringOwner != null && stringLiteralExpression.equals(docStringOwner.getDocStringExpression())) {
         String indent = getIndent(stringLiteralExpression);
index fba97072271ab3d7c4e5b0a96dc2066732f835f7..f9eb7311a21d62ab14c11889d86ae9de15c426e6 100644 (file)
@@ -30,7 +30,6 @@ import com.intellij.psi.*;
 import com.intellij.psi.codeStyle.CodeStyleManager;
 import com.intellij.psi.impl.source.PostprocessReformattingAspect;
 import com.intellij.psi.util.PsiTreeUtil;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.psi.*;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -86,7 +85,7 @@ public class PyStatementMover extends LineMover {
     if (nearLine >= document.getLineCount() || nearLine <= 0) return false;
     final PyStringLiteralExpression stringLiteralExpression = PsiTreeUtil.getParentOfType(elementToMove1, PyStringLiteralExpression.class);
     if (stringLiteralExpression != null) {
-      final Pair<String,String> quotes = PythonStringUtil.getQuotes(stringLiteralExpression.getText());
+      final Pair<String,String> quotes = PyStringLiteralUtil.getQuotes(stringLiteralExpression.getText());
       if (quotes != null && (quotes.first.equals("'''") || quotes.first.equals("\"\"\""))) {
         final String text1 = document.getText(TextRange.create(start, end)).trim();
         final String text2 = document.getText(TextRange.create(document.getLineStartOffset(nearLine), document.getLineEndOffset(nearLine))).trim();
index 325b102fe26746eb9d3f8571bad4229c915d4f1e..abbe391d51a4382168a947f4f988a0dcf912212c 100644 (file)
@@ -26,7 +26,6 @@ import com.intellij.util.IncorrectOperationException;
 import com.intellij.util.containers.Stack;
 import com.jetbrains.python.PyBundle;
 import com.jetbrains.python.PyNames;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.psi.*;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -75,7 +74,7 @@ public class ConvertVariadicParamIntention extends BaseIntentionAction {
     if (function != null) {
       for (PyCallExpression call : findKeywordContainerCalls(function)) {
         final PyExpression firstArgument = ArrayUtil.getFirstElement(call.getArguments());
-        final String firstArgumentValue = PythonStringUtil.getStringValue(firstArgument);
+        final String firstArgumentValue = PyStringLiteralUtil.getStringValue(firstArgument);
         if (firstArgumentValue == null || !PyNames.isIdentifierString(firstArgumentValue)) {
           return false;
         }
@@ -83,7 +82,7 @@ public class ConvertVariadicParamIntention extends BaseIntentionAction {
 
       for (PySubscriptionExpression subscription : findKeywordContainerSubscriptions(function)) {
         final PyExpression indexExpression = subscription.getIndexExpression();
-        final String indexValue = PythonStringUtil.getStringValue(indexExpression);
+        final String indexValue = PyStringLiteralUtil.getStringValue(indexExpression);
         if (indexValue == null || !PyNames.isIdentifierString(indexValue)) {
           return false;
         }
index 04d5765970a16138090d3dd6106862f0bebc596b..128301f3ad408976b04582f54c0d984b3d06572f 100644 (file)
@@ -27,7 +27,6 @@ import com.intellij.util.IncorrectOperationException;
 import com.intellij.util.NotNullFunction;
 import com.jetbrains.python.PyBundle;
 import com.jetbrains.python.PyTokenTypes;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.psi.*;
 import com.jetbrains.python.psi.impl.PyBuiltinCache;
 import com.jetbrains.python.psi.types.PyClassTypeImpl;
@@ -148,7 +147,7 @@ public class PyStringConcatenationToFormatIntention extends BaseIntentionAction
           isUnicode = true;
         }
         if (!quotesDetected) {
-          quotes = PythonStringUtil.getQuotes(expression.getText());
+          quotes = PyStringLiteralUtil.getQuotes(expression.getText());
           quotesDetected = true;
         }
         String value = ((PyStringLiteralExpression)expression).getStringValue();
index b3907b9f1f5d15113f04f80a11c0559e9cddb955..4ba9232fba72f5230ce8628eed2147af9059764b 100644 (file)
@@ -27,7 +27,6 @@ import com.intellij.util.ArrayUtil;
 import com.intellij.util.IncorrectOperationException;
 import com.intellij.util.containers.HashSet;
 import com.jetbrains.python.PyNames;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.psi.*;
 import com.jetbrains.python.psi.impl.ParamHelper;
 import com.jetbrains.python.psi.types.TypeEvalContext;
@@ -188,7 +187,7 @@ public class DocStringParameterReference extends PsiReferenceBase<PyStringLitera
   @Override
   public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
     TextRange range = getRangeInElement();
-    Pair<String, String> quotes = PythonStringUtil.getQuotes(range.substring(myElement.getText()));
+    Pair<String, String> quotes = PyStringLiteralUtil.getQuotes(range.substring(myElement.getText()));
 
     if (quotes != null) {
       range = TextRange.create(range.getStartOffset() + quotes.first.length(), range.getEndOffset() - quotes.second.length());
index 82de48ba2f4869feb6b85be7d28421db734611f8..d78512d19dc685879649b88fee381f60a18cd9a8 100644 (file)
@@ -24,10 +24,10 @@ import com.intellij.psi.InjectedLanguagePlaces;
 import com.intellij.psi.LanguageInjector;
 import com.intellij.psi.PsiLanguageInjectionHost;
 import com.intellij.psi.util.PsiTreeUtil;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.documentation.PyDocumentationSettings;
 import com.jetbrains.python.psi.PyDocStringOwner;
 import com.jetbrains.python.psi.PyStringLiteralExpression;
+import com.jetbrains.python.psi.PyStringLiteralUtil;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
@@ -51,7 +51,7 @@ public class PyDocstringLanguageInjector implements LanguageInjector {
       int end = host.getTextLength() - 1;
       final String text = host.getText();
 
-      final Pair<String,String> quotes = PythonStringUtil.getQuotes(text);
+      final Pair<String,String> quotes = PyStringLiteralUtil.getQuotes(text);
       final List<String> strings = StringUtil.split(text, "\n", false);
 
       boolean gotExample = false;
index 5f3248a5fca1fe339ebb35bd48a4db9a7c95e614..4924c80ef859e450f5a4405ce412d3312b5bfc52 100644 (file)
@@ -22,7 +22,7 @@ import com.intellij.openapi.editor.highlighter.HighlighterIterator;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.tree.IElementType;
 import com.intellij.psi.tree.TokenSet;
-import com.jetbrains.python.PythonStringUtil;
+import com.jetbrains.python.psi.PyStringLiteralUtil;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Arrays;
@@ -61,7 +61,7 @@ public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler implements Mult
       }
       if (myLiteralTokenSet.contains(iterator.getTokenType())) {
         int start = iterator.getStart();
-        if (offset - start <= PythonStringUtil.MAX_PREFIX_LENGTH) {
+        if (offset - start <= PyStringLiteralUtil.MAX_PREFIX_LENGTH) {
           if (getLiteralStartOffset(text, start) == offset) return true;
         }
       }
@@ -89,7 +89,7 @@ public class BaseQuoteHandler extends SimpleTokenSetQuoteHandler implements Mult
   }
 
   private static int getLiteralStartOffset(CharSequence text, int start) {
-    return PythonStringUtil.getPrefixEndOffset(text, start);
+    return PyStringLiteralUtil.getPrefixEndOffset(text, start);
   }
 
   @Override
index 944f073a8e67897738ba5ac3e72f05d5ae8e358c..7331466969035c97939dc5a53f9a089ac4f37104 100644 (file)
@@ -21,7 +21,7 @@ import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.StringEscapesTokenTypes;
 import com.intellij.psi.tree.IElementType;
 import com.jetbrains.python.PyTokenTypes;
-import com.jetbrains.python.PythonStringUtil;
+import com.jetbrains.python.psi.PyStringLiteralUtil;
 import org.jetbrains.annotations.NotNull;
 
 /**
@@ -70,10 +70,10 @@ public class PyStringLiteralLexer extends LexerBase {
     myBufferEnd = endOffset;
 
     // the following could be parsing steps if we wanted this info as tokens
-    final String prefix = PythonStringUtil.getPrefix(buffer, myStart);
+    final String prefix = PyStringLiteralUtil.getPrefix(buffer, myStart);
 
-    myIsFormatted = PythonStringUtil.isFormattedPrefix(prefix);
-    myIsRaw = PythonStringUtil.isRawPrefix(prefix);
+    myIsFormatted = PyStringLiteralUtil.isFormattedPrefix(prefix);
+    myIsRaw = PyStringLiteralUtil.isRawPrefix(prefix);
 
     final int quoteOffset = myStart + prefix.length();
     // which quote char?
diff --git a/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java b/python/src/com/jetbrains/python/psi/PyStringLiteralUtil.java
new file mode 100644 (file)
index 0000000..8c6d19b
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2000-2016 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.jetbrains.python.psi;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * @author Mikhail Golubev
+ */
+public class PyStringLiteralUtil {
+  /**
+   * Valid string prefix characters (lowercased) as defined in Python lexer.
+   */
+  public static final String PREFIX_CHARACTERS = "ubcrf";
+  /**
+   * Maximum length of a string prefix as defined in Python lexer.
+   */
+  public static final int MAX_PREFIX_LENGTH = 3;
+  private static final ImmutableList<String> QUOTES = ImmutableList.of("'''", "\"\"\"", "'", "\"");
+
+  private PyStringLiteralUtil() {
+  }
+
+  /**
+   * 'text' => text
+   * "text" => text
+   * text => text
+   * "text => "text
+   *
+   * @return string without heading and trailing pair of ' or "
+   */
+  @NotNull
+  public static String getStringValue(@NotNull String s) {
+    return getStringValueTextRange(s).substring(s);
+  }
+
+  public static TextRange getStringValueTextRange(@NotNull String s) {
+    final Pair<String, String> quotes = getQuotes(s);
+    if (quotes != null) {
+      return TextRange.create(quotes.getFirst().length(), s.length() - quotes.getSecond().length());
+    }
+    return TextRange.allOf(s);
+  }
+
+  /**
+   * Handles unicode and raw strings
+   *
+   * @param text
+   * @return false if no quotes found, true otherwise
+   *         sdfs -> false
+   *         ur'x' -> true
+   *         "string" -> true
+   */
+
+  public static boolean isQuoted(@Nullable String text) {
+    return text != null && getQuotes(text) != null;
+  }
+
+  /**
+   * 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", ")
+   */
+  @Nullable
+  public static Pair<String, String> getQuotes(@NotNull final String text) {
+    final String prefix = getPrefix(text);
+    final String mainText = text.substring(prefix.length());
+    for (String quote : QUOTES) {
+      final Pair<String, String> quotes = getQuotes(mainText, prefix, quote);
+      if (quotes != null) {
+        return quotes;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * 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}
+   * (case insensitively).  
+   * 
+   * @return end offset of found string prefix
+   */
+  public static int getPrefixEndOffset(@NotNull CharSequence text, int startOffset) {
+    int offset;
+    for (offset = startOffset; offset < Math.min(startOffset + MAX_PREFIX_LENGTH, text.length()); offset++) {
+      if (PREFIX_CHARACTERS.indexOf(Character.toLowerCase(text.charAt(offset))) < 0) {
+        break;
+      }
+    }
+    return offset;
+  }
+
+  @NotNull
+  public static String getPrefix(@NotNull CharSequence text) {
+    return getPrefix(text, 0);
+  }
+
+  /**
+   * Extracts string prefix from the given char sequence using {@link #getPrefixEndOffset(CharSequence, int)}.
+   *
+   * @return extracted string prefix
+   * @see #getPrefixEndOffset(CharSequence, int)
+   */
+  @NotNull
+  public static String getPrefix(@NotNull CharSequence text, int startOffset) {
+    return text.subSequence(startOffset, getPrefixEndOffset(text, startOffset)).toString();
+  }
+
+  /**
+   * @return whether the given prefix contains either 'u' or 'U' character
+   */
+  public static boolean isUnicodePrefix(@NotNull String prefix) {
+    return StringUtil.indexOfIgnoreCase(prefix, 'u', 0) >= 0;
+  }
+
+  /**
+   * @return whether the given prefix contains either 'b' or 'B' character
+   */
+  public static boolean isBytesPrefix(@NotNull String prefix) {
+    return StringUtil.indexOfIgnoreCase(prefix, 'b', 0) >= 0;
+  }
+
+  /**
+   * @return whether the given prefix contains either 'r' or 'R' character
+   */
+  public static boolean isRawPrefix(@NotNull String prefix) {
+    return StringUtil.indexOfIgnoreCase(prefix, 'r', 0) >= 0;
+  }
+
+  /**
+   * @return whether the given prefix contains either 'f' or 'F' character
+   */
+  public static boolean isFormattedPrefix(@NotNull String prefix) {
+    return StringUtil.indexOfIgnoreCase(prefix, 'f', 0) >= 0;
+  }
+
+  @Nullable
+  private static Pair<String, String> getQuotes(@NotNull String text, @NotNull String prefix, @NotNull String quote) {
+    final int length = text.length();
+    final int n = quote.length();
+    if (length >= 2 * n && text.startsWith(quote) && text.endsWith(quote)) {
+      return Pair.create(prefix + text.substring(0, n), text.substring(length - n));
+    }
+    return null;
+  }
+
+  public static TextRange getTextRange(PsiElement element) {
+    if (element instanceof PyStringLiteralExpression) {
+      final List<TextRange> ranges = ((PyStringLiteralExpression)element).getStringValueTextRanges();
+      return ranges.get(0);
+    }
+    else {
+      return new TextRange(0, element.getTextLength());
+    }
+  }
+
+  @Nullable
+  public static String getText(@Nullable PyExpression ex) {
+    if (ex == null) {
+      return null;
+    }
+    else {
+      return ex.getText();
+    }
+  }
+
+  @Nullable
+  public static String getStringValue(@Nullable PsiElement o) {
+    if (o == null) {
+      return null;
+    }
+    if (o instanceof PyStringLiteralExpression) {
+      PyStringLiteralExpression literalExpression = (PyStringLiteralExpression)o;
+      return literalExpression.getStringValue();
+    }
+    else {
+      return o.getText();
+    }
+  }
+
+  public static String stripQuotesAroundValue(String text) {
+    Pair<String, String> quotes = getQuotes(text);
+    if (quotes == null) {
+      return text;
+    }
+
+    return text.substring(quotes.first.length(), text.length() - quotes.second.length());
+  }
+}
index cc47620fcd4923b52aa74f7c7ea387c6bb706451..296d1f23ac5e378faeba42956f4790dbe37a3150 100644 (file)
@@ -67,7 +67,6 @@ import com.jetbrains.NotNullPredicate;
 import com.jetbrains.python.PyBundle;
 import com.jetbrains.python.PyNames;
 import com.jetbrains.python.PyTokenTypes;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.codeInsight.completion.OverwriteEqualsInsertHandler;
 import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
 import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
@@ -1970,28 +1969,28 @@ public class PyUtil {
      * @return true if given string node contains "u" or "U" prefix
      */
     public boolean isUnicode() {
-      return PythonStringUtil.isUnicodePrefix(myPrefix);
+      return PyStringLiteralUtil.isUnicodePrefix(myPrefix);
     }
 
     /**
      * @return true if given string node contains "r" or "R" prefix
      */
     public boolean isRaw() {
-      return PythonStringUtil.isRawPrefix(myPrefix);
+      return PyStringLiteralUtil.isRawPrefix(myPrefix);
     }
 
     /**
      * @return true if given string node contains "b" or "B" prefix
      */
     public boolean isBytes() {
-      return PythonStringUtil.isBytesPrefix(myPrefix);
+      return PyStringLiteralUtil.isBytesPrefix(myPrefix);
     }
 
     /**
      * @return true if given string node contains "f" or "F" prefix
      */
     public boolean isFormatted() {
-      return PythonStringUtil.isFormattedPrefix(myPrefix);
+      return PyStringLiteralUtil.isFormattedPrefix(myPrefix);
     }
 
     /**
index 1ca42ed2e6f96e520d134dd8ff4c1a488cd5fa4b..336db0a31f1c5b347be92ed86b6282ff61fab6fe 100644 (file)
@@ -37,7 +37,6 @@ import com.jetbrains.NotNullPredicate;
 import com.jetbrains.python.PyTokenTypes;
 import com.jetbrains.python.PythonFileType;
 import com.jetbrains.python.PythonLanguage;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.psi.*;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -107,7 +106,7 @@ public class PyElementGeneratorImpl extends PyElementGenerator {
   }
 
   public PyStringLiteralExpression createStringLiteral(@NotNull PyStringLiteralExpression oldElement, @NotNull String unescaped) {
-    Pair<String, String> quotes = PythonStringUtil.getQuotes(oldElement.getText());
+    Pair<String, String> quotes = PyStringLiteralUtil.getQuotes(oldElement.getText());
     if (quotes != null) {
       return createStringLiteralAlreadyEscaped(quotes.first + unescaped + quotes.second);
     }
index 646670976fa2206f14e7e9928b9f758531a1a07f..2efd41e93e621882dd2107e6123d7b5c85a43d34 100644 (file)
@@ -172,9 +172,9 @@ public class PyNamedParameterImpl extends PyBaseElementImpl<PyNamedParameterStub
     if (includeDefaultValue && defaultValue != null) {
       String representation = PyUtil.getReadableRepr(defaultValue, true);
       if (defaultValue instanceof PyStringLiteralExpression) {
-        final Pair<String, String> quotes = PythonStringUtil.getQuotes(defaultValue.getText());
+        final Pair<String, String> quotes = PyStringLiteralUtil.getQuotes(defaultValue.getText());
         if (quotes != null) {
-          representation = quotes.getFirst() + PythonStringUtil.getStringValue(defaultValue) + quotes.getSecond();
+          representation = quotes.getFirst() + PyStringLiteralUtil.getStringValue(defaultValue) + quotes.getSecond();
         }
       }
       sb.append("=").append(representation);
index 5f3691f095ad44f7b2323ccd5dd1ebafe2eb33bf..eb42c7a305fff6efc8c7c3f526ef4f3a8426d55e 100644 (file)
@@ -28,7 +28,6 @@ import com.intellij.psi.impl.source.tree.LeafElement;
 import com.intellij.psi.tree.IElementType;
 import com.intellij.psi.util.PsiTreeUtil;
 import com.jetbrains.python.PyTokenTypes;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.codeInsight.regexp.PythonVerboseRegexpLanguage;
 import com.jetbrains.python.lexer.PythonHighlightingLexer;
 import com.jetbrains.python.psi.*;
@@ -133,7 +132,7 @@ public class PyStringLiteralExpressionImpl extends PyElementImpl implements PySt
   }
 
   public static int getPrefixLength(String text) {
-    return PythonStringUtil.getPrefixEndOffset(text, 0);
+    return PyStringLiteralUtil.getPrefixEndOffset(text, 0);
   }
 
   private boolean isUnicodeByDefault() {
@@ -160,8 +159,8 @@ public class PyStringLiteralExpressionImpl extends PyElementImpl implements PySt
         final TextRange textRange = getNodeTextRange(text);
         final int offset = node.getTextRange().getStartOffset() - elementStart + textRange.getStartOffset();
         final String encoded = textRange.substring(text);
-        final boolean hasRawPrefix = PythonStringUtil.isRawPrefix(PythonStringUtil.getPrefix(text));
-        final boolean hasUnicodePrefix = PythonStringUtil.isUnicodePrefix(PythonStringUtil.getPrefix(text));
+        final boolean hasRawPrefix = PyStringLiteralUtil.isRawPrefix(PyStringLiteralUtil.getPrefix(text));
+        final boolean hasUnicodePrefix = PyStringLiteralUtil.isUnicodePrefix(PyStringLiteralUtil.getPrefix(text));
         result.addAll(getDecodedFragments(encoded, offset, hasRawPrefix, unicodeByDefault || hasUnicodePrefix));
       }
       myDecodedFragments = result;
index 5032d128a44db68c0822dd214d796174d2cff827..e37ad5b47e6a9bac8f5a4b8880a6a361575acc54 100644 (file)
@@ -18,8 +18,8 @@ package com.jetbrains.python.psi.impl;
 import com.intellij.openapi.util.Pair;
 import com.intellij.openapi.util.TextRange;
 import com.intellij.psi.AbstractElementManipulator;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.psi.PyElementGenerator;
+import com.jetbrains.python.psi.PyStringLiteralUtil;
 import org.jetbrains.annotations.NotNull;
 
 /**
@@ -29,7 +29,7 @@ public class PyStringLiteralExpressionManipulator extends AbstractElementManipul
 
   @Override
   public PyStringLiteralExpressionImpl handleContentChange(@NotNull PyStringLiteralExpressionImpl element, @NotNull TextRange range, String newContent) {
-    Pair<String, String> quotes = PythonStringUtil.getQuotes(range.substring(element.getText()));
+    Pair<String, String> quotes = PyStringLiteralUtil.getQuotes(range.substring(element.getText()));
 
     if (quotes != null) {
       range = TextRange.create(range.getStartOffset() + quotes.first.length(), range.getEndOffset() - quotes.second.length());
@@ -44,7 +44,7 @@ public class PyStringLiteralExpressionManipulator extends AbstractElementManipul
   @NotNull
   @Override
   public TextRange getRangeInElement(@NotNull PyStringLiteralExpressionImpl element) {
-    Pair<String, String> pair = PythonStringUtil.getQuotes(element.getText());
+    Pair<String, String> pair = PyStringLiteralUtil.getQuotes(element.getText());
     if (pair != null) {
       return TextRange.from(pair.first.length(), element.getTextLength() - pair.first.length() - pair.second.length());
     }
index a7fb0c83dd2a6bd4a1230d4691cfbccec2366475..f6b5dc98d1304697f7b7639e089046050432d107 100644 (file)
@@ -21,9 +21,7 @@ import com.intellij.openapi.util.TextRange;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.tree.IElementType;
-import com.intellij.util.Function;
 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.PyBuiltinCache;
@@ -125,7 +123,7 @@ public class PyReplaceExpressionUtil implements PyElementTypes {
                                                             @NotNull PsiElement newExpression,
                                                             @NotNull TextRange textRange) {
     final String fullText = oldExpression.getText();
-    final Pair<String, String> detectedQuotes = PythonStringUtil.getQuotes(fullText);
+    final Pair<String, String> detectedQuotes = PyStringLiteralUtil.getQuotes(fullText);
     final Pair<String, String> quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'");
     final String prefix = fullText.substring(0, textRange.getStartOffset());
     final String suffix = fullText.substring(textRange.getEndOffset(), oldExpression.getTextLength());
index 7fb1cf1043f1e0e942ef75b3992575b41abd5e63..457458522f3528bce3892857a9167cafb92abe08 100644 (file)
@@ -43,7 +43,6 @@ import com.intellij.refactoring.util.CommonRefactoringUtil;
 import com.jetbrains.python.PyBundle;
 import com.jetbrains.python.PyNames;
 import com.jetbrains.python.PyTokenTypes;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
 import com.jetbrains.python.psi.*;
 import com.jetbrains.python.psi.impl.PyPsiUtils;
@@ -546,7 +545,7 @@ abstract public class IntroduceHandler implements RefactoringActionHandler {
       if (data != null) {
         final PsiElement parent = data.getFirst();
         final String text = parent.getText();
-        final Pair<String, String> detectedQuotes = PythonStringUtil.getQuotes(text);
+        final Pair<String, String> detectedQuotes = PyStringLiteralUtil.getQuotes(text);
         final Pair<String, String> quotes = detectedQuotes != null ? detectedQuotes : Pair.create("'", "'");
         final TextRange range = data.getSecond();
         final String substring = range.substring(text);
index 2cc3ff78a9219266eb99a1397082cd0b99d88596..82b27dadbed5e6ac0611a29c6c4c431a9a168d68 100644 (file)
@@ -27,10 +27,10 @@ import com.intellij.spellchecker.tokenizer.TokenConsumer;
 import com.intellij.spellchecker.tokenizer.Tokenizer;
 import com.intellij.util.containers.ContainerUtil;
 import com.jetbrains.python.PyTokenTypes;
-import com.jetbrains.python.PythonStringUtil;
 import com.jetbrains.python.inspections.PyStringFormatParser;
 import com.jetbrains.python.psi.PyBinaryExpression;
 import com.jetbrains.python.psi.PyStringLiteralExpression;
+import com.jetbrains.python.psi.PyStringLiteralUtil;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
@@ -48,7 +48,7 @@ public class PythonSpellcheckerStrategy extends SpellcheckingStrategy {
       final List<ASTNode> strNodes = element.getStringNodes();
       final List<String> prefixes = ContainerUtil.mapNotNull(strNodes, n -> StringUtil.nullize(new StringNodeInfo(n).getPrefix()));
       
-      if (element.textContains('\\') && !prefixes.stream().anyMatch(PythonStringUtil::isRawPrefix)) {
+      if (element.textContains('\\') && !prefixes.stream().anyMatch(PyStringLiteralUtil::isRawPrefix)) {
         for (Pair<TextRange, String> fragment : element.getDecodedFragments()) {
           final String value = fragment.getSecond();
           final int startOffset = fragment.getFirst().getStartOffset();