new "String template can be concatenated string" inspection (IDEA-349463)

GitOrigin-RevId: 84871569e435e1f1cd5c06814781739baf977cae
This commit is contained in:
Bas Leijdekkers
2024-06-06 15:16:38 +02:00
committed by intellij-monorepo-bot
parent 78865dc0a1
commit 4777c9dd1b
5 changed files with 512 additions and 0 deletions

View File

@@ -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());
}
}
}

View File

@@ -1743,6 +1743,12 @@
implementationClass="com.intellij.codeInspection.StringTemplateMigrationInspection"
bundle="messages.JavaBundle"
key="inspection.string.template.migration.name"/>
<localInspection groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids" language="JAVA"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids21" enabledByDefault="true" level="INFORMATION"
implementationClass="com.intellij.codeInspection.StringTemplateReverseMigrationInspection"
bundle="messages.JavaBundle"
key="inspection.string.template.reverse.migration.name"/>
<localInspection groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids" language="JAVA"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids21" enabledByDefault="true" level="INFORMATION"

View File

@@ -0,0 +1,22 @@
<html>
<body>
Reports string template expressions using the <code>STR</code> processor and offers a quick-fix to
migrate back to a plain string concatenation.
<p><b>Example:</b></p>
<pre><code>
String name = "Bob";
String greeting = STR."Hello, \{name}. You are 29 years old.";
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
String name = "Bob";
String greeting = "Hello, " + name + ". You are 29 years old.";
</code></pre>
<!-- tooltip end -->
<p><small>New in 2024.2</small></p>
</body>
</html>

View File

@@ -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<caret>."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<caret>.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."<caret>1 = number";
}
}""", """
class StringTemplateMigration {
void test() {
String test = "1 = number";
}
}""");
}
public void testNumbersPlusString() {
doTest("""
class StringTemplateMigration {
void test() {
String test = <caret>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."<caret>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."<caret>\\{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() = <caret>\\{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(<caret>) = \\{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*/"<caret>\\{/*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."<caret>\\{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."<caret>\\{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."<caret>\\{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."<caret>\\{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."<caret>\\{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."<caret>\\{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.""\"
<caret>\\{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."<caret>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}! <caret>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.""\"
{<caret>
"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;
}
}

View File

@@ -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