TextBlockMigration: support two new escape sequences (IDEA-230758)

GitOrigin-RevId: ab4d06cd4d6c198fcfc94d67e1e758de291648c1
This commit is contained in:
Artemiy Sartakov
2020-01-29 04:45:09 +00:00
committed by intellij-monorepo-bot
parent b32d7fc5ac
commit 5136d5999f
28 changed files with 160 additions and 103 deletions
@@ -24,6 +24,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author ven
@@ -56,9 +57,15 @@ public class ConcatenationToMessageFormatAction implements IntentionAction {
boolean textBlocks = Arrays.stream(concatenation.getOperands())
.anyMatch(operand -> operand instanceof PsiLiteralExpressionImpl &&
((PsiLiteralExpressionImpl)operand).getLiteralElementType() == JavaTokenType.TEXT_BLOCK_LITERAL);
final String expressionText = textBlocks
? "\"\"\"\n" + PsiLiteralUtil.escapeTextBlockCharacters(formatString) + "\"\"\""
: "\"" + StringUtil.escapeStringCharacters(formatString) + "\"";
final String expressionText;
if (textBlocks) {
expressionText = Arrays.stream(formatString.split("\n"))
.map(s -> PsiLiteralUtil.escapeTextBlockCharacters(s))
.collect(Collectors.joining("\n", "\"\"\"\n", "\"\"\""));
}
else {
expressionText = "\"" + StringUtil.escapeStringCharacters(formatString) + "\"";
}
PsiExpression formatArgument = factory.createExpressionFromText(expressionText, null);
argumentList.add(formatArgument);
if (PsiUtil.isLanguageLevel5OrHigher(file)) {
@@ -5,6 +5,7 @@ import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightUtil;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.CodeStyleSettings;
@@ -69,15 +70,85 @@ public class TextBlockBackwardMigrationInspection extends AbstractBaseJavaLocalI
private static String convertToConcatenation(@NotNull String text) {
if (text.isEmpty()) return "\"\"";
StringJoiner joiner = new StringJoiner(" +\n");
String[] lines = text.split("\n", -1);
String[] lines = getTextBlockLines(text).split("\n", -1);
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
boolean addNewLine = i != lines.length - 1;
if (!addNewLine && line.isEmpty()) break;
line = line.replaceAll("\\\\040", " ");
joiner.add("\"" + PsiLiteralUtil.escapeQuotes(line) + (addNewLine ? "\\n\"" : "\""));
joiner.add("\"" + line + (addNewLine ? "\\n\"" : "\""));
}
return joiner.toString();
}
@NotNull
private static String getTextBlockLines(@NotNull String text) {
int length = text.length();
StringBuilder result = new StringBuilder(length);
int i = 0;
while (i < length) {
int nSlashes = 0;
int next = i;
while ((next = PsiLiteralUtil.parseBackSlash(text, next)) != -1) {
nSlashes++;
i = next;
}
next = parseQuote(i, text, nSlashes, result);
if (next != -1) {
i = next;
continue;
}
if (nSlashes != 0) {
i = parseEscapedChar(i, text, nSlashes, result);
}
else {
result.append(text.charAt(i));
i++;
}
}
return result.toString();
}
private static int parseEscapedChar(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) {
if (i > text.length()) {
result.append(StringUtil.repeatSymbol('\\', nSlashes));
return i;
}
int next = parseEscapedSpace(i, text, nSlashes, result);
if (next != -1) return next;
next = parseEscapedLineBreak(i, text, nSlashes, result);
if (next != -1) return next;
result.append(StringUtil.repeatSymbol('\\', nSlashes)).append(text.charAt(i));
return i + 1;
}
private static int parseEscapedSpace(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) {
char c = text.charAt(i);
if (c == 's' && nSlashes % 2 != 0) {
result.append(StringUtil.repeatSymbol('\\', nSlashes - 1)).append(' ');
return i + 1;
}
if (StringUtil.startsWith(text, i, "040") && nSlashes % 2 != 0) {
result.append(StringUtil.repeatSymbol('\\', nSlashes - 1)).append(' ');
return i + 3;
}
return -1;
}
private static int parseEscapedLineBreak(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) {
char c = text.charAt(i);
if (c == '\n' && nSlashes % 2 != 0) {
result.append(StringUtil.repeatSymbol('\\', nSlashes - 1));
return i + 1;
}
return -1;
}
private static int parseQuote(int i, @NotNull String text, int nSlashes, @NotNull StringBuilder result) {
char c = text.charAt(i);
if (c != '"') return -1;
if (nSlashes % 2 == 0) nSlashes++;
result.append(StringUtil.repeatSymbol('\\', nSlashes)).append(c);
return i + 1;
}
}
}
@@ -128,16 +128,8 @@ public class TextBlockMigrationInspection extends AbstractBaseJavaLocalInspectio
}
private static String @NotNull [] getTextBlockLines(String @NotNull [] lines) {
StringBuilder blockLines = new StringBuilder();
boolean escapeStartQuote = false;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
boolean isLastLine = i == lines.length - 1;
line = PsiLiteralUtil.escapeTextBlockCharacters(line, escapeStartQuote, isLastLine, isLastLine);
escapeStartQuote = line.endsWith("\"");
blockLines.append(line);
}
return blockLines.toString().split("(?<=\n)");
String blockLines = PsiLiteralUtil.escapeTextBlockCharacters(StringUtil.join(lines), true, true, true);
return blockLines.split("(?<=\n)");
}
private static String concatenateTextBlockLines(String @NotNull [] lines, int indent) {
@@ -29,9 +29,9 @@ public class PsiConcatenationUtil {
/**
* @deprecated use {@code buildUnescapedFormatString} instead and use either
* {@code com.intellij.openapi.util.text.StringUtil#escapeStringCharacters(java.lang.String)}
* {@link StringUtil#escapeStringCharacters(String)}
* or
* {@code com.intellij.openapi.util.text.StringUtil#escapeTextBlockCharacters(java.lang.String)}
* {@link PsiLiteralUtil#escapeTextBlockCharacters(String)}
* to escape the resulting string.
*/
@Deprecated
@@ -180,7 +180,7 @@ public class PsiLiteralUtil {
* @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
* @param escapeSpacesInTheEnd true if spaces in the end of the line should be preserved even if no new line in the end is present
*/
@NotNull
public static String escapeTextBlockCharacters(@NotNull String s, boolean escapeStartQuote,
@@ -246,12 +246,12 @@ public class PsiLiteralUtil {
i++;
}
if (i >= s.length() && escapeSpacesInTheEnd) {
result.append(StringUtil.repeat("\\040", nSpaces));
result.append(StringUtil.repeat(" ", nSpaces - 1)).append("\\s");
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));
result.append(StringUtil.repeat(" ", nSpaces - 1)).append("\\s");
return i;
}
result.append(StringUtil.repeatSymbol(' ', nSpaces));
@@ -324,35 +324,13 @@ public class PsiLiteralUtil {
}
/**
* 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.
* Parse backslash at given index. It will be parsed even in case when backslash is represented as unicode escape sequence.
*
* @param str text
* @param idx parse from
* @return index where next char starts, -1 otherwise
*/
@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) {
public static int parseBackSlash(@NotNull String str, int idx) {
char c = str.charAt(idx);
if (c != '\\') return -1;
int nextIdx = parseEscapedBackSlash(str, idx);
@@ -3,7 +3,7 @@ class C {
void x(int a, int b) {
//keep me
String s = java.text.MessageFormat.format("""
the text
the text\s
block
line2
{0}{1} "to" be""", a, b);
@@ -2,7 +2,7 @@ class C {
void x(int a, int b) {
String s = """
the text\n block
the text \n block
line2
""" +
a + b + <caret>//keep me
@@ -1,8 +1,8 @@
class C {
String empty = """
<html>
<html> \s
<body>
</body>
</html>
</html> \s
""";
}
@@ -3,8 +3,8 @@
class TextBlockMigration {
void escapeSpaces() {
String block = "text \n" +
"block\n";
String block = "text block \n" +
" ";
}
}
@@ -0,0 +1,8 @@
// "Replace with regular string literal" "true"
class TextBlockMigration {
void oneLineNoNewLine() {
String json = "foobar\n";
}
}
@@ -4,7 +4,8 @@ class TextBlockMigration {
void escapeSequence() {
String oneLineTextBlock = """
\u005c"this is one "line"\n text <caret>block\134\u005c\\"
\u005c"this is one "line"\n\s\
text <caret>block\134\u005c\\"
""";
}
@@ -4,9 +4,9 @@ class TextBlockMigration {
void escapeSpaces() {
String block = """
text\040\040
block<caret>
""";
text\040\040\
block \s<caret>
\s""";
}
}
@@ -0,0 +1,11 @@
// "Replace with regular string literal" "true"
class TextBlockMigration {
void oneLineNoNewLine() {
String json = """
fo<caret>o\
bar
""";
}
}
@@ -6,7 +6,7 @@ class TextBlockMigration {
String string = """
foo
bar
baz\040""".indent(2);
baz\s""".indent(2);
}
}
@@ -4,10 +4,10 @@ class TextBlockMigration {
void concatenationWithExtraSpaces() {
String code = """
<html>\040\040
<html> \s
<body>
</body>
</html>\040\040""";
</html> \s""";
}
}
@@ -7,7 +7,7 @@ class TextBlockMigration {
this concatenation contains
three quotes
one after another
"\"\"""";
""\"""";
}
}
@@ -32,7 +32,7 @@ class JavaTextBlocksHighlightingTest : LightJavaCodeInsightFixtureTestCase() {
fun testPasteNewline() = doTestPaste("\n\n")
fun testPasteMultilineText() = doTestPaste("<html>\n <body>\n </body>\n</html>")
fun testPasteMultilineText() = doTestPaste("<html> \n <body>\n </body>\n</html> ")
fun testBadEscape() = doTestPaste("\\");
@@ -23,6 +23,6 @@ public class TextBlockBackwardMigrationInspectionTest extends LightQuickFixParam
@Override
protected LanguageLevel getLanguageLevel() {
return LanguageLevel.JDK_13_PREVIEW;
return LanguageLevel.JDK_14_PREVIEW;
}
}
@@ -25,6 +25,6 @@ public class TextBlockMigrationInspectionTest extends LightQuickFixParameterized
@Override
protected LanguageLevel getLanguageLevel() {
return LanguageLevel.JDK_13_PREVIEW;
return LanguageLevel.JDK_14_PREVIEW;
}
}
@@ -45,7 +45,7 @@ public class JavaTextBlockMigrationPropertyTest extends LightJavaCodeInsightFixt
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_13;
return JAVA_14;
}
@Override
@@ -86,8 +86,8 @@ public class JavaTextBlockMigrationPropertyTest extends LightJavaCodeInsightFixt
MigrationInvoker migrationInvoker = new MigrationInvoker();
BackwardMigrationInvoker backwardMigrationInvoker = new BackwardMigrationInvoker();
for (PsiPolyadicExpression concatenation : concatenations) {
if (SuppressionUtil.isSuppressed(concatenation, migrationInvoker.getToolId()) ||
SuppressionUtil.isSuppressed(concatenation, backwardMigrationInvoker.getToolId())) {
if (SuppressionUtil.isSuppressed(concatenation, MigrationInvoker.getToolId()) ||
SuppressionUtil.isSuppressed(concatenation, BackwardMigrationInvoker.getToolId())) {
continue;
}
PsiExpression[] operands = concatenation.getOperands();
@@ -142,7 +142,6 @@ public class JavaTextBlockMigrationPropertyTest extends LightJavaCodeInsightFixt
}
// IDEA-226395
String[] textBlockLines = getTextBlockLines(lines);
if (textBlockLines == null) return null;
int indent = PsiLiteralUtil.getTextBlockIndent(textBlockLines, true, true);
if (indent > 0 && textBlockLines.length > 0 && textBlockLines[textBlockLines.length - 1].endsWith("\n")) indent = 0;
if (indent > 0) return null;
@@ -150,17 +149,10 @@ public class JavaTextBlockMigrationPropertyTest extends LightJavaCodeInsightFixt
return StringUtil.join(lines);
}
private static String @Nullable [] getTextBlockLines(String @NotNull [] lines) {
String[] blockLines = new String[lines.length];
boolean escapeStartQuote = false;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
boolean isLastLine = i == lines.length - 1;
line = PsiLiteralUtil.escapeTextBlockCharacters(line, escapeStartQuote, isLastLine, isLastLine);
escapeStartQuote = line.endsWith("\"");
blockLines[i] = line;
}
return blockLines;
@NotNull
private static String @NotNull [] getTextBlockLines(String @NotNull [] lines) {
String blockLines = PsiLiteralUtil.escapeTextBlockCharacters(StringUtil.join(lines), true, true, true);
return blockLines.split("(?<=\n)");
}
private static <T extends PsiElement> void invokeIntention(@NotNull T element,
@@ -206,9 +198,6 @@ public class JavaTextBlockMigrationPropertyTest extends LightJavaCodeInsightFixt
@NotNull
String getFixHint();
@NotNull
String getToolId();
}
private static class MigrationInvoker implements ActionInvoker<PsiPolyadicExpression> {
@@ -230,8 +219,7 @@ public class JavaTextBlockMigrationPropertyTest extends LightJavaCodeInsightFixt
}
@NotNull
@Override
public String getToolId() {
private static String getToolId() {
return "TextBlockMigration";
}
}
@@ -249,8 +237,7 @@ public class JavaTextBlockMigrationPropertyTest extends LightJavaCodeInsightFixt
}
@NotNull
@Override
public String getToolId() {
private static String getToolId() {
return "TextBlockBackwardMigration";
}
}
@@ -13,9 +13,9 @@ public class PsiLiteralUtilTest {
@Test
public void testEscapeTextBlockCharacters() {
assertEquals("foo\\040\\040\n", PsiLiteralUtil.escapeTextBlockCharacters("foo \\n"));
assertEquals("foo \\s\n", PsiLiteralUtil.escapeTextBlockCharacters("foo \\n"));
// escapes after 'bar' should be escaped since it's the last line in a text block
assertEquals("foo\\040\\040\nbar\\040\\040", PsiLiteralUtil.escapeTextBlockCharacters("foo \\nbar "));
assertEquals("foo \\s\nbar \\s", PsiLiteralUtil.escapeTextBlockCharacters("foo \\nbar "));
assertEquals("", PsiLiteralUtil.escapeTextBlockCharacters(""));
// last in line quote should be escaped
@@ -16,6 +16,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ReplaceConcatenationWithFormatStringIntention extends Intention {
@@ -111,7 +112,10 @@ public class ReplaceConcatenationWithFormatStringIntention extends Intention {
((PsiLiteralExpressionImpl)operand).getLiteralElementType() == JavaTokenType.TEXT_BLOCK_LITERAL);
if (textBlocks) {
newExpression.append("\"\"\"\n");
newExpression.append(PsiLiteralUtil.escapeTextBlockCharacters(formatString));
formatString = Arrays.stream(formatString.split("\n"))
.map(s -> PsiLiteralUtil.escapeTextBlockCharacters(s))
.collect(Collectors.joining("\n"));
newExpression.append(formatString);
if (insertNewline) {
newExpression.append('\n');
}
@@ -1,8 +1,8 @@
class TextBlocks {
static {
System.out.println("""
first
second
first \s\
second
third""" + <caret>//c1
"""
_no space
@@ -3,7 +3,7 @@ class TextBlocks {
System.out.println("""
first
second
third
third\
""" + <caret>//c1
" \\forth\"" );
}
@@ -4,7 +4,6 @@ class TextBlocks {
System.out.println("""
first
second
third
\\for<caret>th\"""");
third \\forth\""<caret>"");
}
}
@@ -2,9 +2,8 @@ class TextBlocks {
static {
//c1
System.out.println("""
first
second
third_no sp<caret>ace
""");
first second
third_no space
<caret> """);
}
}
@@ -1,6 +1,6 @@
class C {
String s = """
the text\n block
the text \n block
line2
""" +
1 + 2 +<caret>//keep me
@@ -1,8 +1,8 @@
class C {
//keep me
String s = String.format("""
the text
the text\s
block
line2
%d%d t<caret>o be""", 1, 2);
%d%d <caret>to be""", 1, 2);
}