diff --git a/java/java-analysis-impl/src/com/siyeh/ig/PsiReplacementUtil.java b/java/java-analysis-impl/src/com/siyeh/ig/PsiReplacementUtil.java index 9e45993d8187..a6abe38b2df4 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/PsiReplacementUtil.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/PsiReplacementUtil.java @@ -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 */ diff --git a/java/java-impl/src/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspection.java b/java/java-impl/src/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspection.java index 01f57d71c616..ebef9d67860e 100644 --- a/java/java-impl/src/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspection.java +++ b/java/java-impl/src/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspection.java @@ -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); } } } diff --git a/java/java-impl/src/inspectionDescriptions/UnnecessaryStringEscape.html b/java/java-impl/src/inspectionDescriptions/UnnecessaryStringEscape.html index dbae2db37067..7f676be37896 100644 --- a/java/java-impl/src/inspectionDescriptions/UnnecessaryStringEscape.html +++ b/java/java-impl/src/inspectionDescriptions/UnnecessaryStringEscape.html @@ -1,7 +1,7 @@
Reports unnecessarily escaped characters inString and optionally char literals.
-The escaped tab character \t is not reported, because otherwise it will be invisible.
Escaped tab characters \t are not reported, because tab characters are invisible.
Examples:
String s = "\'Scare\' quotes";
diff --git a/java/java-psi-api/src/com/intellij/psi/PsiElementFactory.java b/java/java-psi-api/src/com/intellij/psi/PsiElementFactory.java
index e83cfdbce77b..fd9e2a3768c6 100644
--- a/java/java-psi-api/src/com/intellij/psi/PsiElementFactory.java
+++ b/java/java-psi-api/src/com/intellij/psi/PsiElementFactory.java
@@ -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.
*
diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/PsiElementFactoryImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/PsiElementFactoryImpl.java
index 88580f7fd8bb..8e33ee4bf0da 100644
--- a/java/java-psi-impl/src/com/intellij/psi/impl/PsiElementFactoryImpl.java
+++ b/java/java-psi-impl/src/com/intellij/psi/impl/PsiElementFactoryImpl.java
@@ -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);
diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiFragmentImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiFragmentImpl.java
index 23d69fb01d5f..565080cb6563 100644
--- a/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiFragmentImpl.java
+++ b/java/java-psi-impl/src/com/intellij/psi/impl/source/tree/java/PsiFragmentImpl.java
@@ -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;
diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.after.java b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.after.java
index e69de29bb2d1..3166c6aceb0d 100644
--- a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.after.java
+++ b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.after.java
@@ -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");
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.java b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.java
index d58a3f87ecca..d1121657149b 100644
--- a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.java
+++ b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/EscapedNewLine.java
@@ -2,7 +2,7 @@ class EscapedNewLine {
public static void main(String[] args) {
String s = """
first line
- a\n
+ a\n |
third line
""";
System.out.println(s.replace(' ', '.'));
diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.after.java b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.after.java
new file mode 100644
index 000000000000..d1c6115781f2
--- /dev/null
+++ b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.after.java
@@ -0,0 +1,6 @@
+class StringTemplate1 {
+
+ String x(int i) {
+ return STR." '\{i}''\{i}'''";
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.java b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.java
new file mode 100644
index 000000000000..19801b6932b2
--- /dev/null
+++ b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.java
@@ -0,0 +1,6 @@
+class StringTemplate1 {
+
+ String x(int i) {
+ return STR." \' \{i}\' \' \{i}\' \' \' ";
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.after.java b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.after.java
new file mode 100644
index 000000000000..c248efe72c3b
--- /dev/null
+++ b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.after.java
@@ -0,0 +1,8 @@
+class StringTemplate2 {
+
+ String x(int i) {
+ return STR."""
+ '\{i}'
+ '\{i}'""";
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.java b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.java
new file mode 100644
index 000000000000..c1bedda66643
--- /dev/null
+++ b/java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.java
@@ -0,0 +1,8 @@
+class StringTemplate2 {
+
+ String x(int i) {
+ return STR."""
+ \' \{i}\'
+ \' \{i}\' """;
+ }
+}
\ No newline at end of file
diff --git a/java/java-tests/testSrc/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspectionTest.java b/java/java-tests/testSrc/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspectionTest.java
index 5886f2069841..f8b2a25cc09a 100644
--- a/java/java-tests/testSrc/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspectionTest.java
+++ b/java/java-tests/testSrc/com/siyeh/ig/redundancy/UnnecessaryStringEscapeInspectionTest.java
@@ -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 STR = null;
+
+ @PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
+ @FunctionalInterface
+ interface Processor {
+ 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;
+ }
}
\ No newline at end of file