mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-30 18:28:55 +07:00
[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:
committed by
intellij-monorepo-bot
parent
0af3e03979
commit
fdab4d3999
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
void main() {
|
||||
I<caret>O.println((Runnable) () -> IO.println("Hello"));
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
void main() {
|
||||
Sy<caret>stem.out.print("Hello");
|
||||
<caret> /*some*/
|
||||
System.out.print(/*some2*/"Hello");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
void main() {
|
||||
IO<caret>.print(new char[]{'a', 'b'});
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Test {
|
||||
public static void main(String[] args) {
|
||||
IO.<caret>print("Hello");
|
||||
/*some comment*/
|
||||
IO.<caret>print(/*some2*/"Hello");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class Test {
|
||||
public static void main(String[] args) {
|
||||
System.out.<caret>print(new char[]{'1', '2'});
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ public class ExplicitToImplicitClassMigrationInspectionInspectionTest extends Li
|
||||
}
|
||||
|
||||
public void testWithSeveralIO() { doTest(); }
|
||||
public void testWithSeveralNestedIO() { doTest(); }
|
||||
|
||||
private void doNotFind() {
|
||||
myFixture.enableInspections(new ExplicitToImplicitClassMigrationInspection());
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user