From cae5987ba6ec79ee7cc7f39869863c1acfde1d6d Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Tue, 8 Jul 2025 18:13:27 +0200 Subject: [PATCH] [java] IJ-CR-167924 IDEA-371865 Inspection to convert 'System.out'<->'IO' - extract "java.lang.IO" - tests for allowUnresolved - more reliable works with varargs (cherry picked from commit 8376baaa86afa3806cb096cffd42e9ed0db0d451) (cherry picked from commit 78c8b1aed86c1414989a5a83b9e7d54ca5ed83e0) IJ-MR-169535 GitOrigin-RevId: 704e1b94c19dbf07d5371b8989164070db2cd860 --- .../com/siyeh/ig/callMatcher/CallMatcher.java | 37 ++- .../siyeh/ig/psiutils/ExpressionUtils.java | 2 +- .../MigrateFromJavaLangIoInspection.java | 6 +- .../MigrateToJavaLangIoInspection.java | 2 +- .../beforePartialQualifier.java | 3 + .../beforePrintStream.java | 9 + .../MigrateFromJavaLangIoInspectionTest.java | 12 +- .../MigrateToJavaLangIoInspectionTest.java | 4 + .../testSrc/com/siyeh/ig/CallMatcherTest.java | 212 ++++++++++++++++++ platform/core-api/api-dump.txt | 1 + .../com/intellij/psi/CommonClassNames.java | 2 + 11 files changed, 279 insertions(+), 11 deletions(-) create mode 100644 java/java-tests/testData/inspection/migrateFromJavaLangIo/beforePartialQualifier.java create mode 100644 java/java-tests/testData/inspection/migrateToJavaLangIo/beforePrintStream.java create mode 100644 java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java diff --git a/java/java-analysis-impl/src/com/siyeh/ig/callMatcher/CallMatcher.java b/java/java-analysis-impl/src/com/siyeh/ig/callMatcher/CallMatcher.java index 075ca42a5ff4..714d17f9bd5b 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/callMatcher/CallMatcher.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/callMatcher/CallMatcher.java @@ -23,6 +23,9 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.intellij.psi.CommonClassNames.JAVA_LANG_OBJECT; +import static com.intellij.psi.CommonClassNames.JAVA_LANG_STRING; + /** * This interface represents a condition upon method call * @@ -342,11 +345,13 @@ public interface CallMatcher extends Predicate { 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; + if (!(myClassName.endsWith("." + referenceName) || + myClassName.equals(referenceName))) { + return false; + } PsiElement resolvedQualifier = qualifierRefExpression.resolve(); if (resolvedQualifier != null) return false; return true; @@ -413,9 +418,16 @@ public interface CallMatcher extends Predicate { private static boolean expressionTypeMatches(@Nullable String type, @NotNull PsiExpression argument) { if (type == null) return true; + if (type.endsWith("...")) { + type = type.substring(0, type.length() - 3); + } PsiType psiType = argument.getType(); if (psiType == null) return false; - return psiType.equalsToText(type) || PsiTypesUtil.classNameEquals(psiType, type); + return psiType.equalsToText(type) || + PsiTypesUtil.classNameEquals(psiType, type) || + JAVA_LANG_OBJECT.equals(type) || + //small optimization, because it can be slow and String is popular + (!JAVA_LANG_STRING.equals(type) && InheritanceUtil.isInheritor(psiType, type)); } @Contract(pure = true) @@ -458,12 +470,25 @@ public interface CallMatcher extends Predicate { private boolean unresolvedArgumentListMatch(@NotNull PsiExpressionList expressionList) { if (myParameters == null) return true; PsiExpression[] args = expressionList.getExpressions(); + if (myParameters.length == 0 && args.length != 0) return false; if (myParameters.length > 0) { - if (args.length < myParameters.length - 1) return false; + String lastParameter = myParameters[myParameters.length - 1]; + if (lastParameter != null && lastParameter.endsWith("...")) { + if (args.length < myParameters.length - 1) return false; + } + else { + if (args.length != myParameters.length) return false; + } } - for (int i = 0; i < Math.min(myParameters.length, args.length); i++) { + for (int i = 0; i < args.length; i++) { PsiExpression arg = args[i]; - String parameter = myParameters[i]; + String parameter; + if (i < myParameters.length) { + parameter = myParameters[i]; + } + else { + parameter = myParameters[myParameters.length - 1]; + } if (!expressionTypeMatches(parameter, arg)) return false; } return true; diff --git a/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java b/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java index 51e53776af42..e5004a596568 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/psiutils/ExpressionUtils.java @@ -1483,7 +1483,7 @@ public final class ExpressionUtils { 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); + JAVA_LANG_IO.equals(className); } case "printf", "format" -> { if (arguments.length < 1) yield false; diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateFromJavaLangIoInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateFromJavaLangIoInspection.java index eef82d5fc474..eb566a917fdd 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateFromJavaLangIoInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateFromJavaLangIoInspection.java @@ -11,14 +11,16 @@ import com.siyeh.ig.callMatcher.CallMatcher; import com.siyeh.ig.psiutils.CommentTracker; import org.jetbrains.annotations.NotNull; +import static com.intellij.psi.CommonClassNames.JAVA_LANG_IO; + public final class MigrateFromJavaLangIoInspection extends AbstractBaseJavaLocalInspectionTool { private static final CallMatcher IO_PRINT = CallMatcher.anyOf( - CallMatcher.staticCall("java.lang.IO", "println") + CallMatcher.staticCall(JAVA_LANG_IO, "println") .parameterCount(0) .allowUnresolved(), - CallMatcher.staticCall("java.lang.IO", "println", "print") + CallMatcher.staticCall(JAVA_LANG_IO, "println", "print") .parameterCount(1) .allowUnresolved() ); diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateToJavaLangIoInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateToJavaLangIoInspection.java index 581c1a886b54..8297d9ac663e 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateToJavaLangIoInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateToJavaLangIoInspection.java @@ -119,7 +119,7 @@ public final class MigrateToJavaLangIoInspection extends AbstractBaseJavaLocalIn PsiReferenceExpression methodExpr = methodCall.getMethodExpression(); String methodName = methodExpr.getReferenceName(); if (methodName == null) return; - PsiElement replaced = new CommentTracker().replaceAndRestoreComments(methodExpr, "java.lang.IO." + methodName); + PsiElement replaced = new CommentTracker().replaceAndRestoreComments(methodExpr, JAVA_LANG_IO + "." + methodName); if (replaced instanceof PsiReferenceExpression replacedReferenceExpression) { JavaCodeStyleManager.getInstance(replacedReferenceExpression.getProject()).shortenClassReferences(replacedReferenceExpression); } diff --git a/java/java-tests/testData/inspection/migrateFromJavaLangIo/beforePartialQualifier.java b/java/java-tests/testData/inspection/migrateFromJavaLangIo/beforePartialQualifier.java new file mode 100644 index 000000000000..6a16dd70ce9a --- /dev/null +++ b/java/java-tests/testData/inspection/migrateFromJavaLangIo/beforePartialQualifier.java @@ -0,0 +1,3 @@ +void main() { +O/*some*/.println(/*some2*/"Hello"); +} diff --git a/java/java-tests/testData/inspection/migrateToJavaLangIo/beforePrintStream.java b/java/java-tests/testData/inspection/migrateToJavaLangIo/beforePrintStream.java new file mode 100644 index 000000000000..d6110cd593b7 --- /dev/null +++ b/java/java-tests/testData/inspection/migrateToJavaLangIo/beforePrintStream.java @@ -0,0 +1,9 @@ +import java.io.PrintStream; + +public static void main(String[] args) { + PrintStream stream = getSome(); + stream.println("Hello"); +} + + +private static native PrintStream getSome(); \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateFromJavaLangIoInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateFromJavaLangIoInspectionTest.java index c58971bdd0e1..512267e043fb 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateFromJavaLangIoInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateFromJavaLangIoInspectionTest.java @@ -8,6 +8,7 @@ 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.Nls; import org.jetbrains.annotations.NotNull; public class MigrateFromJavaLangIoInspectionTest extends LightJavaCodeInsightFixtureTestCase { @@ -31,8 +32,17 @@ public class MigrateFromJavaLangIoInspectionTest extends LightJavaCodeInsightFix public void testPrintUnresolved() { doTest("Replace with 'System.out.print()'"); } public void testPrintArrayChar() { + doNotFind(getFixAllMessage()); + } + + private static @Nls @NotNull String getFixAllMessage() { + return InspectionsBundle.message("fix.all.inspection.problems.in.file", + JavaBundle.message("inspection.migrate.from.java.lang.io.name")); + } + + public void testPartialQualifier() { doNotFind( - InspectionsBundle.message("fix.all.inspection.problems.in.file", JavaBundle.message("inspection.migrate.to.java.lang.io.name"))); + getFixAllMessage()); } private void doNotFind(String message) { diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateToJavaLangIoInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateToJavaLangIoInspectionTest.java index 2937f5b9cd4b..2423b4fd75b6 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateToJavaLangIoInspectionTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/MigrateToJavaLangIoInspectionTest.java @@ -36,6 +36,10 @@ public class MigrateToJavaLangIoInspectionTest extends LightJavaCodeInsightFixtu doNotFind(InspectionsBundle.message("fix.all.inspection.problems.in.file", JavaBundle.message("inspection.migrate.to.java.lang.io.name"))); } + public void testPrintStream() { + doNotFind(InspectionsBundle.message("fix.all.inspection.problems.in.file", JavaBundle.message("inspection.migrate.to.java.lang.io.name"))); + } + private void doNotFind(String message) { addIOClass(myFixture); MigrateToJavaLangIoInspection inspection = new MigrateToJavaLangIoInspection(); diff --git a/java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java b/java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java new file mode 100644 index 000000000000..84c98190ffc2 --- /dev/null +++ b/java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java @@ -0,0 +1,212 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.siyeh.ig; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.testFramework.LightProjectDescriptor; +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; +import com.siyeh.ig.callMatcher.CallMatcher; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.psi.CommonClassNames.*; + +public class CallMatcherTest extends LightJavaCodeInsightFixtureTestCase { + + @Override + protected @NotNull LightProjectDescriptor getProjectDescriptor() { + return JAVA_21; + } + + public void testUnresolvedPartiallyMatchQualifier() { + @Language("JAVA") String text = """ + class Main{ + void m() { + IO.println(); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(0) + .allowUnresolved(), text)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "print", "println") + .parameterCount(0) + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "print") + .parameterCount(0) + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(1) + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "print") + .parameterCount(0) + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.NIO", "println") + .parameterCount(0) + .allowUnresolved(), text)); + + + @Language("JAVA") String textWithFullyQualifiedName = """ + class Main{ + void m() { + java.lang.IO.println(); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(0) + .allowUnresolved(), textWithFullyQualifiedName)); + + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.NIO", "println") + .parameterCount(0) + .allowUnresolved(), textWithFullyQualifiedName)); + + } + + + public void testUnresolvedWithParameters() { + @Language("JAVA") String text = """ + class Main{ + void m() { + java.lang.IO.println("test"); + } + } + """; + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(0) + .allowUnresolved(), text)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(1) + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(2) + .allowUnresolved(), text)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterTypes(JAVA_LANG_STRING) + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING) + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterTypes(JAVA_LANG_INTEGER) + .allowUnresolved(), text)); + + @Language("JAVA") String textParameters2 = """ + class Main{ + void m() { + java.lang.IO.println("test", "test2"); + } + } + """; + + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(0) + .allowUnresolved(), textParameters2)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(1) + .allowUnresolved(), textParameters2)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(2) + .allowUnresolved(), textParameters2)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(3) + .allowUnresolved(), textParameters2)); + } + + + public void testUnresolvedWithVarArgs() { + //imagine that there is "java.lang.IO2" with "printf" + @Language("JAVA") String text = """ + class Main{ + void m() { + java.lang.IO2.printf("test"); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterCount(1) + .allowUnresolved(), text)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING + "...") + .allowUnresolved(), text)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") + .allowUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") + .allowUnresolved(), text)); + + + @Language("JAVA") String textWithVarArgs2 = """ + class Main{ + void m() { + java.lang.IO2.printf("test", "a", "b"); + } + } + """; + + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") + .allowUnresolved(), textWithVarArgs2)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING + "...") + .allowUnresolved(), textWithVarArgs2)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING + "...") + .allowUnresolved(), textWithVarArgs2)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING + "...") + .allowUnresolved(), textWithVarArgs2)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") + .allowUnresolved(), textWithVarArgs2)); + assertTrue( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_INTEGER + "...") + .allowUnresolved(), textWithVarArgs2)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_INTEGER + "...") + .allowUnresolved(), textWithVarArgs2)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") + .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING + "...") + .allowUnresolved(), textWithVarArgs2)); + } + + private boolean isMatchedCall(@NotNull CallMatcher matcher, @Language("JAVA") @NotNull String text) { + PsiFile file = myFixture.configureByText("Main.java", text); + PsiElement element = file.findElementAt(myFixture.getCaretOffset()); + PsiMethodCallExpression callExpression = PsiTreeUtil.getParentOfType(element, PsiMethodCallExpression.class, false); + return matcher.matches(callExpression); + } +} diff --git a/platform/core-api/api-dump.txt b/platform/core-api/api-dump.txt index df5ed397029c..c2db3af79e25 100644 --- a/platform/core-api/api-dump.txt +++ b/platform/core-api/api-dump.txt @@ -997,6 +997,7 @@ com.intellij.psi.CommonClassNames - sf:JAVA_LANG_FUNCTIONAL_INTERFACE:java.lang.String - sf:JAVA_LANG_INTEGER:java.lang.String - sf:JAVA_LANG_INVOKE_MH_POLYMORPHIC:java.lang.String +- sf:JAVA_LANG_IO:java.lang.String - sf:JAVA_LANG_ITERABLE:java.lang.String - sf:JAVA_LANG_LONG:java.lang.String - sf:JAVA_LANG_MATH:java.lang.String diff --git a/platform/core-api/src/com/intellij/psi/CommonClassNames.java b/platform/core-api/src/com/intellij/psi/CommonClassNames.java index 16c1ff612464..c0e56aa3c6aa 100644 --- a/platform/core-api/src/com/intellij/psi/CommonClassNames.java +++ b/platform/core-api/src/com/intellij/psi/CommonClassNames.java @@ -112,6 +112,8 @@ public interface CommonClassNames { String JAVA_LANG_NULL_POINTER_EXCEPTION = "java.lang.NullPointerException"; + String JAVA_LANG_IO = "java.lang.IO"; + String JAVA_NIO_CHARSET_CHARSET = "java.nio.charset.Charset"; String JAVA_NET_URI = "java.net.URI";