[codeInsight] IDEA-113640 Provide intention to combine System.out.println(String.format(...)) into System.out.printf

The RedundantStringFormatCallInspection inspection used to be able to
detect excessive String.format calls and get rid of them not changing
the callsite at all. This patch enhances the inspection's capabilities
with changing the callsite if it is either PrintStream#print or
PrintStream#println and converting it to PrintStream#printf adding "%n"
if necessary.

Signed-off-by: Nikita Eshkeev <nikita.eshkeev@jetbrains.com>

GitOrigin-RevId: 7edc5b0a84fb6c7b9caf504b1afb8905c5684985
This commit is contained in:
Nikita Eshkeev
2020-05-01 14:07:13 +00:00
committed by intellij-monorepo-bot
parent a2b594c60d
commit d605adb8d1
13 changed files with 655 additions and 160 deletions
@@ -0,0 +1,8 @@
<html>
<body>
Reports the combinations of System.out.println(String.format(...)) so they can be converted to System.out.printf(...) using a quick-fix
<!-- tooltip end -->
<p>Quick fix: removes String.format() calls and converts println to printf. E.g. System.out.<strong>println</strong>(String.format("%s, %s!", "Hello", "World")) will be converted to System.out.<strong>printf</strong>("%s, %s!<strong>%n</strong>", "Hello", "World")</p>
<p><small>New in 2020.2</small></p>
</body>
</html>
@@ -3,16 +3,15 @@ package com.intellij.psi.util;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiJavaToken;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.*;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public class PsiLiteralUtil {
@NonNls public static final String HEX_PREFIX = "0x";
@NonNls public static final String BIN_PREFIX = "0b";
@@ -640,4 +639,44 @@ public class PsiLiteralUtil {
return lineBreakIdx + 1;
}
}
/**
* This method appends a suffix to a {@link PsiLiteralExpression} and returns a new
* {@link PsiLiteralExpression} that contains the resulting content leaving the original
* {@link PsiLiteralExpression} unchanged.
*
* @param expression the expression to append a string to
* @param suffix the suffix to add to the expression
* @return a new instance of {@link PsiLiteralExpression} that contains the concatenated
* value of the original {@link PsiLiteralExpression} and the suffix.
*/
@Nullable
@Contract(value = "null, _ -> null; _, null -> param1; !null, _ -> !null", pure = true)
public static PsiLiteralExpression append(@Nullable final PsiLiteralExpression expression, @Nullable final String suffix) {
if (expression == null) return null;
if (StringUtil.isEmpty(suffix)) return expression;
final Object value = expression.getValue();
if (value == null) return expression;
final StringBuilder newExpression = new StringBuilder();
final String leftText = value.toString();
if (expression.isTextBlock()) {
final String indent = StringUtil.repeat(" ", getTextBlockIndent(expression));
newExpression.append("\"\"\"").append('\n').append(indent);
newExpression.append(leftText.replaceAll("\n", "\n" + indent));
newExpression.append(StringUtil.escapeStringCharacters(suffix));
newExpression.append("\"\"\"");
}
else {
newExpression.append('"');
newExpression.append(StringUtil.escapeStringCharacters(leftText));
newExpression.append(StringUtil.escapeStringCharacters(suffix));
newExpression.append('"');
}
final PsiElementFactory factory = JavaPsiFacade.getElementFactory(expression.getProject());
return (PsiLiteralExpression)factory.createExpressionFromText(newExpression.toString(), null);
}
}
@@ -0,0 +1,52 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
import java.util.Locale;
class Main {
static {
System.out.printf("%s, %s!", "Hello", "World");
System.out.printf(
/* condition start */ true /* condition end */
? /* first leg start */ "%s, %s!" /* first leg end */
: /* second leg start */ "%s: %s" /* second leg end */,
/* first arg start */ "Hello"/* first arg end */,
/* second arg start */ "World" /* second arg end */);
System.out.printf(/* one */ Locale.US, /* two */ "%s, %s!" /* three */, /* four */ "Hello" /* five */, /* six */ "World" /* seven */);
}
Main() {
System.out.printf("%s, World!", "Hello");
System.out.printf("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.print("========");
}
void f() {
System.out.printf("%s, World!", "Hello");
System.out.printf("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.print("========");
}
void out(PrintStream printer) {
printer.printf("%s, World!", "Hello");
printer.printf("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf(/* one */ Locale.US, /* two */ "%s, %s!" /* three */, /* four */ "Hello" /* five */, /* six */ "World" /* seven */);
printer.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.print("========");
}
void caller() {
print(String.format("%s, %s!", "Hello", "World"));
}
static void print(String value) {}
}
@@ -0,0 +1,28 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
class Main {
static {
System.out.print(/* begin */ "Hello, World!"/* end */);
System.out.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
Main() {
System.out.print(/* begin */ "Hello, World!"/* end */);
System.out.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
void f() {
System.out.print(/* begin */ "Hello, World!"/* end */);
System.out.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
void out(PrintStream printer) {
printer.print(/* begin */ "Hello, World!"/* end */);
printer.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
void caller() {
print(/* begin */ "Hello, World!"/* end */);
print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
static void print(String value) {}
}
@@ -0,0 +1,38 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
import java.util.Locale;
import static java.lang.String.format;
class Main {
static {
String s1 = "test";
String s1n = format("test%n");
String s2 = "test";
String s2n = format(Locale.US, "test%n");
String s3 = "test";
String s3l = "test";
String s3n = String.format(Locale.US, "test%n");
System.out.println(/* one */ /* two */ /* three */ "hello, " /* four */);
}
Main() {
String s1 = "test";
String s1n = format("test%n");
String s2 = "test";
String s2n = format(Locale.US, "test%n");
String s3 = "test";
String s3l = "test";
String s3n = String.format(Locale.US, "test%n");
}
void f() {
String s1 = "test";
String s1n = format("test%n");
String s2 = "test";
String s2n = format(Locale.US, "test%n");
String s3 = "test";
String s3l = "test";
String s3n = String.format(Locale.US, "test%n");
}
}
@@ -0,0 +1,52 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
class Main {
static {
System.out.printf("%s, %s!%n", "Hello", "World");
System.out.printf(
/* condition start */ (false /* condition end */
? /* first leg start */ "%s, %s!" /* first leg end */
: /* second leg start */ "%s: %s") + "%n" /* second leg end */,
/* first arg start */ "Hello"/* first arg end */,
/* second arg start */ "World" /* second arg end */);
}
Main() {
System.out.printf("Hello, World!%n%n");
System.out.printf("%s, World!%n", "Hello");
System.out.printf("%s, %s!%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + "5%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7) + "%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.println("========");
}
void f() {
System.out.printf("Hello, World!%n%n");
System.out.printf("%s, World!%n", "Hello");
System.out.printf("%s, %s!%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + "5%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7) + "%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.println("========");
}
void out(PrintStream printer) {
printer.printf("Hello, World!%n%n");
printer.printf("%s, World!%n", "Hello");
printer.printf("%s, %s!%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf(/* one */ "%s," + /* two */ " %s!%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf(/* one */ "%s," + /* two */ " %s!" + "5%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7) + "%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.println("========");
}
void caller() {
println(String.format("%s, %s!", "Hello", "World"));
}
static void println(String value) {}
}
@@ -0,0 +1,52 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
import java.util.Locale;
class Main {
static {
System.out.print(String.format("%s, %s!", "Hello", "World"));
System.out.print(String.format(
/* condition start */ true /* condition end */
? /* first leg start */ "%s, %s!" /* first leg end */
: /* second leg start */ "%s: %s" /* second leg end */,
/* first arg start */ "Hello"/* first arg end */,
/* second arg start */ "World" /* second arg end */));
System.out.print(String.format(/* one */ Locale.US, /* two */ "%s, %s!" /* three */, /* four */ "Hello" /* five */, /* six */ "World" /* seven */));
}
Main() {
System.out.print(String.format("%s, World!", "Hello"));
System.out.print(String.format("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.print(String.format(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.print(String.format(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.print(String.format(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.print("========");
}
void f() {
System.out.print(String.format("%s, World!", "Hello"));
System.out.print(String.format("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.print(String.format(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.print(String.format(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.print(String.format(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.print("========");
}
void out(PrintStream printer) {
printer.print(String.<caret>format("%s, World!", "Hello"));
printer.print(String.format("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.print(String.format(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.print(String.format(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.print(String.format(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.print(String.format(/* one */ Locale.US, /* two */ "%s, %s!" /* three */, /* four */ "Hello" /* five */, /* six */ "World" /* seven */));
printer.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.print("========");
}
void caller() {
print(String.format("%s, %s!", "Hello", "World"));
}
static void print(String value) {}
}
@@ -0,0 +1,28 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
class Main {
static {
System.out.print(String.<caret>format(/* begin */ "Hello, World!"/* end */));
System.out.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
Main() {
System.out.print(String.format(/* begin */ "Hello, World!"/* end */));
System.out.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
void f() {
System.out.print(String.format(/* begin */ "Hello, World!"/* end */));
System.out.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
void out(PrintStream printer) {
printer.print(String.format(/* begin */ "Hello, World!"/* end */));
printer.print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
void caller() {
print(String.format(/* begin */ "Hello, World!"/* end */));
print(String.format(/* begin */ "Hello, World!%n"/* end */));
}
static void print(String value) {}
}
@@ -0,0 +1,38 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
import java.util.Locale;
import static java.lang.String.format;
class Main {
static {
String s1 = f<caret>ormat("test");
String s1n = format("test%n");
String s2 = format(Locale.US, "test");
String s2n = format(Locale.US, "test%n");
String s3 = String.format("test");
String s3l = String.format(Locale.US, "test");
String s3n = String.format(Locale.US, "test%n");
System.out.println(String.format(/* one */ Locale.CANADA /* two */, /* three */ "hello, " /* four */));
}
Main() {
String s1 = format("test");
String s1n = format("test%n");
String s2 = format(Locale.US, "test");
String s2n = format(Locale.US, "test%n");
String s3 = String.format("test");
String s3l = String.format(Locale.US, "test");
String s3n = String.format(Locale.US, "test%n");
}
void f() {
String s1 = format("test");
String s1n = format("test%n");
String s2 = format(Locale.US, "test");
String s2n = format(Locale.US, "test%n");
String s3 = String.format("test");
String s3l = String.format(Locale.US, "test");
String s3n = String.format(Locale.US, "test%n");
}
}
@@ -0,0 +1,52 @@
// "Fix all 'Redundant call to 'String.format()'' problems in file" "true"
import java.io.PrintStream;
class Main {
static {
System.out.println(String.<caret>format("%s, %s!", "Hello", "World"));
System.out.println(String.format(
/* condition start */ false /* condition end */
? /* first leg start */ "%s, %s!" /* first leg end */
: /* second leg start */ "%s: %s" /* second leg end */,
/* first arg start */ "Hello"/* first arg end */,
/* second arg start */ "World" /* second arg end */));
}
Main() {
System.out.println(String.format("Hello, World!%n"));
System.out.println(String.format("%s, World!", "Hello"));
System.out.println(String.format("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.println(String.format(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.println(String.format(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.println(String.format(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.println("========");
}
void f() {
System.out.println(String.format("Hello, World!%n"));
System.out.println(String.format("%s, World!", "Hello"));
System.out.println(String.format("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.println(String.format(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.println(String.format(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.println(String.format(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
System.out.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
System.out.println("========");
}
void out(PrintStream printer) {
printer.println(String.format("Hello, World!%n"));
printer.println(String.format("%s, World!", "Hello"));
printer.println(String.format("%s, %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.println(String.format(/* one */ "%s," + /* two */ " %s!", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.println(String.format(/* one */ "%s," + /* two */ " %s!" + 5, /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.println(String.format(/* one */ "%s," + /* two */ " %s!" + (5 /* four */ + /* five */ 7), /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */));
printer.printf("%s, %s", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.printf("%s, %s%n", /* param1 start */ "Hello" /* param1 end */, /* param2 start */ "World" /* param2 end */);
printer.println("========");
}
void caller() {
println(String.format("%s, %s!", "Hello", "World"));
}
static void println(String value) {}
}
@@ -0,0 +1,18 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.java.codeInsight.intention;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.siyeh.ig.performance.RedundantStringFormatCallInspection;
public class ReplaceStringFormatWithPrintfIntentionTest extends LightQuickFixParameterizedTestCase {
@Override
protected String getBasePath() {
return "/codeInsight/replaceStringFormat";
}
@Override
protected LocalInspectionTool[] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new RedundantStringFormatCallInspection()};
}
}
@@ -1,180 +1,131 @@
/*
* Copyright 2008-2018 Bas Leijdekkers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.siyeh.ig.performance;
import com.intellij.codeInspection.CommonQuickFixBundle;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiLiteralUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.InspectionGadgetsFix;
import com.siyeh.ig.PsiReplacementUtil;
import com.siyeh.ig.callMatcher.CallMapper;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.CommentTracker;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import com.siyeh.ig.psiutils.ExpressionUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class RedundantStringFormatCallInspection extends BaseInspection {
import java.io.PrintStream;
import java.util.Locale;
import static com.siyeh.ig.callMatcher.CallMatcher.instanceCall;
import static com.siyeh.ig.callMatcher.CallMatcher.staticCall;
public final class RedundantStringFormatCallInspection extends LocalInspectionTool {
@Override
@NotNull
protected String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("redundant.call.problem.descriptor");
public @NotNull PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, final boolean isOnTheFly) {
return new RemoveRedundantStringFormatVisitor(holder, isOnTheFly);
}
@Override
protected InspectionGadgetsFix buildFix(Object... infos) {
final Boolean printf = (Boolean)infos[0];
return printf.booleanValue() ? new ReplaceWithPrintFix() : new RedundantStringFormatCallFix();
}
private static final class RemoveRedundantStringFormatVisitor extends JavaElementVisitor {
@Override
public boolean isEnabledByDefault() {
return true;
}
private static final CallMatcher PRINTSTREAM_PRINTF = instanceCall(PrintStream.class.getName(), "printf")
.parameterTypes(String.class.getName(), "java.lang.Object...");
private static final CallMatcher PRINTSTREAM_PRINT = instanceCall(PrintStream.class.getName(), "print")
.parameterTypes(String.class.getName());
private static final CallMatcher PRINTSTREAM_PRINTLN = instanceCall(PrintStream.class.getName(), "println")
.parameterTypes(String.class.getName());
private static class ReplaceWithPrintFix extends InspectionGadgetsFix {
@Nls
@NotNull
@Override
public String getFamilyName() {
return CommonQuickFixBundle.message("fix.replace.x.with.y", "printf()", "print()");
private static final CallMatcher STRING_FORMAT = staticCall(String.class.getName(), "format");
private final CallMapper<ProblemDescriptor> myProcessors = new CallMapper<ProblemDescriptor>()
.register(PRINTSTREAM_PRINTF, this::getRedundantPrintfProblem)
.register(STRING_FORMAT, this::getRedundantStringFormatProblem);
@NotNull private final ProblemsHolder myHolder;
private final boolean myIsOnTheFly;
@NotNull private final InspectionManager myManager;
private RemoveRedundantStringFormatVisitor(@NotNull final ProblemsHolder holder, final boolean isOnTheFly) {
myHolder = holder;
myManager = myHolder.getManager();
myIsOnTheFly = isOnTheFly;
}
@Override
protected void doFix(Project project, ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
final PsiElement parent = element.getParent();
final PsiElement grandParent = parent.getParent();
if (!(grandParent instanceof PsiMethodCallExpression)) {
return;
}
final PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression)grandParent;
final PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression();
CommentTracker commentTracker = new CommentTracker();
@NonNls final StringBuilder newExpression = new StringBuilder();
final PsiExpression qualifierExpression = methodExpression.getQualifierExpression();
if (qualifierExpression != null) {
newExpression.append(commentTracker.text(qualifierExpression)).append('.');
}
newExpression.append("print").append(commentTracker.text(methodCallExpression.getArgumentList()));
PsiReplacementUtil.replaceExpression(methodCallExpression, newExpression.toString(), commentTracker);
}
}
private static class RedundantStringFormatCallFix extends InspectionGadgetsFix {
@Override
@NotNull
public String getFamilyName() {
return InspectionGadgetsBundle.message("redundant.string.format.call.quickfix");
}
@Override
protected void doFix(Project project, ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement().getParent().getParent();
if (!(element instanceof PsiMethodCallExpression)) {
return;
}
final PsiMethodCallExpression methodCallExpression = (PsiMethodCallExpression)element;
final PsiExpression[] arguments = methodCallExpression.getArgumentList().getExpressions();
new CommentTracker().replaceAndRestoreComments(methodCallExpression, arguments[arguments.length - 1]);
}
}
@Override
public BaseInspectionVisitor buildVisitor() {
return new RedundantStringFormatCallVisitor();
}
private static class RedundantStringFormatCallVisitor extends BaseInspectionVisitor {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression expression) {
super.visitMethodCallExpression(expression);
final PsiReferenceExpression methodExpression = expression.getMethodExpression();
@NonNls final String methodName = methodExpression.getReferenceName();
final boolean printf;
if ("format".equals(methodName)) {
printf = false;
}
else if ("printf".equals(methodName)) {
printf = true;
}
else {
return;
}
final PsiExpressionList argumentList = expression.getArgumentList();
final PsiExpression[] arguments = argumentList.getExpressions();
if (arguments.length > 2 || arguments.length == 0) {
return;
}
final PsiMethod method = expression.resolveMethod();
if (method == null) {
return;
}
final PsiClass aClass = method.getContainingClass();
if (aClass == null) {
return;
}
final String className = aClass.getQualifiedName();
if (printf) {
if (!"java.io.PrintStream".equals(className)) {
return;
}
}
else if (!CommonClassNames.JAVA_LANG_STRING.equals(className)) {
return;
}
final PsiExpression firstArgument = arguments[0];
final PsiType firstType = firstArgument.getType();
if (firstType == null) {
return;
}
if (firstType.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
if (arguments.length == 1 && !containsPercentN(firstArgument)) {
registerMethodCallError(expression, printf);
}
}
else if (firstType.equalsToText("java.util.Locale")) {
if (arguments.length != 2) {
return;
}
final PsiExpression secondArgument = arguments[1];
final PsiType secondType = secondArgument.getType();
if (secondType == null || !secondType.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
return;
}
if (containsPercentN(secondArgument)) {
return;
}
registerMethodCallError(expression, printf);
public void visitMethodCallExpression(@NotNull final PsiMethodCallExpression call) {
final ProblemDescriptor descriptor = myProcessors.mapFirst(call);
if (descriptor != null) {
myHolder.registerProblem(descriptor);
}
}
private static boolean containsPercentN(PsiExpression expression) {
@Nullable
private ProblemDescriptor getRedundantPrintfProblem(@NotNull final PsiMethodCallExpression call) {
final PsiExpressionList args = call.getArgumentList();
if (args.getExpressionCount() != 1) return null;
final PsiExpression formatValue = args.getExpressions()[0];
if (containsNewlineToken(formatValue)) return null;
final PsiElement method = call.getMethodExpression().getReferenceNameElement();
if (method == null) return null;
final TextRange textRange = new TextRange(method.getStartOffsetInParent(),
method.getStartOffsetInParent() + method.getTextLength());
return myManager.createProblemDescriptor(call, textRange,
InspectionGadgetsBundle.message("redundant.call.problem.descriptor"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly,
new ReplaceWithPrintFix());
}
@Nullable
private ProblemDescriptor getRedundantStringFormatProblem(@NotNull final PsiMethodCallExpression call) {
if (isStringFormatCallRedundant(call)) {
return myManager.createProblemDescriptor(call, (TextRange)null,
InspectionGadgetsBundle.message("redundant.string.format.call.display.name"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly,
new RemoveRedundantStringFormatFix());
}
final PsiMethodCallExpression printlnCall = PsiTreeUtil.getParentOfType(call, PsiMethodCallExpression.class);
final boolean isPrintlnCall = PRINTSTREAM_PRINTLN.test(printlnCall);
if (!isPrintlnCall) {
if (!PRINTSTREAM_PRINT.test(printlnCall)) return null;
}
return myManager.createProblemDescriptor(call, (TextRange)null,
InspectionGadgetsBundle.message("redundant.string.format.call.display.name"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myIsOnTheFly,
new StringFormatToPrintfQuickFix(isPrintlnCall));
}
@Contract(pure = true)
private static boolean isStringFormatCallRedundant(@NotNull final PsiMethodCallExpression call) {
final PsiExpressionList params = call.getArgumentList();
if (params.getExpressionCount() == 1) {
return !containsNewlineToken(params.getExpressions()[0]);
}
else if (params.getExpressionCount() == 2) {
final PsiExpression firstArg = params.getExpressions()[0];
if (firstArg.getType() == null || !firstArg.getType().equalsToText(Locale.class.getName())) return false;
return !containsNewlineToken(params.getExpressions()[1]);
}
return false;
}
@Contract("null -> false")
private static boolean containsNewlineToken(@Nullable final PsiExpression expression) {
if (expression == null) {
return false;
}
if (expression instanceof PsiLiteralExpression) {
final PsiLiteralExpression literalExpression = (PsiLiteralExpression)expression;
@NonNls final String expressionText = literalExpression.getText();
final String expressionText = literalExpression.getText();
return expressionText.contains("%n");
}
if (expression instanceof PsiPolyadicExpression) {
@@ -185,12 +136,151 @@ public class RedundantStringFormatCallInspection extends BaseInspection {
}
final PsiExpression[] operands = polyadicExpression.getOperands();
for (PsiExpression operand : operands) {
if (containsPercentN(operand)) {
if (containsNewlineToken(operand)) {
return true;
}
}
}
return false;
}
private static final class ReplaceWithPrintFix implements LocalQuickFix {
@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return CommonQuickFixBundle.message("fix.replace.x.with.y", "printf()", "print()");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
if (!(element instanceof PsiMethodCallExpression)) return;
final PsiMethodCallExpression printStreamPrintfCall = (PsiMethodCallExpression)element;
ExpressionUtils.bindCallTo(printStreamPrintfCall, "print");
}
}
private static final class RemoveRedundantStringFormatFix implements LocalQuickFix {
@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message("redundant.string.format.call.quickfix");
}
@Override
public void applyFix(@NotNull Project project,
@NotNull ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
if (!(element instanceof PsiMethodCallExpression)) return;
final PsiMethodCallExpression stringFormat = (PsiMethodCallExpression)element;
final PsiElement parent = stringFormat.getParent();
if (parent instanceof PsiExpressionList && ((PsiExpressionList)parent).getExpressionCount() == 1 && parent.getParent() instanceof PsiMethodCallExpression){
final PsiMethodCallExpression printCall = (PsiMethodCallExpression)parent.getParent();
final PsiExpression[] args = stringFormat.getArgumentList().getExpressions();
if (args.length > 1) {
new CommentTracker().deleteAndRestoreComments(args[0]);
}
new CommentTracker().replaceAndRestoreComments(printCall.getArgumentList(), stringFormat.getArgumentList());
}
else {
final CommentTracker ct = new CommentTracker();
final PsiExpression[] args = stringFormat.getArgumentList().getExpressions();
final String expression = ct.text(args[args.length - 1]);
ct.replaceAndRestoreComments(stringFormat, expression);
}
}
}
private static final class StringFormatToPrintfQuickFix implements LocalQuickFix {
private final boolean myIsPrintlnCall;
private StringFormatToPrintfQuickFix(boolean isPrintlnCall) {
myIsPrintlnCall = isPrintlnCall;
}
@Override
public @IntentionFamilyName @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message("redundant.string.format.call.quickfix");
}
@Override
public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor descriptor) {
final PsiElement element = descriptor.getPsiElement();
if (!(element instanceof PsiMethodCallExpression)) return;
final PsiMethodCallExpression stringFormatCall = (PsiMethodCallExpression)element;
final PsiMethodCallExpression printlnCall = PsiTreeUtil.getParentOfType(stringFormatCall, PsiMethodCallExpression.class);
if (printlnCall == null) return;
final PsiExpressionList stringFormatArgs = stringFormatCall.getArgumentList();
if (myIsPrintlnCall) {
addNewlineToFormatValue(stringFormatArgs);
}
ExpressionUtils.bindCallTo(printlnCall, "printf");
final PsiExpressionList printlnArgs = printlnCall.getArgumentList();
new CommentTracker().replaceAndRestoreComments(printlnArgs, stringFormatArgs);
}
private static void addNewlineToFormatValue(@NotNull final PsiExpressionList stringFormatArgs) {
if (stringFormatArgs.getExpressionCount() == 0) return;
final PsiExpression formatValueArg = getArgWithFormatValue(stringFormatArgs);
if (formatValueArg != null) {
appendWithNewlineToken(formatValueArg);
}
}
@Nullable
@Contract(pure = true)
private static PsiExpression getArgWithFormatValue(@NotNull final PsiExpressionList stringFormatArgs) {
final PsiExpression firstFormatArg = stringFormatArgs.getExpressions()[0];
final PsiType firstType = firstFormatArg.getType();
if (firstType == null) return null;
if (firstType.equalsToText(Locale.class.getName())) {
if (stringFormatArgs.getExpressionCount() <= 1) return null;
final PsiExpression secondFormatArg = stringFormatArgs.getExpressions()[1];
final PsiType secondType = secondFormatArg.getType();
if (secondType == null || !secondType.equalsToText(String.class.getName())) return null;
return secondFormatArg;
}
else if (firstType.equalsToText(String.class.getName())) {
return firstFormatArg;
}
return null;
}
private static void appendWithNewlineToken(@NotNull final PsiElement formatArg) {
final String newLineToken = "%n";
if (formatArg instanceof PsiLiteralExpression) {
final PsiLiteralExpression replacement = PsiLiteralUtil.append((PsiLiteralExpression)formatArg, newLineToken);
formatArg.replace(replacement);
}
else if (formatArg instanceof PsiPolyadicExpression){
final PsiElement lastChild = formatArg.getLastChild();
if (lastChild instanceof PsiLiteralExpression) {
final PsiLiteralExpression replacement = PsiLiteralUtil.append((PsiLiteralExpression)lastChild, newLineToken);
lastChild.replace(replacement);
}
else {
final CommentTracker ct = new CommentTracker();
final String text = String.format("%s + \"%s\"", ct.text(formatArg), newLineToken);
ct.replaceAndRestoreComments(formatArg, text);
}
}
else {
final CommentTracker ct = new CommentTracker();
final String text = String.format("(%s) + \"%s\"", ct.text(formatArg), newLineToken);
ct.replaceAndRestoreComments(formatArg, text);
}
}
}
}
}
@@ -7,15 +7,15 @@ import static java.lang.String.format;
public class RedundantStringFormatCall {
public static final String A = String.format("%n");
String b = String.<warning descr="Redundant call to 'format()'">format</warning>("no parameters");
String b = <warning descr="Redundant call to 'String.format()'">String.format("no parameters")</warning>;
String c = String.format("asdf%n" +
"asdf%n");
String d = String.format("asdf" + "asdf" + "asdf%n");
String e = <warning descr="Redundant call to 'format()'">format</warning>("test");
String e = <warning descr="Redundant call to 'String.format()'">format("test")</warning>;
void m() {
System.out.println(String.format("string contains %%n%n")); // ok
System.out.println(String.format(Locale.ENGLISH, "string contains %%n%n"));
System.out.println(<warning descr="Redundant call to 'String.format()'">String.format("string contains %%n%n")</warning>); // ok
System.out.println(<warning descr="Redundant call to 'String.format()'">String.format(Locale.ENGLISH, "string contains %%n%n")</warning>);
System.out.<warning descr="Redundant call to 'printf()'">printf</warning>("empty battery");
}
}