[java] IDEA-371865 Inspection to convert 'System.out'<->'IO'

- fixes for comments
- simplified replacement
- support char[]
- support ImplicitArrayToStringInspection

(cherry picked from commit 49f9f829821a498259aa53ebb12cd0b0007f1238)


(cherry picked from commit 5f4445631fa55911d98477066d3821423c37ec11)

IJ-MR-169535

GitOrigin-RevId: cf93370312fc5fbe632eb73ca8840d157732da50
This commit is contained in:
Mikhail Pyltsin
2025-07-04 17:10:14 +02:00
committed by intellij-monorepo-bot
parent 0af3e03979
commit fdab4d3999
18 changed files with 200 additions and 62 deletions

View File

@@ -304,6 +304,71 @@ public interface CallMatcher extends Predicate<PsiMethodCallExpression> {
myCallType = callType;
}
/**
* Creates a new matcher based on the current matcher, allowing unresolved method calls to be matched.
* This matcher supports verifying unresolved method calls and their context, such as method names,
* qualifier expressions, and class names.
* <p>
* The resulting matcher enforces the following criteria for unresolved calls:
* - Method name must match the specified names.
* - The argument list must match certain conditions based on parameter types.
* - Class name must end with qualifier expressions. Qualifier expression should be unresolved.
* - Call type (for example, static/instance) is not checked
* <p>
* This matcher supports only {@link #test(PsiMethodCallExpression)} method.
*
* @return a new CallMatcher instance that allows unresolved method calls to be matched
*/
public CallMatcher allowUnresolved() {
return new CallMatcher() {
@Override
public Stream<String> names() {
return Simple.this.names();
}
@Override
public boolean methodReferenceMatches(PsiMethodReferenceExpression methodRef) {
throw new UnsupportedOperationException("PsiMethodReferenceExpression is not supported");
}
@Override
public boolean test(@Nullable PsiMethodCallExpression call) {
if (Simple.this.test(call)) return true;
if (call == null) return false;
String name = call.getMethodExpression().getReferenceName();
if (name == null || !myNames.contains(name)) return false;
if (!unresolvedArgumentListMatch(call.getArgumentList())) return false;
PsiMethod method = call.resolveMethod();
if (method != null) return false;
PsiExpression qualifierExpression = call.getMethodExpression().getQualifierExpression();
if (!(qualifierExpression instanceof PsiReferenceExpression qualifierRefExpression)) return false;
if (qualifierRefExpression.getQualifierExpression() != null) return false;
String referenceName = qualifierRefExpression.getReferenceName();
if (referenceName == null && myClassName.isEmpty()) return true;
if (referenceName == null) return false;
if (!myClassName.endsWith(referenceName)) return false;
PsiElement resolvedQualifier = qualifierRefExpression.resolve();
if (resolvedQualifier != null) return false;
return true;
}
@Override
public boolean methodMatches(@Nullable PsiMethod method) {
throw new UnsupportedOperationException("PsiMethod is not supported");
}
@Override
public boolean uCallMatches(@Nullable UCallExpression call) {
throw new UnsupportedOperationException("UCallExpression is not supported");
}
@Override
public boolean uCallableReferenceMatches(@Nullable UCallableReferenceExpression reference) {
throw new UnsupportedOperationException("UCallableReferenceExpression is not supported");
}
};
}
@Override
public Stream<String> names() {
return myNames.stream();
@@ -346,6 +411,13 @@ public interface CallMatcher extends Predicate<PsiMethodCallExpression> {
return psiType.equalsToText(type) || PsiTypesUtil.classNameEquals(psiType, type);
}
private static boolean expressionTypeMatches(@Nullable String type, @NotNull PsiExpression argument) {
if (type == null) return true;
PsiType psiType = argument.getType();
if (psiType == null) return false;
return psiType.equalsToText(type) || PsiTypesUtil.classNameEquals(psiType, type);
}
@Contract(pure = true)
@Override
public boolean methodReferenceMatches(PsiMethodReferenceExpression methodRef) {
@@ -383,6 +455,20 @@ public interface CallMatcher extends Predicate<PsiMethodCallExpression> {
Simple::parameterTypeMatches).allMatch(Boolean.TRUE::equals);
}
private boolean unresolvedArgumentListMatch(@NotNull PsiExpressionList expressionList) {
if (myParameters == null) return true;
PsiExpression[] args = expressionList.getExpressions();
if (myParameters.length > 0) {
if (args.length < myParameters.length - 1) return false;
}
for (int i = 0; i < Math.min(myParameters.length, args.length); i++) {
PsiExpression arg = args[i];
String parameter = myParameters[i];
if (!expressionTypeMatches(parameter, arg)) return false;
}
return true;
}
@Override
@Contract(value = "null -> false", pure = true)
public boolean methodMatches(@Nullable PsiMethod method) {

View File

@@ -1479,10 +1479,11 @@ public final class ExpressionUtils {
yield !hasCharArrayParameter(method);
}
case "print", "println" -> {
if (arguments.length != 1 || hasCharArrayParameter(method)) yield false;
yield JAVA_UTIL_FORMATTER.equals(className) ||
InheritanceUtil.isInheritor(containingClass, JAVA_IO_PRINT_STREAM) ||
InheritanceUtil.isInheritor(containingClass, JAVA_IO_PRINT_WRITER);
if (arguments.length != 1) yield false;
yield (!hasCharArrayParameter(method) && (JAVA_UTIL_FORMATTER.equals(className) ||
InheritanceUtil.isInheritor(containingClass, JAVA_IO_PRINT_STREAM) ||
InheritanceUtil.isInheritor(containingClass, JAVA_IO_PRINT_WRITER))) ||
"java.lang.IO".equals(className);
}
case "printf", "format" -> {
if (arguments.length < 1) yield false;

View File

@@ -328,19 +328,21 @@ public final class ExplicitToImplicitClassMigrationInspection extends AbstractBa
if (!(file instanceof PsiJavaFile javaFile)) {
return;
}
List<PsiMethodCallExpression> systemOutPrints = new ArrayList<>();
List<SmartPsiElementPointer<PsiMethodCallExpression>> systemOutPrints = new ArrayList<>();
SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(file.getProject());
javaFile.accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
if (MigrateToJavaLangIoInspection.isSystemOutPrintln(expression)) {
systemOutPrints.add(expression);
return;
systemOutPrints.add(smartPointerManager.createSmartPsiElementPointer(expression));
}
super.visitMethodCallExpression(expression);
}
});
for (PsiMethodCallExpression print : systemOutPrints) {
MigrateToJavaLangIoInspection.replaceToIO(print);
for (SmartPsiElementPointer<PsiMethodCallExpression> print : systemOutPrints) {
PsiMethodCallExpression element = print.getElement();
if (element == null) continue;
MigrateToJavaLangIoInspection.replaceToIO(element);
}
}

View File

@@ -6,23 +6,23 @@ import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.CommentTracker;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
import java.util.stream.Collectors;
public final class MigrateFromJavaLangIoInspection extends AbstractBaseJavaLocalInspectionTool {
private static final CallMatcher IO_PRINT =
CallMatcher.anyOf(
CallMatcher.staticCall("java.lang.IO", "println").parameterCount(0),
CallMatcher.staticCall("java.lang.IO", "println", "print").parameterCount(1)
CallMatcher.staticCall("java.lang.IO", "println")
.parameterCount(0)
.allowUnresolved(),
CallMatcher.staticCall("java.lang.IO", "println", "print")
.parameterCount(1)
.allowUnresolved()
);
private static final Set<String> IO_PRINT_NAMES = IO_PRINT.names().collect(Collectors.toSet());
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new JavaElementVisitor() {
@@ -72,33 +72,14 @@ public final class MigrateFromJavaLangIoInspection extends AbstractBaseJavaLocal
PsiReferenceExpression methodExpr = methodCall.getMethodExpression();
String methodName = methodExpr.getReferenceName();
if (methodName == null) return;
PsiExpression[] arguments = methodCall.getArgumentList().getExpressions();
StringBuilder replacement = new StringBuilder("System.out.").append(methodName).append("(");
if (arguments.length == 1) {
replacement.append(arguments[0].getText());
PsiElement replaced = new CommentTracker().replaceAndRestoreComments(methodExpr, "java.lang.System.out." + methodName);
if (replaced instanceof PsiReferenceExpression replacedReferenceExpression) {
JavaCodeStyleManager.getInstance(replacedReferenceExpression.getProject()).shortenClassReferences(replacedReferenceExpression);
}
replacement.append(')');
PsiElementFactory factory = JavaPsiFacade.getElementFactory(methodCall.getProject());
PsiExpression expr = factory.createExpressionFromText(replacement.toString(), methodCall);
new CommentTracker().replace(methodCall, expr);
}
private static boolean isIOPrint(@NotNull PsiMethodCallExpression expression) {
boolean isResolvedIO = IO_PRINT.test(expression);
if (isResolvedIO) return true;
String name = expression.getMethodExpression().getReferenceName();
if (!IO_PRINT_NAMES.contains(name)) return false;
PsiExpression[] args = expression.getArgumentList().getExpressions();
if (!(args.length == 0 || args.length == 1)) return false;
PsiMethod method = expression.resolveMethod();
if (method != null) return false;
PsiExpression qualifierExpression = expression.getMethodExpression().getQualifierExpression();
if (!(qualifierExpression instanceof PsiReferenceExpression qualifierRefExpression)) return false;
if (qualifierRefExpression.getQualifierExpression() != null) return false;
String referenceName = qualifierRefExpression.getReferenceName();
if (!"IO".equals(referenceName)) return false;
PsiElement resolvedQualifier = qualifierRefExpression.resolve();
if (resolvedQualifier != null) return false;
return true;
if (!IO_PRINT.test(expression)) return false;
return MigrateToJavaLangIoInspection.callIOAndSystemIdentical(expression.getArgumentList());
}
}

View File

@@ -8,6 +8,7 @@ import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.CommentTracker;
@@ -116,28 +117,25 @@ public final class MigrateToJavaLangIoInspection extends AbstractBaseJavaLocalIn
PsiReferenceExpression methodExpr = methodCall.getMethodExpression();
String methodName = methodExpr.getReferenceName();
if (methodName == null) return;
PsiExpression[] arguments = methodCall.getArgumentList().getExpressions();
StringBuilder replacement = new StringBuilder("IO.").append(methodName).append("(");
if (arguments.length == 1) {
replacement.append(arguments[0].getText());
PsiElement replaced = new CommentTracker().replaceAndRestoreComments(methodExpr, "java.lang.IO." + methodName);
if (replaced instanceof PsiReferenceExpression replacedReferenceExpression) {
JavaCodeStyleManager.getInstance(replacedReferenceExpression.getProject()).shortenClassReferences(replacedReferenceExpression);
}
replacement.append(')');
PsiElementFactory factory = JavaPsiFacade.getElementFactory(methodCall.getProject());
PsiExpression expr = factory.createExpressionFromText(replacement.toString(), methodCall);
new CommentTracker().replace(methodCall, expr);
}
static boolean isSystemOutPrintln(@NotNull PsiMethodCallExpression expression) {
if (!PRINT_STREAM_PRINT.test(expression)) return false;
PsiReferenceExpression methodExpression = expression.getMethodExpression();
PsiExpression qualifier = methodExpression.getQualifierExpression();
if (!(qualifier instanceof PsiReferenceExpression ref)) return false;
PsiElement resolved = ref.resolve();
if (!(resolved instanceof PsiField field)) return false;
if (!field.getName().equals("out")) return false;
PsiClass containingClass = field.getContainingClass();
if (containingClass == null) return false;
if (!CommonClassNames.JAVA_LANG_SYSTEM.equals(containingClass.getQualifiedName())) return false;
return callIOAndSystemIdentical(expression.getArgumentList());
}
static boolean callIOAndSystemIdentical(@NotNull PsiExpressionList list) {
PsiExpression[] expressions = list.getExpressions();
if (expressions.length == 0) return true;
if (expressions.length == 1) {
PsiType type = expressions[0].getType();
if (type == null) return false;
if (type instanceof PsiArrayType arrayType && PsiTypes.charType().equals(arrayType.getComponentType())) return false;
}
return true;
}
}

View File

@@ -0,0 +1,18 @@
package com.siyeh.igtest.bugs.implicit_array_to_string;
import java.io.PrintWriter;
import java.util.Formatter;
public class ImplicitArrayToStringIO {
void foo() {
IO.println("T");
IO.print("T");
IO.println(<warning descr="Implicit call to 'toString()' on array 'new char[]{'1', '2'}'">new char[]{'1', '2'}</warning>);
IO.print(<warning descr="Implicit call to 'toString()' on array 'new char[]{'1', '2'}'">new char[]{'1', '2'}</warning>);
IO.println(<warning descr="Implicit call to 'toString()' on array 'new byte[]{'1', '2'}'">new byte[]{'1', '2'}</warning>);
IO.print(<warning descr="Implicit call to 'toString()' on array 'new byte[]{'1', '2'}'">new byte[]{'1', '2'}</warning>);
}
}

View File

@@ -0,0 +1,4 @@
void main() {
I<caret>O.println((Runnable) () -> IO.println("Hello"));
}

View File

@@ -0,0 +1,6 @@
<warning descr="Explicit class declaration can be converted into a compact source file">public class before<caret>WithSeveralNestedIO</warning> {
public static void main(String[] args) {
System.out.println((Runnable) () -> System.out.println("Hello"));
}
}

View File

@@ -1,3 +1,4 @@
void main() {
Sy<caret>stem.out.print("Hello");
<caret> /*some*/
System.out.print(/*some2*/"Hello");
}

View File

@@ -0,0 +1,3 @@
void main() {
IO<caret>.print(new char[]{'a', 'b'});
}

View File

@@ -1,3 +1,3 @@
void main() {
<error descr="Cannot resolve symbol 'IO'">IO<caret></error>/*some*/.print("Hello");
<error descr="Cannot resolve symbol 'IO'">IO<caret></error>/*some*/.print(/*some2*/"Hello");
}

View File

@@ -1,5 +1,6 @@
class Test {
public static void main(String[] args) {
IO.<caret>print("Hello");
/*some comment*/
IO.<caret>print(/*some2*/"Hello");
}
}

View File

@@ -0,0 +1,5 @@
class Test {
public static void main(String[] args) {
System.out.<caret>print(new char[]{'1', '2'});
}
}

View File

@@ -1,5 +1,5 @@
class Test {
public static void main(String[] args) {
System./*some comment*/out.<caret>print("Hello");
System./*some comment*/out.<caret>print(/*some2*/"Hello");
}
}

View File

@@ -95,6 +95,7 @@ public class ExplicitToImplicitClassMigrationInspectionInspectionTest extends Li
}
public void testWithSeveralIO() { doTest(); }
public void testWithSeveralNestedIO() { doTest(); }
private void doNotFind() {
myFixture.enableInspections(new ExplicitToImplicitClassMigrationInspection());

View File

@@ -2,7 +2,10 @@
package com.intellij.java.codeInspection;
import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.InspectionsBundle;
import com.intellij.codeInspection.MigrateFromJavaLangIoInspection;
import com.intellij.java.JavaBundle;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
@@ -27,6 +30,20 @@ public class MigrateFromJavaLangIoInspectionTest extends LightJavaCodeInsightFix
public void testPrintUnresolved() { doTest("Replace with 'System.out.print()'"); }
public void testPrintArrayChar() {
doNotFind(
InspectionsBundle.message("fix.all.inspection.problems.in.file", JavaBundle.message("inspection.migrate.to.java.lang.io.name")));
}
private void doNotFind(String message) {
MigrateToJavaLangIoInspectionTest.addIOClass(myFixture);
MigrateFromJavaLangIoInspection inspection = new MigrateFromJavaLangIoInspection();
myFixture.enableInspections(inspection);
myFixture.testHighlighting(true, true, true, "before" + getTestName(false) + ".java");
IntentionAction intention = myFixture.getAvailableIntention(message);
assertNull(intention);
}
private void doTest(String message) {
myFixture.enableInspections(new MigrateFromJavaLangIoInspection());
myFixture.testHighlighting(true, true, true, "before" + getTestName(false) + ".java");

View File

@@ -28,6 +28,10 @@ public class MigrateToJavaLangIoInspectionTest extends LightJavaCodeInsightFixtu
public void testPrintRegularClass() { doTest("Replace with 'IO.print()'"); }
public void testPrintArrayChar() {
doNotFind(InspectionsBundle.message("fix.all.inspection.problems.in.file", JavaBundle.message("inspection.migrate.to.java.lang.io.name")));
}
public void testPrintf() {
doNotFind(InspectionsBundle.message("fix.all.inspection.problems.in.file", JavaBundle.message("inspection.migrate.to.java.lang.io.name")));
}
@@ -49,7 +53,7 @@ public class MigrateToJavaLangIoInspectionTest extends LightJavaCodeInsightFixtu
myFixture.checkResultByFile("after" + getTestName(false) + ".java");
}
static void addIOClass(@NotNull JavaCodeInsightTestFixture fixture) {
public static void addIOClass(@NotNull JavaCodeInsightTestFixture fixture) {
fixture.addClass("""
package java.lang;
public final class IO {

View File

@@ -2,6 +2,9 @@
package com.siyeh.ig.bugs;
import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.java.codeInspection.MigrateToJavaLangIoInspectionTest;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.LightProjectDescriptor;
import com.siyeh.ig.LightJavaInspectionTestCase;
import org.jetbrains.annotations.NotNull;
@@ -13,6 +16,13 @@ public class ImplicitArrayToStringInspectionTest extends LightJavaInspectionTest
doTest();
}
public void testImplicitArrayToStringIO() {
IdeaTestUtil.withLevel(getModule(), LanguageLevel.JDK_25, () -> {
MigrateToJavaLangIoInspectionTest.addIOClass(myFixture);
doTest();
});
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {