From 599fb6bf19d3458d9aa8b6532709f211cf6288f5 Mon Sep 17 00:00:00 2001 From: Bas Leijdekkers Date: Fri, 4 Aug 2023 17:16:54 +0200 Subject: [PATCH] Java: update "Unnecessarily escaped character" inspection for string literals (IDEA-326944) GitOrigin-RevId: 49792b6510b8deaf20e49c509c948ba51f508de1 --- .../src/com/siyeh/ig/PsiReplacementUtil.java | 8 +- .../UnnecessaryStringEscapeInspection.java | 198 ++++++++++++------ .../UnnecessaryStringEscape.html | 2 +- .../com/intellij/psi/PsiElementFactory.java | 5 + .../psi/impl/PsiElementFactoryImpl.java | 41 +++- .../source/tree/java/PsiFragmentImpl.java | 2 +- .../EscapedNewLine.after.java | 12 ++ .../EscapedNewLine.java | 2 +- .../StringTemplate1.after.java | 6 + .../StringTemplate1.java | 6 + .../StringTemplate2.after.java | 8 + .../StringTemplate2.java | 8 + ...UnnecessaryStringEscapeInspectionTest.java | 23 +- 13 files changed, 245 insertions(+), 76 deletions(-) create mode 100644 java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.after.java create mode 100644 java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate1.java create mode 100644 java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.after.java create mode 100644 java/java-tests/testData/ig/com/siyeh/igtest/redundancy/unnecessary_string_escape/StringTemplate2.java 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 in String 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