mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Java: update "Unnecessarily escaped character" inspection for string literals (IDEA-326944)
GitOrigin-RevId: 49792b6510b8deaf20e49c509c948ba51f508de1
This commit is contained in:
committed by
intellij-monorepo-bot
parent
fd613082f9
commit
599fb6bf19
@@ -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
|
||||
*/
|
||||
|
||||
+129
-69
@@ -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 "@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;
|
||||
|
||||
+12
@@ -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");
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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(' ', '.'));
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
class StringTemplate1 {
|
||||
|
||||
String x(int i) {
|
||||
return STR." '\{i}''\{i}'''";
|
||||
}
|
||||
}
|
||||
+6
@@ -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>";
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
class StringTemplate2 {
|
||||
|
||||
String x(int i) {
|
||||
return STR."""
|
||||
'\{i}'
|
||||
'\{i}'""";
|
||||
}
|
||||
}
|
||||
+8
@@ -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>""";
|
||||
}
|
||||
}
|
||||
+21
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user