diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/StringTemplateReverseMigrationInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/StringTemplateReverseMigrationInspection.java new file mode 100644 index 000000000000..6d032d6e8256 --- /dev/null +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/StringTemplateReverseMigrationInspection.java @@ -0,0 +1,106 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInspection; + +import com.intellij.java.JavaBundle; +import com.intellij.modcommand.ModPsiUpdater; +import com.intellij.modcommand.PsiUpdateModCommandQuickFix; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiPrecedenceUtil; +import com.intellij.psi.util.PsiUtil; +import com.intellij.util.containers.ContainerUtil; +import com.siyeh.ig.psiutils.CommentTracker; +import com.siyeh.ig.psiutils.ExpressionUtils; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author Bas Leijdekkers + */ +public final class StringTemplateReverseMigrationInspection extends AbstractBaseJavaLocalInspectionTool { + + @Override + public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + return new JavaElementVisitor() { + @Override + public void visitTemplateExpression(@NotNull PsiTemplateExpression expression) { + PsiTemplate template = expression.getTemplate(); + PsiLiteralExpression literal = expression.getLiteralExpression(); + if (template == null && literal == null) return; + PsiExpression processor = PsiUtil.deparenthesizeExpression(expression.getProcessor()); + if (!(processor instanceof PsiReferenceExpression reference) || !"STR".equals(reference.getReferenceName())) return; + PsiElement target = reference.resolve(); + if (target != null) { + if (!(target instanceof PsiField field)) return; + PsiClass aClass = field.getContainingClass(); + if (aClass == null || !CommonClassNames.JAVA_LANG_STRING_TEMPLATE.equals(aClass.getQualifiedName())) return; + } + else if (reference.getQualifierExpression() != null) return; + if (template != null && ContainerUtil.exists(template.getFragments(), f -> f.getValue() == null)) return; + if (literal != null && literal.getValue() == null) return; + holder.registerProblem(expression, + JavaBundle.message("inspection.string.template.reverse.migration.string.message"), + new ReplaceWithStringConcatenationFix()); + } + }; + } + + private static class ReplaceWithStringConcatenationFix extends PsiUpdateModCommandQuickFix { + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return JavaBundle.message("inspection.replace.with.string.concatenation.fix"); + } + + @Override + protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) { + if (!(element instanceof PsiTemplateExpression templateExpression)) return; + PsiLiteralExpression literal = templateExpression.getLiteralExpression(); + if (literal != null) { + templateExpression.replace(literal); + return; + } + PsiTemplate template = templateExpression.getTemplate(); + if (template == null) return; + List<@NotNull PsiFragment> fragments = template.getFragments(); + List<@NotNull PsiExpression> expressions = template.getEmbeddedExpressions(); + CommentTracker ct = new CommentTracker(); + StringBuilder concatenation = new StringBuilder(); + boolean start = true; + for (int i = 0; i < expressions.size(); i++) { + PsiFragment fragment = fragments.get(i); + String value = fragment.getValue(); + if (value == null) return; + if (!value.isEmpty()) { + if (!concatenation.isEmpty()) concatenation.append('+'); + concatenation.append('"').append(StringUtil.escapeStringCharacters(value)).append('"'); + start = false; + } + if (!concatenation.isEmpty()) concatenation.append('+'); + PsiExpression expression = expressions.get(i); + int precedence = PsiPrecedenceUtil.getPrecedence(expression); + boolean needParentheses = + precedence > PsiPrecedenceUtil.ADDITIVE_PRECEDENCE || + !start && precedence == PsiPrecedenceUtil.ADDITIVE_PRECEDENCE && !ExpressionUtils.hasStringType(expression); + if (needParentheses) { + concatenation.append('(').append(ct.text(expression)).append(')'); + } + else { + String text = ct.text(expression); + concatenation.append(text.isEmpty() ? "null" : text); + } + } + String last = fragments.get(fragments.size() - 1).getValue(); + if (last == null) return; + if (!last.isEmpty()) { + concatenation.append("+\"").append(StringUtil.escapeStringCharacters(last)).append('"'); + } + ct.replaceAndRestoreComments(templateExpression, concatenation.toString()); + } + } +} diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index f02fe41557d3..7cc3cc1916ba 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -1743,6 +1743,12 @@ implementationClass="com.intellij.codeInspection.StringTemplateMigrationInspection" bundle="messages.JavaBundle" key="inspection.string.template.migration.name"/> + + +Reports string template expressions using the STR processor and offers a quick-fix to +migrate back to a plain string concatenation. + +

Example:

+

+  String name = "Bob";
+  String greeting = STR."Hello, \{name}. You are 29 years old.";
+
+ +

After the quick-fix is applied:

+

+  String name = "Bob";
+  String greeting = "Hello, " + name + ". You are 29 years old.";
+
+ + + +

New in 2024.2

+ + \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/StringTemplateReverseMigrationInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/StringTemplateReverseMigrationInspectionTest.java new file mode 100644 index 000000000000..3475df485d40 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/StringTemplateReverseMigrationInspectionTest.java @@ -0,0 +1,375 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.codeInspection; + +import com.intellij.codeInspection.StringTemplateReverseMigrationInspection; +import com.intellij.java.JavaBundle; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +/** + * @see StringTemplateReverseMigrationInspection + */ +public class StringTemplateReverseMigrationInspectionTest extends LightJavaCodeInsightFixtureTestCase { + public void testSimple() { + doTest(""" + class StringTemplateMigration { + void test() { + String name = "World"; + String test = STR."Hello \\{name}!!!"; + } + }""", """ + class StringTemplateMigration { + void test() { + String name = "World"; + String test = "Hello " + name + "!!!"; + } + }"""); + } + + public void testOverrideStringProcessor() { + doTest(""" + class StringTemplateMigration { + public static final String STR = "surprise!"; + void test() { + String name = "World"; + String test = java.lang.StringTemplate.STR."Hello \\{name}!!!"; + } + private static class StringTemplate {} + }""", """ + class StringTemplateMigration { + public static final String STR = "surprise!"; + void test() { + String name = "World"; + String test = "Hello " + name + "!!!"; + } + private static class StringTemplate {} + }"""); + } + + public void testNoTemplate() { + doTest(""" + class StringTemplateMigration { + void test() { + String test = STR."1 = number"; + } + }""", """ + class StringTemplateMigration { + void test() { + String test = "1 = number"; + } + }"""); + } + + public void testNumbersPlusString() { + doTest(""" + class StringTemplateMigration { + void test() { + String test = STR."\\{1 + 2} = number = 12"; + } + }""", """ + class StringTemplateMigration { + void test() { + String test = 1 + 2 + " = number = 12"; + } + }"""); + } + + public void testCharacterLiteral() { + doTest(""" + class Test { + private String name; + private Date birthDate; + + @Override + public String toString() { + return STR."Test{name='\\{this.name}', birthDate=\\{this.birthDate}}"; + } + }""", """ + class Test { + private String name; + private Date birthDate; + + @Override + public String toString() { + return "Test{name='" + this.name + "', birthDate=" + this.birthDate + "}"; + } + }"""); + } + + public void testDivide() { + doTest(""" + class StringTemplateMigration { + void test(int i) { + String test = STR."\\{i/3} = number"; + } + }""", """ + class StringTemplateMigration { + void test(int i) { + String test = i / 3 + " = number"; + } + }"""); + } + + public void testCallAMethod() { + doTest(""" + class StringTemplateMigration { + void test() { + String test = STR.""\" + fun() = \\{get("foo")} + fun() = \\{this.get("bar")}""\"; + } + StringTemplateMigration get(String data) { + return this; + } + }""", + """ + class StringTemplateMigration { + void test() { + String test = "fun() = " + get("foo") + "\\nfun() = " + this.get("bar"); + } + StringTemplateMigration get(String data) { + return this; + } + }"""); + } + + public void testCallAStaticMethod() { + doTest(""" + class StringTemplateMigration { + void test() { + String test = STR.""\" + fun() = \\{StringTemplateMigration.sum(7, 8)} + fun() = \\{sum(9, 10)}""\"; + } + static int sum(int a, int b) { + return a + b; + } + }""", + """ + class StringTemplateMigration { + void test() { + String test = "fun() = " + StringTemplateMigration.sum(7, 8) + "\\nfun() = " + sum(9, 10); + } + static int sum(int a, int b) { + return a + b; + } + }"""); + } + + public void testKeepComments() { + doTest(""" + class StringTemplateMigration { + void test() { + final String action = "Hello"; + String name = "World"; + String test = STR/*1*/./*2*/"\\{/*3*/action/*4*/} \\{/*5*/name/*6*/}!!!"/*7*/;//8 + } + }""", """ + class StringTemplateMigration { + void test() { + final String action = "Hello"; + String name = "World"; + /*1*/ + /*2*/ + /*3*/ + /*4*/ + /*5*/ + /*6*/ + String test = action + " " + name + "!!!"/*7*/;//8 + } + }"""); + } + + public void testTernaryOperator() { + doTest(""" + class StringTemplateMigration { + void test(String b) { + System.out.println(STR."\\{true ? "a" : b}c"); + } + }""", """ + class StringTemplateMigration { + void test(String b) { + System.out.println((true ? "a" : b) + "c"); + } + }"""); + } + + public void testNumberTypesBeforeString() { + doTest(""" + class StringTemplateMigration { + void test(int i) { + System.out.println(STR."\\{i + 0.2f + 1.1 + 1_000_000 + 7l + 0x0f + 012 + 0b11} = number"); + } + }""", """ + class StringTemplateMigration { + void test(int i) { + System.out.println(i + 0.2f + 1.1 + 1_000_000 + 7l + 0x0f + 012 + 0b11 + " = number"); + } + }"""); + } + + public void testNumberTypesAfterString() { + doTest(""" + class StringTemplateMigration { + void test(String s) { + System.out.println(STR."\\{s} = 11.127\\{0.2f}\\{1_000_000}\\{7l}\\{0x0f}\\{012}\\{0b11}1.21.31.41.5"); + } + }""", """ + class StringTemplateMigration { + void test(String s) { + System.out.println(s + " = 11.127" + 0.2f + 1_000_000 + 7l + 0x0f + 012 + 0b11 + "1.21.31.41.5"); + } + }"""); + } + + + public void testOnlyVars() { + doTest(""" + class StringTemplateMigration { + void test() { + String str = "_"; + System.out.println(STR."\\{str}\\{str}\\{str}"); + } + }""", """ + class StringTemplateMigration { + void test() { + String str = "_"; + System.out.println(str + str + str); + } + }"""); + } + + public void testNewString() { + doTest(""" + class StringTemplateMigration { + void test() { + System.out.println(STR."\\{new String("_")}\\{new String("_")}"); + } + }""", """ + class StringTemplateMigration { + void test() { + System.out.println(new String("_") + new String("_")); + } + }"""); + } + + public void testIntVars() { + doTest(""" + class StringTemplateMigration { + void test() { + int value = 17; + System.out.println(STR."\\{value + value}\\{value}\\{value}"); + } + }""", """ + class StringTemplateMigration { + void test() { + int value = 17; + System.out.println(value + value + value + value); + } + }"""); + } + + public void testEscapeChars() { + doTest(""" + class StringTemplateMigration { + void test() { + int quote = "\\""; + System.out.println(STR.""\" + \\{quote} + \\\\ " \\t \\b \\r \\f ' ©©\\{quote}""\"); + } + }""", + """ + class StringTemplateMigration { + void test() { + int quote = "\\""; + System.out.println(quote + "\\n \\\\ \\" \\t \\b \\r \\f ' ©©" + quote); + } + }"""); + } + + public void testNullValue() { + doTest(""" + class StringTemplateMigration { + void test() { + System.out.println(STR."text is \\{}null"); + } + }""", """ + class StringTemplateMigration { + void test() { + System.out.println("text is " + null + "null"); + } + }"""); + } + + public void testTextBlocks() { + doTest(""" + class TextBlock { + String name = "Java21"; + + String message = STR.""\" + Hello\\{name}! Text block "example". + ""\"; + } + """, + """ + class TextBlock { + String name = "Java21"; + + String message = "Hello" + name + "! Text block \\"example\\".\\n"; + } + """); + } + + public void testFormatting() { + doTest(""" + class StringTemplateMigration { + void test() { + int requestCode = 200; + + String helloJSON = + STR.""\" + { + "cod": \\"\\{requestCode}", + "message": 0, + "cnt": 40, + "city": { + "id": 524901, + "name": "ABC", + "coord": { + "lat": 55.7522, + "lon": 37.6156 + }, + "country": "XY", + "population": 0, + "timezone": 10800, + "sunrise": 1688431913, + "sunset": 1688494529 + } + }""\"; + } + }""", """ + class StringTemplateMigration { + void test() { + int requestCode = 200; + + String helloJSON = + "{\\n \\"cod\\": \\"" + requestCode + "\\",\\n \\"message\\": 0,\\n \\"cnt\\": 40,\\n \\"city\\": {\\n \\"id\\": 524901,\\n \\"name\\": \\"ABC\\",\\n \\"coord\\": {\\n \\"lat\\": 55.7522,\\n \\"lon\\": 37.6156\\n },\\n \\"country\\": \\"XY\\",\\n \\"population\\": 0,\\n \\"timezone\\": 10800,\\n \\"sunrise\\": 1688431913,\\n \\"sunset\\": 1688494529\\n }\\n}"; + } + }"""); + } + + private void doTest(@NotNull @Language("Java") String before, @NotNull @Language("Java") String after) { + myFixture.configureByText("Template.java", before); + myFixture.enableInspections(new StringTemplateReverseMigrationInspection()); + myFixture.launchAction(myFixture.findSingleIntention(JavaBundle.message("inspection.replace.with.string.concatenation.fix"))); + myFixture.checkResult(after); + } + + @Override + protected @NotNull LightProjectDescriptor getProjectDescriptor() { + return LightJavaCodeInsightFixtureTestCase.JAVA_21; + } +} \ No newline at end of file diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index 844daeaaf948..a6d7cc3e6967 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -691,6 +691,7 @@ inspection.replace.with.switch.expression.fix.name=Replace with 'switch' express inspection.replace.with.switch.expression.fix.family.name=Migrate to enhanced switch inspection.replace.with.text.block.fix=Replace with text block inspection.replace.with.string.template.fix=Replace with string template +inspection.replace.with.string.concatenation.fix=Replace with string concatenation inspection.replace.with.trivial.lambda.fix.family.name=Replace with trivial lambda inspection.replace.with.trivial.lambda.fix.name=Replace with lambda returning ''{0}'' inspection.require.non.null=Replace null check with Objects/Stream static call @@ -747,8 +748,10 @@ inspection.text.block.migration.concatenation.message=Concatenation can be repla inspection.text.block.migration.name=Text block can be used inspection.text.block.migration.suggest.literal.replacement=Report single string literals inspection.string.template.migration.string.message=String can be replaced with template +inspection.string.template.reverse.migration.string.message=String template can be replaced with string concatenation inspection.string.template.migration.concatenation.message=Concatenation can be replaced with string template inspection.string.template.migration.name=String template can be used +inspection.string.template.reverse.migration.name=String template can be concatenated string inspection.implicit.to.explicit.class.backward.migration.name=Implicitly declared class can be replaced with ordinary class inspection.implicit.to.explicit.class.backward.migration.fix.name=Convert implicitly declared class into regular class inspection.explicit.to.implicit.class.migration.name=Explicit class declaration can be converted into implicitly declared class