From b12ff61e378c0a12280c22a5f238d7cea631f031 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Wed, 9 Jul 2025 19:05:56 +0200 Subject: [PATCH] [java] IJ-CR-167924 IDEA-371865 Inspection to convert 'System.out'<->'IO' - add case for several qualifiers GitOrigin-RevId: e422d2f595e29a3ff3f56f01dbbc6fa7c52d9e8d --- .../com/siyeh/ig/callMatcher/CallMatcher.java | 151 ++++++++----- .../MigrateFromJavaLangIoInspection.java | 4 +- .../testSrc/com/siyeh/ig/CallMatcherTest.java | 201 +++++++++++++++--- 3 files changed, 273 insertions(+), 83 deletions(-) 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 714d17f9bd5b..4456493c04dd 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 @@ -1,8 +1,10 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.siyeh.ig.callMatcher; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.*; +import com.intellij.psi.util.ImportsUtil; import com.intellij.psi.util.InheritanceUtil; import com.intellij.psi.util.PsiTypesUtil; import com.intellij.psi.util.PsiUtil; @@ -17,7 +19,9 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.uast.UCallExpression; import org.jetbrains.uast.UCallableReferenceExpression; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -316,62 +320,14 @@ public interface CallMatcher extends Predicate { * - 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 + * - Call is checked as it is static. *

* 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 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; - String referenceName = qualifierRefExpression.getReferenceName(); - if (referenceName == null && myClassName.isEmpty()) return true; - if (referenceName == null) return false; - if (!(myClassName.endsWith("." + referenceName) || - myClassName.equals(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"); - } - }; + public CallMatcher allowStaticUnresolved() { + return new UnresolvedStaticCallMatcher(); } @Override @@ -527,6 +483,99 @@ public interface CallMatcher extends Predicate { public String toString() { return myClassName + "." + String.join("|", myNames); } + + /** + * @see Simple#allowStaticUnresolved() + */ + private class UnresolvedStaticCallMatcher implements CallMatcher{ + @Override + public Stream 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; + if(!qualifierMatch(call.getMethodExpression().getQualifierExpression(), call)) return false; + return true; + } + + private boolean qualifierMatch(@Nullable PsiExpression expression, @NotNull PsiMethodCallExpression call) { + StringBuilder referenceName = new StringBuilder(); + if (expression instanceof PsiReferenceExpression qualifierRefExpression) { + PsiReferenceExpression currentQualifier = qualifierRefExpression; + while (true) { + String nextReferenceName = currentQualifier.getReferenceName(); + if (nextReferenceName == null) break; + if (referenceName.isEmpty()) { + referenceName = new StringBuilder(nextReferenceName); + } + else { + referenceName.insert(0, nextReferenceName + "."); + } + if (currentQualifier.getQualifierExpression() instanceof PsiReferenceExpression referenceExpression) { + currentQualifier = referenceExpression; + } + else { + break; + } + } + } + if (myClassName.contentEquals(referenceName)) return true; + if (myClassName.equals("java.lang." + referenceName)) return true; + if (!(call.getContainingFile() instanceof PsiJavaFile javaFile)) return false; + if (javaFile.getPackageStatement() != null) { + if (myClassName.equals(javaFile.getPackageStatement().getPackageName() + "." + referenceName)) return true; + } + List importStatements = new ArrayList<>(ImportsUtil.getAllImplicitImports(javaFile)); + PsiImportList importList = javaFile.getImportList(); + if (importList != null) { + importStatements.addAll(List.of(importList.getAllImportStatements())); + } + for (PsiImportStatementBase statement : importStatements) { + if (!(statement instanceof PsiImportStaticStatement staticStatement)) continue; + if (staticStatement.isOnDemand() && staticStatement.getImportReference() != null) { + if (myClassName.equals(staticStatement.getImportReference().getQualifiedName() + "." + referenceName)) return true; + if ((referenceName.isEmpty()) && myClassName.equals(staticStatement.getImportReference().getQualifiedName())) return true; + } + if (!staticStatement.isOnDemand() && staticStatement.getImportReference() != null) { + String staticReference = staticStatement.getImportReference().getQualifiedName(); + String shortName = StringUtil.getShortName(staticReference); + if (shortName.contentEquals(referenceName) || + referenceName.toString().startsWith(shortName + ".")) { + if (myClassName.equals(StringUtil.getPackageName(staticReference) + "." + referenceName)) return true; + } + } + } + return false; + } + + @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"); + } + } } enum CallType { 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 eb566a917fdd..460bd157c127 100644 --- a/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateFromJavaLangIoInspection.java +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/MigrateFromJavaLangIoInspection.java @@ -19,10 +19,10 @@ public final class MigrateFromJavaLangIoInspection extends AbstractBaseJavaLocal CallMatcher.anyOf( CallMatcher.staticCall(JAVA_LANG_IO, "println") .parameterCount(0) - .allowUnresolved(), + .allowStaticUnresolved(), CallMatcher.staticCall(JAVA_LANG_IO, "println", "print") .parameterCount(1) - .allowUnresolved() + .allowStaticUnresolved() ); @Override diff --git a/java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java b/java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java index 84c98190ffc2..2da01fd3bf7e 100644 --- a/java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java +++ b/java/java-tests/testSrc/com/siyeh/ig/CallMatcherTest.java @@ -31,27 +31,27 @@ public class CallMatcherTest extends LightJavaCodeInsightFixtureTestCase { assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(0) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "print", "println") .parameterCount(0) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "print") .parameterCount(0) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(1) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "print") .parameterCount(0) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.NIO", "println") .parameterCount(0) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); @Language("JAVA") String textWithFullyQualifiedName = """ @@ -64,13 +64,33 @@ public class CallMatcherTest extends LightJavaCodeInsightFixtureTestCase { assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(0) - .allowUnresolved(), textWithFullyQualifiedName)); + .allowStaticUnresolved(), textWithFullyQualifiedName)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang2.IO", "println") + .parameterCount(0) + .allowStaticUnresolved(), textWithFullyQualifiedName)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.NIO", "println") .parameterCount(0) - .allowUnresolved(), textWithFullyQualifiedName)); + .allowStaticUnresolved(), textWithFullyQualifiedName)); + + @Language("JAVA") String textWithEmptyFullyQualifiedName = """ + class Main{ + void m() { + println(); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("", "println") + .parameterCount(0) + .allowStaticUnresolved(), textWithEmptyFullyQualifiedName)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") + .parameterCount(0) + .allowStaticUnresolved(), textWithEmptyFullyQualifiedName)); } @@ -85,27 +105,27 @@ public class CallMatcherTest extends LightJavaCodeInsightFixtureTestCase { assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(0) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(1) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(2) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterTypes(JAVA_LANG_STRING) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterTypes(JAVA_LANG_INTEGER) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); @Language("JAVA") String textParameters2 = """ class Main{ @@ -118,19 +138,19 @@ public class CallMatcherTest extends LightJavaCodeInsightFixtureTestCase { assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(0) - .allowUnresolved(), textParameters2)); + .allowStaticUnresolved(), textParameters2)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(1) - .allowUnresolved(), textParameters2)); + .allowStaticUnresolved(), textParameters2)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(2) - .allowUnresolved(), textParameters2)); + .allowStaticUnresolved(), textParameters2)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO", "println") .parameterCount(3) - .allowUnresolved(), textParameters2)); + .allowStaticUnresolved(), textParameters2)); } @@ -146,19 +166,19 @@ public class CallMatcherTest extends LightJavaCodeInsightFixtureTestCase { assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterCount(1) - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING + "...") - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") - .allowUnresolved(), text)); + .allowStaticUnresolved(), text)); @Language("JAVA") String textWithVarArgs2 = """ @@ -172,35 +192,156 @@ public class CallMatcherTest extends LightJavaCodeInsightFixtureTestCase { assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") - .allowUnresolved(), textWithVarArgs2)); + .allowStaticUnresolved(), textWithVarArgs2)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING + "...") - .allowUnresolved(), textWithVarArgs2)); + .allowStaticUnresolved(), textWithVarArgs2)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING + "...") - .allowUnresolved(), textWithVarArgs2)); + .allowStaticUnresolved(), textWithVarArgs2)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING + "...") - .allowUnresolved(), textWithVarArgs2)); + .allowStaticUnresolved(), textWithVarArgs2)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_OBJECT + "...") - .allowUnresolved(), textWithVarArgs2)); + .allowStaticUnresolved(), textWithVarArgs2)); assertTrue( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_INTEGER + "...") - .allowUnresolved(), textWithVarArgs2)); + .allowStaticUnresolved(), textWithVarArgs2)); assertFalse( isMatchedCall(CallMatcher.staticCall("java.lang.IO2", "printf") .parameterTypes(JAVA_LANG_STRING, JAVA_LANG_STRING, JAVA_LANG_INTEGER + "...") - .allowUnresolved(), textWithVarArgs2)); + .allowStaticUnresolved(), 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)); + .allowStaticUnresolved(), textWithVarArgs2)); + } + + public void testUnresolvedSamePackage() { + @Language("JAVA") String text = """ + package foo.bar; + class Main{ + void m() { + IO2.printf("test"); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar2.IO2", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + + } + + public void testUnresolvedOnDemandImports() { + @Language("JAVA") String text = """ + import static foo.bar.IO2.*; + package foo.bar; + class Main{ + void m() { + printf("test"); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar2.IO2", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + + @Language("JAVA") String textNested = """ + import static foo.bar.IO2.*; + package foo.bar; + class Main{ + void m() { + IO.printf("test"); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2.IO", "printf") + .parameterCount(1) + .allowStaticUnresolved(), textNested)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2.ION", "printf") + .parameterCount(1) + .allowStaticUnresolved(), textNested)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO", "printf") + .parameterCount(1) + .allowStaticUnresolved(), textNested)); + } + + public void testUnresolvedImports() { + @Language("JAVA") String text = """ + import static foo.bar.IO2; + package foo.bar; + class Main{ + void m() { + IO2.printf("test"); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar2.IO2", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO", "printf") + .parameterCount(1) + .allowStaticUnresolved(), text)); + + + @Language("JAVA") String textNested = """ + import static foo.bar.IO2; + package foo.bar; + class Main{ + void m() { + IO2.IO.printf("test"); + } + } + """; + assertTrue( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2.IO", "printf") + .parameterCount(1) + .allowStaticUnresolved(), textNested)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2", "printf") + .parameterCount(1) + .allowStaticUnresolved(), textNested)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO", "printf") + .parameterCount(1) + .allowStaticUnresolved(), textNested)); + assertFalse( + isMatchedCall(CallMatcher.staticCall("foo.bar.IO2.IO.I", "printf") + .parameterCount(1) + .allowStaticUnresolved(), textNested)); } private boolean isMatchedCall(@NotNull CallMatcher matcher, @Language("JAVA") @NotNull String text) {