TextBlockMigrationInspection: added inspection that reports cases when string or concatenation can be converted to text block (IDEA-217333)

GitOrigin-RevId: 50750c58f63d1289b236c612ed33e0655b66ef76
This commit is contained in:
Artemiy Sartakov
2019-09-11 12:30:39 +07:00
committed by intellij-monorepo-bot
parent d002bd2dab
commit 550b0aeb8b
26 changed files with 560 additions and 37 deletions

View File

@@ -1330,6 +1330,12 @@
bundle="messages.InspectionsBundle"
key="inspection.redundant.explicit.close"
implementationClass="com.intellij.codeInspection.RedundantExplicitCloseInspection"/>
<localInspection groupPath="Java,Java language level migration aids" language="JAVA" shortName="TextBlockMigration"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids13" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.TextBlockMigrationInspection"
bundle="messages.InspectionsBundle"
key="inspection.text.block.migration.name"/>
<localInspection groupPath="Java,Java language level migration aids" language="JAVA" shortName="TextBlockBackwardMigration"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids13" enabledByDefault="true" level="INFORMATION"

View File

@@ -246,7 +246,10 @@ public class StringLiteralCopyPasteProcessor implements CopyPastePreProcessor {
StringBuilder buffer = new StringBuilder(text.length());
final String[] lines = LineTokenizer.tokenize(text.toCharArray(), false, false);
for (int i = 0; i < lines.length; i++) {
buffer.append(PsiLiteralUtil.escapeTextBlockCharacters(lines[i], i == 0 && escapeStartQuote, i == lines.length - 1 && escapeEndQuote));
String content = PsiLiteralUtil.escapeBackSlashesInTextBlock(lines[i]);
content = PsiLiteralUtil.escapeTextBlockCharacters(content, i == 0 && escapeStartQuote,
i == lines.length - 1 && escapeEndQuote, true);
buffer.append(content);
if (i < lines.length - 1) {
buffer.append('\n');
}

View File

@@ -0,0 +1,111 @@
// 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.codeInspection;
import com.google.common.base.Strings;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightUtil;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.java.PsiLiteralExpressionImpl;
import com.intellij.psi.util.PsiLiteralUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.ig.PsiReplacementUtil;
import com.siyeh.ig.psiutils.CommentTracker;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.intellij.util.ObjectUtils.tryCast;
public class TextBlockMigrationInspection extends AbstractBaseJavaLocalInspectionTool {
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
if (!HighlightUtil.Feature.TEXT_BLOCKS.isAvailable(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR;
return new JavaElementVisitor() {
@Override
public void visitPolyadicExpression(PsiPolyadicExpression expression) {
if (!isConcatenation(expression)) return;
int newLinesCnt = 0;
for (PsiExpression operand : expression.getOperands()) {
String text = getExpressionText(operand, false);
if (text == null) return;
if (newLinesCnt <= 1) newLinesCnt += StringUtils.countMatches(text, "\n");
}
if (newLinesCnt <= 1) return;
holder.registerProblem(expression, InspectionsBundle.message("inspection.text.block.migration.message", "Concatenation"),
new ReplaceWithTextBlockFix());
}
@Override
public void visitLiteralExpression(PsiLiteralExpression expression) {
String text = getExpressionText(expression, false);
if (text == null || StringUtils.countMatches(text, "\n") <= 1) return;
holder.registerProblem(expression, InspectionsBundle.message("inspection.text.block.migration.message", "String"),
new ReplaceWithTextBlockFix());
}
};
}
private static class ReplaceWithTextBlockFix implements LocalQuickFix {
@Nls(capitalization = Nls.Capitalization.Sentence)
@NotNull
@Override
public String getFamilyName() {
return InspectionsBundle.message("inspection.replace.with.text.block.fix");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiExpression expression = PsiUtil.skipParenthesizedExprDown(tryCast(descriptor.getPsiElement(), PsiExpression.class));
if (expression == null) return;
Document document = PsiDocumentManager.getInstance(project).getDocument(expression.getContainingFile());
if (document == null) return;
int expressionOffset = expression.getTextOffset();
int offset = expressionOffset - document.getLineStartOffset(document.getLineNumber(expressionOffset));
PsiLiteralExpressionImpl literalExpression = tryCast(expression, PsiLiteralExpressionImpl.class);
if (literalExpression != null && literalExpression.getLiteralElementType() == JavaTokenType.STRING_LITERAL) {
replaceWithTextBlock(new PsiLiteralExpressionImpl[]{literalExpression}, offset, literalExpression);
return;
}
PsiPolyadicExpression polyadicExpression = tryCast(expression, PsiPolyadicExpression.class);
if (polyadicExpression == null || !isConcatenation(polyadicExpression)) return;
replaceWithTextBlock(polyadicExpression.getOperands(), offset, polyadicExpression);
}
private static void replaceWithTextBlock(@NotNull PsiExpression[] operands, int offset, @NotNull PsiExpression toReplace) {
StringBuilder textBlock = new StringBuilder();
String indent = Strings.repeat(" ", offset);
textBlock.append("\"\"\"\n").append(indent);
boolean escapeStartQuote = false;
for (int i = 0; i < operands.length; i++) {
PsiExpression operand = operands[i];
String text = getExpressionText(operand, true);
if (text == null) return;
boolean isLastLine = i == operands.length - 1;
text = PsiLiteralUtil.escapeTextBlockCharacters(text, escapeStartQuote, isLastLine, isLastLine);
escapeStartQuote = text.endsWith("\"");
textBlock.append(text.replaceAll("\n", '\n' + indent));
}
textBlock.append("\"\"\"");
PsiReplacementUtil.replaceExpression(toReplace, textBlock.toString(), new CommentTracker());
}
}
private static boolean isConcatenation(@NotNull PsiPolyadicExpression expression) {
PsiType type = expression.getType();
return type != null && type.equalsToText(CommonClassNames.JAVA_LANG_STRING);
}
@Nullable
private static String getExpressionText(@NotNull PsiExpression expression, boolean isRawText) {
PsiLiteralExpressionImpl literal = tryCast(PsiUtil.skipParenthesizedExprDown(expression), PsiLiteralExpressionImpl.class);
if (literal == null || literal.getLiteralElementType() == JavaTokenType.TEXT_BLOCK_LITERAL) return null;
if (literal.getLiteralElementType() == JavaTokenType.STRING_LITERAL && isRawText) return literal.getInnerText();
Object value = literal.getValue();
return value == null ? null : value.toString();
}
}

View File

@@ -0,0 +1,26 @@
<html>
<body>
Reports cases when string or concatenation of strings can be replaced with a text block.
<!-- tooltip end -->
Example:
<pre><code>
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
</code></pre>
<p>can be replaced with</p>
<pre><code>
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
</code></pre>
<p>This inspection works if the language level is 13 Preview.</p>
<p><small>New in 2019.3</small></p>
</body>
</html>

View File

@@ -7,7 +7,6 @@ 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;
@@ -153,46 +152,173 @@ public class PsiLiteralUtil {
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
@Contract(pure = true)
public static String escapeTextBlockCharacters(@NotNull String s) {
return escapeTextBlockCharacters(s, false, true);
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 355).
* 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
@Contract(pure = true)
public static String escapeTextBlockCharacters(@NotNull String s, boolean escapeStartQuote, boolean escapeEndQuote) {
public static String escapeTextBlockCharacters(@NotNull String s, boolean escapeStartQuote,
boolean escapeEndQuote, boolean escapeSpacesInTheEnd) {
int i = 0;
int length = s.length();
if (length == 0) return s;
StringBuilder result = new StringBuilder(length);
int q = 0;
for (int i = 0; i < length; i++) {
char c = s.charAt(i);
if (c == '"') {
if (escapeStartQuote && i == 0) result.append('\\');
q++;
while (i < length) {
int nextIdx = parseQuotes(i, s, result, escapeStartQuote, escapeEndQuote);
if (nextIdx != -1) {
i = nextIdx;
continue;
}
else {
appendQuotes(q, result);
if (c == '\\') result.append('\\');
result.append(c);
q = 0;
nextIdx = parseSpaces(i, s, result, escapeSpacesInTheEnd);
if (nextIdx != -1) {
i = nextIdx;
continue;
}
}
appendQuotes(q, result);
if (escapeEndQuote && result.charAt(result.length() - 1) == '"') {
result.insert(result.length() - 1, '\\');
nextIdx = parseBackSlashes(i, s, result);
if (nextIdx != -1) {
i = nextIdx;
continue;
}
result.append(s.charAt(i));
i++;
}
return result.toString();
}
private static void appendQuotes(int quotes, StringBuilder result) {
int q = quotes;
while (q > 0) {
if (quotes >= 3) result.append('\\');
result.append(StringUtil.repeat("\"", Math.min(q, 3)));
q -= 3;
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();
}
/**

View File

@@ -1,5 +1,5 @@
class C {
String empty = """
\"""
target\""\"<caret>""";
""\"
target""\"<caret>""";
}

View File

@@ -0,0 +1,12 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenation() {
String foobarbaz = """
foo
bar
baz""";
}
}

View File

@@ -0,0 +1,12 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithMultipleNewLines() {
String text = """
This text should be on the same line as \\n this one
foo
bar
""";
}
}

View File

@@ -0,0 +1,14 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithMultipleNewLines() {
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
}
}

View File

@@ -0,0 +1,14 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithNonStrings() {
String answer = """
The answer to the meaning of life,
the universe,
and everything
is 42
""";
}
}

View File

@@ -0,0 +1,13 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithExtraSpaces() {
String code = """
<html>\040\040
<body>
</body>
</html>\040\040""";
}
}

View File

@@ -0,0 +1,13 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithThreeQuotes() {
String quotes = """
this concatenation contains
three quotes
one after another
"\"\"""";
}
}

View File

@@ -0,0 +1,13 @@
// "Replace with text block" "true"
class TextBlockMigration {
void literalWithNewLine() {
String foo = """
foo
bar
baz
""";
}
}

View File

@@ -0,0 +1,11 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenation() {
String foobarbaz = "foo\n" <caret>+
"bar\n" +
"baz";
}
}

View File

@@ -0,0 +1,10 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithMultipleNewLines() {
String text = "This<caret> text should be on the same line as \\n this one\n" +
"foo\n" +
"bar\n";
}
}

View File

@@ -0,0 +1,12 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithMultipleNewLines() {
String html = "<html>\n" +<caret>
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
}
}

View File

@@ -0,0 +1,12 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithNonStrings() {
String answer = "T<caret>he answer to the meaning of life,\n" +
"the universe,\n" +
"and everything\n" +
"is " + 42 + '\n';
}
}

View File

@@ -0,0 +1,10 @@
// "Fix all 'Text block can be used' problems in file" "false"
class TextBlockMigration {
void concatenationWithOneNewLine() {
String code = "<<caret>html>\n" +
"</html>";
}
}

View File

@@ -0,0 +1,12 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithExtraSpaces() {
String code = "<<caret>html> \n" +
" <body>\n" +
" </body>\n" +
"</html> ";
}
}

View File

@@ -0,0 +1,14 @@
// "Replace with text block" "true"
class TextBlockMigration {
void concatenationWithThreeQuotes() {
String quotes = "<caret>this concatenation contains\n" +
" three quotes\n" +
"one after another\n" +
"\"" +
'"' +
"\"";
}
}

View File

@@ -0,0 +1,9 @@
// "Fix all 'Text block can be used' problems in file" "false"
class TextBlockMigration {
void empty() {
String empty = "<caret>";
}
}

View File

@@ -0,0 +1,9 @@
// "Replace with text block" "true"
class TextBlockMigration {
void literalWithNewLine() {
String foo = "foo\nbar<caret>\nbaz\n";
}
}

View File

@@ -0,0 +1,9 @@
// "Fix all 'Text block can be used' problems in file" "false"
class TextBlockMigration {
void literalWithNewLine() {
String foo = "foo\nbar<caret>";
}
}

View File

@@ -0,0 +1,29 @@
// 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.java.codeInspection;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.TextBlockMigrationInspection;
import com.intellij.pom.java.LanguageLevel;
import org.jetbrains.annotations.NotNull;
/**
* @see TextBlockMigrationInspection
*/
public class TextBlockMigrationInspectionTest extends LightQuickFixParameterizedTestCase {
@NotNull
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new TextBlockMigrationInspection()};
}
@Override
protected String getBasePath() {
return "/inspection/textBlockMigration/";
}
@Override
protected LanguageLevel getLanguageLevel() {
return LanguageLevel.JDK_13_PREVIEW;
}
}

View File

@@ -1,12 +1,10 @@
// 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.text.StringUtil;
import com.siyeh.ig.LightInspectionTestCase;
import org.junit.Test;
import static com.intellij.psi.util.PsiLiteralUtil.escapeTextBlockCharacters;
import static org.junit.Assert.*;
import static com.intellij.psi.util.PsiLiteralUtil.escapeBackSlashesInTextBlock;
import static org.junit.Assert.assertEquals;
/**
* @author Bas Leijdekkers
@@ -15,8 +13,29 @@ public class PsiLiteralUtilTest {
@Test
public void testEscapeTextBlockCharacters() {
assertEquals("\\\"\"\"\\\"\"\"\\\"\\\"", escapeTextBlockCharacters("\"\"\"\"\"\"\"\"", false, true));
assertEquals("\\\\", escapeTextBlockCharacters("\\", false, true));
assertEquals("", escapeTextBlockCharacters("", false, true));
assertEquals("foo\\040\\040\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("", PsiLiteralUtil.escapeTextBlockCharacters(""));
// last in line quote should be escaped
assertEquals("\\\"", PsiLiteralUtil.escapeTextBlockCharacters("\""));
assertEquals("\"\\\"", PsiLiteralUtil.escapeTextBlockCharacters("\"\""));
// all escaped quotes should be unescaped
assertEquals("\"\\\"", PsiLiteralUtil.escapeTextBlockCharacters("\\\"\""));
// every third quote should be escaped
assertEquals("\"\"\\\"\"\"\\\"\"\\\"", PsiLiteralUtil.escapeTextBlockCharacters("\"\"\"\"\"\"\"\""));
// all sequences except new line should stay as is
assertEquals("\\t\n", PsiLiteralUtil.escapeTextBlockCharacters("\\t\\n"));
}
@Test
public void testEscapeBackSlashesInTextBlock() {
assertEquals("", escapeBackSlashesInTextBlock(""));
assertEquals("\\\\", escapeBackSlashesInTextBlock("\\"));
// backslash before quote should be preserved
assertEquals("\\\\\"", escapeBackSlashesInTextBlock("\\\""));
}
}

View File

@@ -1005,6 +1005,10 @@ inspection.fold.expression.into.stream.fix.name=Fold expression into Stream chai
inspection.fold.expression.into.string.display.name=Expression can be folded into 'String.join'
inspection.fold.expression.into.string.fix.name=Fold expression into 'String.join'
inspection.fold.expression.fix.family.name=Fold expression
inspection.text.block.migration.name=Text block can be used
inspection.text.block.migration.message={0} can be replaced with text block
inspection.replace.with.text.block.fix=Replace with text block
inspection.text.block.backward.migration.name=Text block can be replaced with regular string literal
inspection.text.block.backward.migration.message=Text block can be converted to regular string literal
inspection.replace.with.regular.string.literal.fix=Replace with regular string literal