mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 11:53:49 +07:00
new "String template can be concatenated string" inspection (IDEA-349463)
GitOrigin-RevId: 84871569e435e1f1cd5c06814781739baf977cae
This commit is contained in:
committed by
intellij-monorepo-bot
parent
78865dc0a1
commit
4777c9dd1b
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user