Files
openide/java/java-psi-api/src/com/intellij/psi/util/PsiLiteralUtil.java
Artemiy Sartakov 9388320e76 ExpressionUtils#findStringLiteralRange: text blocks support
GitOrigin-RevId: b85c4c6e3968196f137010d9273458654ac7fbea
2020-01-16 08:11:51 +00:00

552 lines
18 KiB
Java

// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.psi.util;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiJavaToken;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PsiLiteralUtil {
@NonNls public static final String HEX_PREFIX = "0x";
@NonNls public static final String BIN_PREFIX = "0b";
@NonNls public static final String _2_IN_31 = Long.toString(-1L << 31).substring(1);
@NonNls public static final String _2_IN_63 = Long.toString(-1L << 63).substring(1);
@Nullable
public static Integer parseInteger(String text) {
try {
if (text.startsWith(HEX_PREFIX)) {
// should fit in 32 bits
final long value = parseDigits(text.substring(2), 4, 32);
return Integer.valueOf((int)value);
}
if (text.startsWith(BIN_PREFIX)) {
// should fit in 32 bits
final long value = parseDigits(text.substring(2), 1, 32);
return Integer.valueOf((int)value);
}
if (StringUtil.startsWithChar(text, '0')) {
// should fit in 32 bits
final long value = parseDigits(text, 3, 32);
return Integer.valueOf((int)value);
}
return parseIntegerNoPrefix(text);
}
catch (NumberFormatException e) {
return null;
}
}
@Nullable
public static Integer parseIntegerNoPrefix(String text) {
final long l = Long.parseLong(text, 10);
if (text.equals(_2_IN_31) || l == (long)(int)l) {
return Integer.valueOf((int)l);
}
else {
return null;
}
}
@Nullable
public static Long parseLong(String text) {
if (StringUtil.endsWithChar(text, 'L') || StringUtil.endsWithChar(text, 'l')) {
text = text.substring(0, text.length() - 1);
}
try {
if (text.startsWith(HEX_PREFIX)) {
return parseDigits(text.substring(2), 4, 64);
}
if (text.startsWith(BIN_PREFIX)) {
return parseDigits(text.substring(2), 1, 64);
}
if (StringUtil.startsWithChar(text, '0')) {
// should fit in 64 bits
return parseDigits(text, 3, 64);
}
if (_2_IN_63.equals(text)) return Long.valueOf(-1L << 63);
return Long.valueOf(text, 10);
}
catch (NumberFormatException e) {
return null;
}
}
@Nullable
public static Float parseFloat(String text) {
try {
return Float.valueOf(text);
}
catch (NumberFormatException e) {
return null;
}
}
@Nullable
public static Double parseDouble(String text) {
try {
return Double.valueOf(text);
}
catch (NumberFormatException e) {
return null;
}
}
// convert text to number according to radix specified
// if number is more than maxBits bits long, throws NumberFormatException
public static long parseDigits(final String text, final int bitsInRadix, final int maxBits) throws NumberFormatException {
final int radix = 1 << bitsInRadix;
final int textLength = text.length();
if (textLength == 0) {
throw new NumberFormatException(text);
}
long integer = textLength == 1 ? 0 : Long.parseLong(text.substring(0, textLength - 1), radix);
if ((integer & (-1L << (maxBits - bitsInRadix))) != 0) {
throw new NumberFormatException(text);
}
final int lastDigit = Character.digit(text.charAt(textLength - 1), radix);
if (lastDigit == -1) {
throw new NumberFormatException(text);
}
integer <<= bitsInRadix;
integer |= lastDigit;
return integer;
}
/**
* Converts passed character literal (like 'a') to string literal (like "a").
*
* @param charLiteral character literal to convert.
* @return resulting string literal
*/
@NotNull
public static String stringForCharLiteral(@NotNull String charLiteral) {
if ("'\"'".equals(charLiteral)) {
return "\"\\\"\"";
}
else if ("'\\''".equals(charLiteral)) {
return "\"'\"";
}
else {
return '\"' + charLiteral.substring(1, charLiteral.length() - 1) +
'\"';
}
}
/**
* Returns true if given literal expression is invalid and reusing its text representation
* in refactorings/quick-fixes may result in parse errors.
*
* @param expression a literal expression to check
* @return true if the literal text cannot be safely used to build refactored expression
*/
public static boolean isUnsafeLiteral(PsiLiteralExpression expression) {
PsiElement literal = expression.getFirstChild();
assert literal instanceof PsiJavaToken : literal;
IElementType type = ((PsiJavaToken)literal).getTokenType();
return (type == JavaTokenType.CHARACTER_LITERAL || type == JavaTokenType.STRING_LITERAL) && expression.getValue() == null;
}
/**
* Converts given string to text block content.
* String is converted as a last string in a text block.
*
* @param s original text
* @see #escapeTextBlockCharacters(String, boolean, boolean, boolean)
*/
@NotNull
public static String escapeTextBlockCharacters(@NotNull String s) {
return escapeTextBlockCharacters(s, false, true, true);
}
/**
* Converts given string to text block content.
* <p>During conversion:</p>
* <li>All escaped quotes are unescaped.</li>
* <li>Every third quote is escaped. If escapeStartQuote / escapeEndQuote is set then start / end quote is also escaped.</li>
* <li>All spaces before \n are converted to \040 escape sequence.
* This is required since spaces in the end of the line are trimmed by default (see JEP 368).
* If escapeSpacesInTheEnd is set, then all spaces before the end of the line are converted even if new line in the end is missing. </li>
* <li> All new line escape sequences are interpreted. </li>
* <li>Rest of the content is processed as is.</li>
*
* @param s original text
* @param escapeStartQuote true if first quote should be escaped (e.g. when copy-pasting into text block after two quotes)
* @param escapeEndQuote true if last quote should be escaped (e.g. inserting text into text block before closing quotes)
* @param escapeSpacesInTheEnd true if spaces in the end of the line should be converted to \040 even if no new line in the end is present
*/
@NotNull
public static String escapeTextBlockCharacters(@NotNull String s, boolean escapeStartQuote,
boolean escapeEndQuote, boolean escapeSpacesInTheEnd) {
int i = 0;
int length = s.length();
StringBuilder result = new StringBuilder(length);
while (i < length) {
int nextIdx = parseQuotes(i, s, result, escapeStartQuote, escapeEndQuote);
if (nextIdx != -1) {
i = nextIdx;
continue;
}
nextIdx = parseSpaces(i, s, result, escapeSpacesInTheEnd);
if (nextIdx != -1) {
i = nextIdx;
continue;
}
nextIdx = parseBackSlashes(i, s, result);
if (nextIdx != -1) {
i = nextIdx;
continue;
}
result.append(s.charAt(i));
i++;
}
return result.toString();
}
private static int parseQuotes(int start, @NotNull String s, @NotNull StringBuilder result,
boolean escapeStartQuote, boolean escapeEndQuote) {
char c = s.charAt(start);
if (c != '"') return -1;
int nQuotes = 1;
int i = start;
while (true) {
int nextIdx = i + 1 >= s.length() ? -1 : parseBackSlash(s, i + 1);
if (nextIdx == -1) nextIdx = i + 1;
if (nextIdx >= s.length() || s.charAt(nextIdx) != '"') break;
nQuotes++;
i = nextIdx;
}
for (int q = 0; q < nQuotes; q++) {
if (q == 0 && start == 0 && escapeStartQuote ||
q % 3 == 2 ||
q == nQuotes - 1 && i + 1 == s.length() && escapeEndQuote) {
result.append("\\\"");
}
else {
result.append('"');
}
}
return i + 1;
}
private static int parseSpaces(int start, @NotNull String s, @NotNull StringBuilder result, boolean escapeSpacesInTheEnd) {
char c = s.charAt(start);
if (c != ' ') return -1;
int i = start;
int nSpaces = 0;
while (i < s.length() && s.charAt(i) == ' ') {
nSpaces++;
i++;
}
if (i >= s.length() && escapeSpacesInTheEnd) {
result.append(StringUtil.repeat("\\040", nSpaces));
return i;
}
int nextIdx = i >= s.length() ? -1 : parseBackSlash(s, i);
if (nextIdx != -1 && nextIdx < s.length() && s.charAt(nextIdx) == 'n') {
result.append(StringUtil.repeat("\\040", nSpaces));
return i;
}
result.append(StringUtil.repeatSymbol(' ', nSpaces));
return i;
}
private static int parseBackSlashes(int start, @NotNull String s, @NotNull StringBuilder result) {
int i = parseBackSlash(s, start);
if (i == -1) return -1;
int prev = start;
int nextIdx;
int nSlashes = 1;
while (i < s.length()) {
nextIdx = parseBackSlash(s, i);
if (nextIdx != -1) {
result.append(s, prev, i);
prev = i;
i = nextIdx;
nSlashes++;
}
else {
break;
}
}
if (i >= s.length()) {
// line ends with a backslash
result.append(s, prev, s.length());
}
else if (nSlashes % 2 == 0) {
// symbol after slashes is not escaped
result.append(s, prev, i);
}
else {
// found something that is escaped with a backslash
char next = s.charAt(i);
if (next == 'n') {
result.append('\n');
}
else if (next == '"') {
return i;
}
else {
result.append(s, prev, i).append(next);
}
return i + 1;
}
return i;
}
/**
* Escapes backslashes in a text block (even if they're represented as an escape sequence).
*/
@NotNull
public static String escapeBackSlashesInTextBlock(@NotNull String str) {
int i = 0;
int length = str.length();
StringBuilder result = new StringBuilder(length);
while (i < length) {
int nextIdx = parseBackSlash(str, i);
if (nextIdx != -1) {
result.append("\\\\");
i = nextIdx;
}
else {
result.append(str.charAt(i));
i++;
}
}
return result.toString();
}
/**
* Replaces all unescaped quotes with escaped ones.
* If text contains backslash escape sequence it's replaced with a regular backslash.
* The rest of the symbols are left unchanged.
*/
@NotNull
public static String escapeQuotes(@NotNull String str) {
StringBuilder sb = new StringBuilder(str.length());
int nSlashes = 0;
int idx = 0;
while (idx < str.length()) {
char c = str.charAt(idx);
int nextIdx = parseBackSlash(str, idx);
if (nextIdx > 0) {
nSlashes++;
}
else {
if (c == '\"' && nSlashes % 2 == 0) {
sb.append('\\');
}
nSlashes = 0;
nextIdx = idx + 1;
}
sb.append(c);
idx = nextIdx;
}
return sb.toString();
}
private static int parseBackSlash(@NotNull String str, int idx) {
char c = str.charAt(idx);
if (c != '\\') return -1;
int nextIdx = parseEscapedBackSlash(str, idx);
return nextIdx > 0 ? nextIdx : idx + 1;
}
private static int parseEscapedBackSlash(@NotNull String str, int idx) {
int next = idx + 1;
if (next >= str.length() || str.charAt(next) != 'u') return -1;
while (str.charAt(next) == 'u') {
next++;
}
if (next + 3 >= str.length()) return -1;
try {
int code = Integer.parseInt(str.substring(next, next + 4), 16);
if (code == '\\') return next + 4;
}
catch (NumberFormatException ignored) {
}
return -1;
}
/**
* Determines how many whitespaces would be excluded at the beginning of each line of text block content.
* See JEP 368 for more details.
*
* @param lines text block content
*/
public static int getTextBlockIndent(String @NotNull [] lines) {
return getTextBlockIndent(lines, false, false);
}
/**
* @see #getTextBlockIndent(String[])
*/
public static int getTextBlockIndent(String @NotNull [] lines, boolean preserveContent, boolean ignoreLastLine) {
int prefix = Integer.MAX_VALUE;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
int indent = 0;
while (indent < line.length() && Character.isWhitespace(line.charAt(indent))) indent++;
if (indent == line.length() && (i < lines.length - 1 || ignoreLastLine)) {
if (!preserveContent) lines[i] = "";
}
else if (indent < prefix) prefix = indent;
}
return prefix;
}
/**
* Maps the substring range inside Java String literal value back into the source code range.
*
* @param text string literal as present in source code (including quotes)
* @param from start offset inside the represented string
* @param to end offset inside the represented string
* @return the range which represents the corresponding substring inside source representation,
* or null if from/to values are out of bounds.
*/
@Nullable
public static TextRange mapBackStringRange(@NotNull String text, int from, int to) {
if (from > to || to < 0) return null;
if (text.length() < 2 || !text.startsWith("\"") || !text.endsWith("\"")) {
return null;
}
if (text.indexOf('\\') == -1) {
return new TextRange(from + 1, to + 1);
}
text = text.substring(1, text.length() - 1);
int charsSoFar = 0;
int mappedFrom = -1;
for (int i = 0; i != -1; i = getCharEndIndex(text, i)) {
if (charsSoFar == from) {
mappedFrom = i;
}
if (charsSoFar == to) {
// +1 to count open quote
return new TextRange(mappedFrom + 1, i + 1);
}
charsSoFar++;
}
return null;
}
/**
* Maps the substring range inside Java Text Block literal value back into the source code range.
*
* @param indent text block indent
* @return range in source code representation, null when from/to out of bounds or given text block source code representation is invalid
*/
@Nullable
public static TextRange mapBackTextBlockRange(@NotNull String text, int from, int to, int indent) {
if (from > to || to < 0) return null;
TextBlockModel model = TextBlockModel.create(text, indent);
if (model == null) return null;
return model.mapTextBlockRangeBack(from, to);
}
private static int getCharEndIndex(@NotNull String line, int i) {
if (i >= line.length()) return -1;
char c = line.charAt(i++);
if (c == '\\') {
// like \u0020
char c1 = line.charAt(i++);
if (c1 == 'u') {
while (i < line.length() && line.charAt(i) == 'u') i++;
i += 4;
} else if (c1 >= '0' && c1 <= '7') { // octal escape
char c2 = i < line.length() ? line.charAt(i) : 0;
if (c2 >= '0' && c2 <= '7') {
i++;
char c3 = i < line.length() ? line.charAt(i) : 0;
if (c3 >= '0' && c3 <= '7' && c1 <= '3') {
i++;
}
}
}
}
return i;
}
private static class TextBlockModel {
private final String[] lines;
private final int indent;
private final int startPrefixLength;
private TextBlockModel(String[] lines, int indent, int startPrefixLength) {
this.lines = lines;
this.indent = indent;
this.startPrefixLength = startPrefixLength;
}
@Nullable
private TextRange mapTextBlockRangeBack(int from, int to) {
int curOffset = startPrefixLength;
int charsSoFar = 0;
int mappedFrom = -1;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
int linePrefixLength = findLinePrefixLength(line, indent);
line = line.substring(linePrefixLength);
boolean isLastLine = i == lines.length - 1;
int lineSuffixLength = findLineSuffixLength(line, isLastLine);
line = line.substring(0, line.length() - lineSuffixLength);
if (!isLastLine) line += '\n';
curOffset += linePrefixLength;
int charIdx;
int nextIdx = 0;
while (true) {
if (from == charsSoFar) {
mappedFrom = curOffset + nextIdx;
}
if (to == charsSoFar) {
return new TextRange(mappedFrom, curOffset + nextIdx);
}
charIdx = nextIdx;
nextIdx = getCharEndIndex(line, charIdx);
if (nextIdx == -1) break;
charsSoFar++;
if (nextIdx == line.length()) curOffset += lineSuffixLength;
}
curOffset += line.length();
}
return null;
}
private static int findLinePrefixLength(@NotNull String line, int indent) {
boolean isBlankLine = line.chars().allMatch(Character::isWhitespace);
return isBlankLine ? line.length() : indent;
}
private static int findLineSuffixLength(@NotNull String line, boolean isLastLine) {
if (isLastLine) return 0;
int lastIdx = line.length() - 1;
for (int i = lastIdx; i >= 0; i--) if (line.charAt(i) != ' ') return lastIdx - i;
return 0;
}
@Nullable
private static TextBlockModel create(@NotNull String text, int indent) {
if (text.length() < 7 || !text.startsWith("\"\"\"") || !text.endsWith("\"\"\"")) return null;
int startPrefixLength = findStartPrefixLength(text);
if (startPrefixLength == -1) return null;
String[] lines = text.substring(startPrefixLength, text.length() - 3).split("\n", -1);
return new TextBlockModel(lines, indent, startPrefixLength);
}
@Contract(pure = true)
private static int findStartPrefixLength(@NotNull String text) {
int lineBreakIdx = text.indexOf("\n");
if (lineBreakIdx == -1) return -1;
return lineBreakIdx + 1;
}
}
}