Java: update "Unnecessarily escaped character" inspection for string literals (IDEA-326944)

GitOrigin-RevId: 49792b6510b8deaf20e49c509c948ba51f508de1
This commit is contained in:
Bas Leijdekkers
2023-08-04 19:58:12 +00:00
committed by intellij-monorepo-bot
parent fd613082f9
commit 599fb6bf19
13 changed files with 245 additions and 76 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig;
import com.intellij.openapi.project.Project;
@@ -15,6 +15,12 @@ import org.jetbrains.annotations.Nullable;
public final class PsiReplacementUtil {
public static void replaceFragment(@NotNull PsiFragment fragment, @NotNull @NonNls String newFragmentText) {
final PsiElementFactory factory = JavaPsiFacade.getElementFactory(fragment.getProject());
final PsiFragment newFragment = factory.createStringTemplateFragment(newFragmentText, fragment.getTokenType(), fragment);
fragment.replace(newFragment);
}
/**
* Consider to use {@link #replaceExpression(PsiExpression, String, CommentTracker)} to preserve comments
*/
@@ -3,7 +3,8 @@ package com.siyeh.ig.redundancy;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightUtil;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.CleanupLocalInspectionTool;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
@@ -12,6 +13,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.java.PsiFragmentImpl;
import com.intellij.psi.util.PsiLiteralUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.InspectionGadgetsBundle;
@@ -69,71 +71,108 @@ public class UnnecessaryStringEscapeInspection extends BaseInspection implements
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
if (!(element instanceof PsiLiteralExpression literalExpression)) {
return;
}
final PsiType type = literalExpression.getType();
if (type == null) {
return;
}
final String text = literalExpression.getText();
final String text = element.getText();
if (!myText.equals(text)) {
return;
}
if (type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
final StringBuilder newExpression = new StringBuilder();
if (literalExpression.isTextBlock()) {
int offset = 0;
int start = findUnnecessaryTextBlockEscapes(text, 4);
while (start >= 0) {
newExpression.append(text, offset, start);
offset = start + 2;
@NonNls final String escape = text.substring(start, offset);
if ("\\n".equals(escape)) {
final int indent = PsiLiteralUtil.getTextBlockIndent(literalExpression);
if (indent < 0) return;
newExpression.append('\n').append(StringUtil.repeatSymbol(' ', indent));
}
else {
newExpression.append(escape.charAt(1));
}
start = findUnnecessaryTextBlockEscapes(text, offset);
}
newExpression.append(text.substring(offset));
final Document document = element.getContainingFile().getViewProvider().getDocument();
assert document != null;
final TextRange replaceRange = element.getTextRange();
document.replaceString(replaceRange.getStartOffset(), replaceRange.getEndOffset(), newExpression.toString());
return;
if (element instanceof PsiFragment fragment) {
if (fragment.isTextBlock()) {
int indent = PsiFragmentImpl.getTextBlockFragmentIndent(fragment);
PsiReplacementUtil.replaceFragment(fragment, buildNewTextBlockText(text, indent));
}
else {
boolean escaped = false;
final int length = text.length();
for (int i = 0; i < length; i++) {
final char c = text.charAt(i);
if (escaped) {
if (c != '\'') newExpression.append('\\');
newExpression.append(c);
escaped = false;
}
else if (c == '\\') escaped = true;
else newExpression.append(c);
PsiReplacementUtil.replaceFragment(fragment, buildNewStringText(text));
}
}
else if (element instanceof PsiLiteralExpression literalExpression) {
final PsiType type = literalExpression.getType();
if (type == null) {
return;
}
if (type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
if (literalExpression.isTextBlock()) {
final int indent = PsiLiteralUtil.getTextBlockIndent(literalExpression);
if (indent < 0) return;
final String newTextBlockTest = buildNewTextBlockText(text, indent);
final Document document = element.getContainingFile().getViewProvider().getDocument();
assert document != null;
final TextRange replaceRange = element.getTextRange();
document.replaceString(replaceRange.getStartOffset(), replaceRange.getEndOffset(), newTextBlockTest);
}
else {
PsiReplacementUtil.replaceExpression(literalExpression, buildNewStringText(text));
}
}
PsiReplacementUtil.replaceExpression(literalExpression, newExpression.toString());
else if (PsiTypes.charType().equals(type) && text.equals("'\\\"'")) {
PsiReplacementUtil.replaceExpression(literalExpression, "'\"'");
}
}
else if (PsiTypes.charType().equals(type) && text.equals("'\\\"'")) {
PsiReplacementUtil.replaceExpression(literalExpression, "'\"'");
}
private static String buildNewStringText(String text) {
final StringBuilder newExpression = new StringBuilder();
boolean escaped = false;
final int length = text.length();
for (int i = 0; i < length; i++) {
final char c = text.charAt(i);
if (escaped) {
if (c != '\'') newExpression.append('\\');
newExpression.append(c);
escaped = false;
}
else if (c == '\\') {
escaped = true;
}
else {
newExpression.append(c);
}
}
return newExpression.toString();
}
@NotNull
private static String buildNewTextBlockText(String text, int indent) {
final StringBuilder newExpression = new StringBuilder();
int offset = 0;
int end = text.endsWith("\"\"\"") ? text.length() - 3 : text.length() - 2;
int start = findUnnecessaryTextBlockEscapes(text, text.startsWith("\"\"\"") ? 4 : 1, end);
while (start >= 0) {
newExpression.append(text, offset, start);
offset = start + 2;
@NonNls final String escape = text.substring(start, offset);
if ("\\n".equals(escape)) {
newExpression.append('\n').append(StringUtil.repeatSymbol(' ', indent));
}
else {
newExpression.append(escape.charAt(1));
}
start = findUnnecessaryTextBlockEscapes(text, offset, end);
}
newExpression.append(text.substring(offset));
return newExpression.toString();
}
}
static int findUnnecessaryTextBlockEscapes(String text, int start) {
static int findUnnecessaryStringEscapes(String text, int start) {
boolean slash = false;
final int max = text.length() - 1; // skip closing "
for (int i = start; i < max; i++) {
final char c = text.charAt(i);
if (slash) {
slash = false;
if (c == '\'') return i - 1;
}
else if (c == '\\') slash = true;
}
return -1;
}
static int findUnnecessaryTextBlockEscapes(String text, int start, int end) {
boolean slash = false;
boolean ws = false;
int doubleQuotes = 0;
final int max = text.length() - 3; // skip closing """
for (int i = start; i < max; i++) {
for (int i = start; i < end; i++) {
final char ch = text.charAt(i);
if (ch == '\\') slash = !slash;
else if (ch == ' ' || ch == '\t') ws = true;
@@ -146,9 +185,9 @@ public class UnnecessaryStringEscapeInspection extends BaseInspection implements
return i - 1;
}
else if (ch == '"' && doubleQuotes < 2) {
if (i == max - 1) return -1;
if (i == max - 2) return i - 1;
if (i < max - 2 && text.charAt(i + 1) == '"') {
if (i == end - 1) return -1;
if (i == end - 2) return i - 1;
if (i < end - 2 && text.charAt(i + 1) == '"') {
if (doubleQuotes != 1 && text.charAt(i + 2) != '"') return i - 1;
}
else {
@@ -173,6 +212,32 @@ public class UnnecessaryStringEscapeInspection extends BaseInspection implements
private class RedundantStringEscapeVisitor extends BaseInspectionVisitor {
@Override
public void visitFragment(@NotNull PsiFragment fragment) {
super.visitFragment(fragment);
HighlightInfo.Builder error = HighlightUtil.checkFragmentError(fragment);
if (error != null) {
return;
}
final String text = fragment.getText();
if (fragment.isTextBlock()) {
int end = fragment.getTokenType() == JavaTokenType.TEXT_BLOCK_TEMPLATE_END ? text.length() - 3 : text.length() - 2;
int start = findUnnecessaryTextBlockEscapes(text, 1, end);
while (start >= 0) {
registerErrorAtOffset(fragment, start, 2, text);
start = findUnnecessaryTextBlockEscapes(text, start + 2, end);
}
}
else {
int start = findUnnecessaryStringEscapes(text, 1);
while (start >= 0) {
registerErrorAtOffset(fragment, start, 2, text);
start = findUnnecessaryStringEscapes(text, start + 2);
}
}
}
@Override
public void visitLiteralExpression(@NotNull PsiLiteralExpression expression) {
super.visitLiteralExpression(expression);
@@ -180,31 +245,26 @@ public class UnnecessaryStringEscapeInspection extends BaseInspection implements
if (type == null) {
return;
}
HighlightInfo.Builder
parsingError = HighlightUtil.checkLiteralExpressionParsingError(expression, PsiUtil.getLanguageLevel(expression), null, null);
HighlightInfo.Builder parsingError =
HighlightUtil.checkLiteralExpressionParsingError(expression, PsiUtil.getLanguageLevel(expression), null, null);
if (parsingError != null) {
return;
}
if (type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
final String text = expression.getText();
if (expression.isTextBlock()) {
final String text = expression.getText();
int start = findUnnecessaryTextBlockEscapes(text, 4);
int end = text.length() - 3;
int start = findUnnecessaryTextBlockEscapes(text, 4, end);
while (start >= 0) {
registerErrorAtOffset(expression, start, 2, text);
start = findUnnecessaryTextBlockEscapes(text, start + 2);
start = findUnnecessaryTextBlockEscapes(text, start + 2, end);
}
}
else {
final String text = expression.getText();
boolean slash = false;
final int max = text.length() - 1; // skip closing "
for (int i = 1; i < max; i++) {
final char c = text.charAt(i);
if (slash) {
slash = false;
if (c == '\'') registerErrorAtOffset(expression, i - 1, 2, text);
}
else if (c == '\\') slash = true;
int start = findUnnecessaryStringEscapes(text, 1);
while (start >= 0) {
registerErrorAtOffset(expression, start, 2, text);
start = findUnnecessaryStringEscapes(text, start + 2);
}
}
}
@@ -1,7 +1,7 @@
<html>
<body>
Reports unnecessarily escaped characters in <code>String</code> and optionally <code>char</code> literals.
<p>The escaped tab character <code>\t</code> is not reported, because otherwise it will be invisible.</p>
<p>Escaped tab characters <code>\t</code> are not reported, because tab characters are invisible.</p>
<p>Examples:</p>
<pre><code>
String s = "\'Scare\' quotes";
@@ -363,6 +363,11 @@ public interface PsiElementFactory extends PsiJavaParserFacade, JVMElementFactor
@Nullable PsiExpression initializer,
@Nullable PsiElement context);
/**
* Creates a string template fragment with the specified text of the specified token type.
*/
@NotNull PsiFragment createStringTemplateFragment(@NotNull String newText, @NotNull IElementType tokenType, @Nullable PsiElement context);
/**
* Creates a PSI element for the "&#64;param" JavaDoc tag.
*
@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.impl;
import com.intellij.lang.*;
@@ -734,6 +734,45 @@ public final class PsiElementFactoryImpl extends PsiJavaParserFacadeImpl impleme
return catchSection;
}
@Override
@NotNull
public PsiFragment createStringTemplateFragment(@NotNull String newText, @NotNull IElementType tokenType, @Nullable PsiElement context) {
int index;
if (tokenType == JavaTokenType.TEXT_BLOCK_TEMPLATE_BEGIN) {
newText += "}\"\"\"";
index = 0;
}
else if (tokenType == JavaTokenType.TEXT_BLOCK_TEMPLATE_MID) {
newText = "\"\"\"\n\\{" + newText + "}\"\"\"";
index = 1;
}
else if (tokenType == JavaTokenType.TEXT_BLOCK_TEMPLATE_END) {
newText = "\"\"\"\n\\{" + newText;
index = 1;
}
else if (tokenType == JavaTokenType.STRING_TEMPLATE_BEGIN) {
newText += "}\"";
index = 0;
}
else if (tokenType == JavaTokenType.STRING_TEMPLATE_MID) {
newText = "\"\\{" + newText + "}\"";
index = 1;
}
else if (tokenType == JavaTokenType.STRING_TEMPLATE_END) {
newText = "\"\\{" + newText;
index = 1;
}
else {
throw new IllegalArgumentException();
}
PsiTemplateExpression expression = (PsiTemplateExpression)createExpressionFromText(newText, context);
PsiTemplate template = expression.getTemplate();
assert template != null;
PsiFragment fragment = template.getFragments().get(index);
GeneratedMarkerVisitor.markGenerated(fragment);
return fragment;
}
@Override
public boolean isValidClassName(@NotNull String name) {
return isIdentifier(name);
@@ -116,7 +116,7 @@ public final class PsiFragmentImpl extends LeafPsiElement implements PsiFragment
return result.toString();
}
private static int getTextBlockFragmentIndent(PsiFragment fragment) {
public static int getTextBlockFragmentIndent(PsiFragment fragment) {
final PsiElement parent = fragment.getParent();
if (!(parent instanceof PsiTemplate)) {
return -1;
@@ -0,0 +1,12 @@
class EscapedNewLine {
public static void main(String[] args) {
String s = """
first line
a
|
third line
""";
System.out.println(s.replace(' ', '.'));
System.out.println("123");
}
}
@@ -2,7 +2,7 @@ class EscapedNewLine {
public static void main(String[] args) {
String s = """
first line
a<warning descr="'\n' is unnecessarily escaped"><caret>\n</warning>
a<warning descr="'\n' is unnecessarily escaped"><caret>\n</warning>|
third line
""";
System.out.println(s.replace(' ', '.'));
@@ -0,0 +1,6 @@
class StringTemplate1 {
String x(int i) {
return STR." '\{i}''\{i}'''";
}
}
@@ -0,0 +1,6 @@
class StringTemplate1 {
String x(int i) {
return STR." <warning descr="'\'' is unnecessarily escaped"><caret>\'</warning>\{i}<warning descr="'\'' is unnecessarily escaped">\'</warning><warning descr="'\'' is unnecessarily escaped">\'</warning>\{i}<warning descr="'\'' is unnecessarily escaped">\'</warning><warning descr="'\'' is unnecessarily escaped">\'</warning><warning descr="'\'' is unnecessarily escaped">\'</warning>";
}
}
@@ -0,0 +1,8 @@
class StringTemplate2 {
String x(int i) {
return STR."""
'\{i}'
'\{i}'""";
}
}
@@ -0,0 +1,8 @@
class StringTemplate2 {
String x(int i) {
return STR."""
<warning descr="'\'' is unnecessarily escaped"><caret>\'</warning>\{i}<warning descr="'\'' is unnecessarily escaped">\'</warning>
<warning descr="'\'' is unnecessarily escaped">\'</warning>\{i}<warning descr="'\'' is unnecessarily escaped">\'</warning>""";
}
}
@@ -1,8 +1,10 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.redundancy;
import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.testFramework.LightProjectDescriptor;
import com.siyeh.ig.LightJavaInspectionTestCase;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
@@ -15,12 +17,25 @@ public class UnnecessaryStringEscapeInspectionTest extends LightJavaInspectionTe
public void testDoubleQuoteInChar() { doQuickFixTest(); }
public void testSingleQuoteInString() { doQuickFixTest(); }
public void testMultipleProblemsInSingleString() { doQuickFixTest(); }
public void testEscapedNewLine() { doTest(); }
public void testEscapedNewLine() { doQuickFixTest(); }
public void testStringTemplate1() { doQuickFixTest(); }
public void testStringTemplate2() { doQuickFixTest(); }
public void testEscapedNewLineNotUnnecessary() { doTest(); }
public void testBrokenCode() { doTest(); }
protected void doQuickFixTest() {
myFixture.addClass("""
package java.lang;
public interface StringTemplate {
Processor<String, RuntimeException> STR = null;
@PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
@FunctionalInterface
interface Processor<R, E extends Throwable> {
R process(StringTemplate stringTemplate) throws E;
}
}""");
doTest();
checkQuickFixAll();
}
@@ -38,4 +53,8 @@ public class UnnecessaryStringEscapeInspectionTest extends LightJavaInspectionTe
return inspection;
}
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return JAVA_21;
}
}