[java-inspection] IDEA-355916 update implicit<->explicit classes inspections

- tests
- optimize imports
- ReplaceOnDemandImportIntention supports module imports

GitOrigin-RevId: 228135179fc26f44064dd196ae70326fd91fb3ee
This commit is contained in:
Mikhail Pyltsin
2024-07-25 19:11:41 +02:00
committed by intellij-monorepo-bot
parent 7b0eaea1e9
commit d186699847
38 changed files with 242 additions and 69 deletions

View File

@@ -12,6 +12,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.search.PackageScope;
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.search.searches.ReferencesSearch;
@@ -19,9 +20,12 @@ import com.intellij.psi.util.PsiMethodUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ipp.imports.ReplaceOnDemandImportIntention;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public final class ExplicitToImplicitClassMigrationInspection extends AbstractBaseJavaLocalInspectionTool {
@@ -168,6 +172,26 @@ public final class ExplicitToImplicitClassMigrationInspection extends AbstractBa
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
PsiFile containingFile = element.getContainingFile();
if (!(containingFile instanceof PsiJavaFile javaFile)) {
return;
}
PsiImportList list = javaFile.getImportList();
if (list != null) {
List<SmartPsiElementPointer<PsiImportStatementBase>> pointers = new ArrayList<>();
PsiImportStatementBase[] statements = list.getAllImportStatements();
for (PsiImportStatementBase statement : statements) {
SmartPsiElementPointer<PsiImportStatementBase> pointer = SmartPointerManager.createPointer(statement);
pointers.add(pointer);
}
for (SmartPsiElementPointer<PsiImportStatementBase> pointer : pointers) {
PsiImportStatementBase pointerElement = pointer.getElement();
if (pointerElement == null) continue;
if (!pointerElement.isOnDemand()) continue;
ReplaceOnDemandImportIntention.replaceOnDemand(pointerElement);
}
}
PsiClass psiClass = ObjectUtils.tryCast(element, PsiClass.class);
if (psiClass == null) {
return;
@@ -177,11 +201,17 @@ public final class ExplicitToImplicitClassMigrationInspection extends AbstractBa
if (lBrace == null || rBrace == null || lBrace.getNextSibling() == null || rBrace.getPrevSibling() == null) {
return;
}
CommentTracker tracker = new CommentTracker();
String body = tracker.rangeText(lBrace.getNextSibling(), rBrace.getPrevSibling());
PsiImplicitClass newClass = PsiElementFactory.getInstance(project).createImplicitClassFromText(body, psiClass);
PsiElement replaced = tracker.replace(psiClass, newClass);
tracker.insertCommentsBefore(replaced);
PsiFile replacedContainingFile = replaced.getContainingFile();
if (replacedContainingFile != null) {
JavaCodeStyleManager.getInstance(project).optimizeImports(replacedContainingFile);
}
}
}
}

View File

@@ -101,8 +101,9 @@ public final class ImplicitToExplicitClassBackwardMigrationInspection extends Ab
if (!(containingFile instanceof PsiJavaFile psiJavaFile)) {
return;
}
List<ImplicitlyImportedStaticMember> imports =
ContainerUtil.filterIsInstance(psiJavaFile.getImplicitlyImportedElements(), ImplicitlyImportedStaticMember.class);
ImplicitlyImportedElement[] elements = psiJavaFile.getImplicitlyImportedElements();
List<ImplicitlyImportedStaticMember> staticImports = ContainerUtil.filterIsInstance(elements, ImplicitlyImportedStaticMember.class);
List<ImplicitlyImportedModule> moduleImports = ContainerUtil.filterIsInstance(elements, ImplicitlyImportedModule.class);
PsiElement replaced = implicitClass.replace(newClass);
PsiJavaFile newPsiJavaFile = PsiTreeUtil.getParentOfType(replaced, PsiJavaFile.class);
if (newPsiJavaFile == null) {
@@ -112,15 +113,39 @@ public final class ImplicitToExplicitClassBackwardMigrationInspection extends Ab
if (importList == null) {
return;
}
addImplicitStaticImports(project, staticImports, implicitClass, importList);
addImplicitJavaModuleImports(project, moduleImports, importList);
JavaCodeStyleManager.getInstance(project).optimizeImports(newPsiJavaFile);
}
private static void addImplicitJavaModuleImports(@NotNull Project project,
@NotNull List<ImplicitlyImportedModule> moduleImports,
@NotNull PsiImportList list) {
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
for (@NotNull ImplicitlyImportedModule anImport : moduleImports) {
PsiImportModuleStatement moduleImportStatement = factory.createImportModuleStatementFromText(anImport.getModuleName());
PsiImportModuleStatement[] declarations = list.getImportModuleStatements();
if (declarations.length == 0) {
list.add(moduleImportStatement);
}
else {
list.addBefore(moduleImportStatement, declarations[0]);
}
}
}
private static void addImplicitStaticImports(@NotNull Project project,
@NotNull List<ImplicitlyImportedStaticMember> staticImports,
@NotNull PsiImplicitClass implicitClass,
@NotNull PsiImportList importList) {
JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
for (@NotNull ImplicitlyImportedStaticMember importMember : imports) {
for (@NotNull ImplicitlyImportedStaticMember importMember : staticImports) {
PsiClass psiClass = psiFacade.findClass(importMember.getContainingClass(), implicitClass.getResolveScope());
if (psiClass == null) {
continue;
}
PsiReferenceExpressionImpl.bindToElementViaStaticImport(psiClass, importMember.getMemberName(), importList);
}
JavaCodeStyleManager.getInstance(project).optimizeImports(newPsiJavaFile);
}
}
}

View File

@@ -10,6 +10,7 @@ import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ipp.base.MCIntention;
import com.siyeh.ipp.base.PsiElementPredicate;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
@@ -37,19 +38,23 @@ public final class ReplaceOnDemandImportIntention extends MCIntention {
@Override
protected void invoke(@NotNull PsiElement element) {
final PsiImportStatementBase importStatementBase = (PsiImportStatementBase)element;
replaceOnDemand(importStatementBase);
}
public static void replaceOnDemand(@NotNull PsiImportStatementBase importStatementBase) {
final PsiJavaFile javaFile = (PsiJavaFile)importStatementBase.getContainingFile();
final PsiManager manager = importStatementBase.getManager();
final PsiElementFactory factory = JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
if (importStatementBase instanceof PsiImportStatement importStatement) {
if (importStatementBase instanceof PsiImportModuleStatement || importStatementBase instanceof PsiImportStatement) {
final PsiClass[] classes = javaFile.getClasses();
final String qualifiedName = importStatement.getQualifiedName();
final ClassCollector visitor = new ClassCollector(qualifiedName);
final ClassCollector visitor = ClassCollector.create(importStatementBase);
if(visitor==null) return;
for (PsiClass aClass : classes) {
aClass.accept(visitor);
}
final PsiClass[] importedClasses = visitor.getImportedClasses();
Arrays.sort(importedClasses, new PsiClassComparator());
createImportStatements(importStatement, importedClasses, factory::createImportStatement);
createImportStatements(importStatementBase, importedClasses, factory::createImportStatement);
}
else if (importStatementBase instanceof PsiImportStaticStatement) {
PsiClass targetClass = ((PsiImportStaticStatement)importStatementBase).resolveTargetClass();
@@ -84,11 +89,20 @@ public final class ReplaceOnDemandImportIntention extends MCIntention {
private static class ClassCollector extends JavaRecursiveElementWalkingVisitor {
@Nullable
private final String importedPackageName;
@Nullable
private final PsiImportModuleStatement importModuleStatement;
private final Set<PsiClass> importedClasses = new HashSet<>();
ClassCollector(String importedPackageName) {
ClassCollector(@NotNull String importedPackageName) {
this.importedPackageName = importedPackageName;
this.importModuleStatement = null;
}
ClassCollector(@NotNull PsiImportModuleStatement importModuleStatement) {
this.importedPackageName = null;
this.importModuleStatement = importModuleStatement;
}
@Override
@@ -105,15 +119,34 @@ public final class ReplaceOnDemandImportIntention extends MCIntention {
final String qualifiedName = aClass.getQualifiedName();
final String packageName =
ClassUtil.extractPackageName(qualifiedName);
if (!importedPackageName.equals(packageName)) {
if (importedPackageName != null && importedPackageName.equals(packageName)) {
importedClasses.add(aClass);
return;
}
importedClasses.add(aClass);
if (importModuleStatement != null) {
PsiPackageAccessibilityStatement aPackage = importModuleStatement.findImportedPackage(packageName);
if (aPackage != null) {
importedClasses.add(aClass);
}
}
}
public PsiClass[] getImportedClasses() {
return importedClasses.toArray(PsiClass.EMPTY_ARRAY);
}
@Nullable
static ClassCollector create(@NotNull PsiImportStatementBase statementBase) {
if (statementBase instanceof PsiImportModuleStatement moduleStatement) {
return new ClassCollector(moduleStatement);
}
if (statementBase instanceof PsiImportStatement importStatement) {
String qualifiedName = importStatement.getQualifiedName();
if (qualifiedName == null) return null;
return new ClassCollector(qualifiedName);
}
return null;
}
}
private static final class PsiClassComparator
@@ -126,6 +159,9 @@ public final class ReplaceOnDemandImportIntention extends MCIntention {
if (qualifiedName1 == null) {
return -1;
}
if (qualifiedName2 == null) {
return 1;
}
return qualifiedName1.compareTo(qualifiedName2);
}
}

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "true-preview"
public static void main(String[] args) {
System.out.println("Hello, world!");
}

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "true-preview"
public static void main(String[] args) {
System.out.println("Hello, world!");
}

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "true-preview"
public static void main(String[] args) {
System.out.println("Hello, world!");

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "true-preview"
/**
* comments

View File

@@ -1,5 +1,3 @@
// "Convert into implicitly declared class" "true-preview"
public static void main() {
System.out.println("Hello, world!");
}

View File

@@ -1,5 +1,3 @@
// "Convert into implicitly declared class" "true-preview"
public void main() {
System.out.println("Hello, world!");
}

View File

@@ -0,0 +1,6 @@
import p.List;
public static void main(Strin<caret>g[] args) {
List a = null;
System.out.println("Hello, world!");
}

View File

@@ -1,5 +1,4 @@
// "Convert into implicitly declared class" "false"
public class AnotherFil<caret>eName {
<error descr="Class 'AnotherFileName' is public, should be declared in a file named 'AnotherFileName.java'">public class AnotherFileName</error> {
public static void main(String[] args) {
System.out.println("Hello, world!");
}

View File

@@ -1,5 +1,4 @@
// "Convert into implicitly declared class" "true-preview"
public<caret> class beforeCaretAtClass {
<warning descr="Explicit class declaration can be converted into implicitly declared class">public class beforeCaretAtClass</warning> {
public static void main(String[] args) {
System.out.println("Hello, world!");
}

View File

@@ -1,5 +1,4 @@
// "Convert into implicitly declared class" "true-preview"
public<caret> class beforeExtendsObject extends Object {
<warning descr="Explicit class declaration can be converted into implicitly declared class">public class beforeExtendsObject</warning> extends Object {
public static void main(String[] args) {
System.out.println("Hello, world!");
}

View File

@@ -1,5 +1,3 @@
// "Convert into implicitly declared class" "false"
public interface beforeInterfa<caret>ce {
public static void main(String[] args) {
System.out.println("Hello, world!");

View File

@@ -1,5 +1,3 @@
// "Convert into implicitly declared class" "false"
public class beforeSeveralSi<caret>mple {
public static void main(String[] args) {
System.out.println("Hello, world!");

View File

@@ -1,6 +1,5 @@
// "Convert into implicitly declared class" "true-preview"
public class beforeSi<caret>mple {
<warning descr="Explicit class declaration can be converted into implicitly declared class">public class before<caret>Simple</warning> {
public static void main(String[] args) {
System.out.println("Hello, world!");
}

View File

@@ -1,10 +1,9 @@
// "Convert into implicitly declared class" "true-preview"
/**
<warning descr="Explicit class declaration can be converted into implicitly declared class">/**
* comments
*/
public /*comments2*/ class beforeSi<caret>mpleWithComments {
public /*comments2*/ class beforeSimpleW<caret>ithComments</warning> {
/*comments3*/
public static void main(String[] args) {
//comments4

View File

@@ -1,5 +1,4 @@
// "Convert into implicitly declared class" "false"
import org.junit.jupiter.api.Test;
import org.<error descr="Cannot resolve symbol 'junit'">junit</error>.jupiter.api.Test;
public class EmptyTest<caret>
{
@@ -8,7 +7,7 @@ public class EmptyTest<caret>
}
@Test
@<error descr="Cannot resolve symbol 'Test'">Test</error>
public void test1() {
}

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "false"
@SuppressWarnings("a")
public class beforeWith<caret>Annotation {
public void main() {

View File

@@ -1,5 +1,3 @@
// "Convert into implicitly declared class" "false"
public class beforeWith<caret>EnhancedMain2 {
public void main(String arg) {
System.out.println("Hello, world!");

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "false"
public class beforeWithConstructo<caret>r {
public beforeWithConstructor(String t) {

View File

@@ -1,6 +1,4 @@
// "Convert into implicitly declared class" "true-preview"
public class be<caret>foreWithEnhancedMain {
<warning descr="Explicit class declaration can be converted into implicitly declared class">public class beforeWithEnhancedMain</warning> {
public static void main() {
System.out.println("Hello, world!");
}

View File

@@ -1,6 +1,4 @@
// "Convert into implicitly declared class" "true-preview"
public class beforeWith<caret>EnhancedMain2 {
<warning descr="Explicit class declaration can be converted into implicitly declared class">public class beforeWith<caret>EnhancedMain2</warning> {
public void main() {
System.out.println("Hello, world!");
}

View File

@@ -1,5 +1,4 @@
// "Convert into implicitly declared class" "false"
public class beforeWithExtendLis<caret>t extends Something {
public class beforeWithExtendList extends <error descr="Cannot resolve symbol 'Something'">Something</error> {
public static void main(String[] args) {
System.out.println("Hello, world!");
}

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "false"
public class be<caret>foreWithGeneric<T> {
public static void main(String[] args) {
System.out.println("Hello, world!");

View File

@@ -0,0 +1,9 @@
import p.*;
<warning descr="Explicit class declaration can be converted into implicitly declared class">public class beforeWithImport<caret>Conflict</warning> {
public static void main(String[] args) {
List a = null;
System.out.println("Hello, world!");
}
}

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "false"
package a;
public class beforeWithPackag<caret>e {

View File

@@ -1,8 +1,7 @@
// "Convert into implicitly declared class" "false"
public class beforeWi<caret>thSyntaxError {
public static void main(String[] args) {
error error error;
<error descr="Cannot resolve symbol 'error'">error</error> error<error descr="';' expected"> </error><error descr="Variable 'error' might not have been initialized">error</error>;
System.out.println("Hello, world!");
}
}

View File

@@ -1,4 +1,3 @@
// "Convert into implicitly declared class" "false"
public class beforeWithUsage<caret>s {
public static void main(String[] args) {

View File

@@ -0,0 +1,7 @@
import test.List;
public class beforeC<caret>onflictModuleImport {
public static void main(String[] args) {
List a = null;
}
}

View File

@@ -0,0 +1,8 @@
import java.util.ArrayList;
import java.util.List;
public class beforeSimpleModuleImport {
public static void main(String[] args) {
List<String> a = new ArrayList<>();
}
}

View File

@@ -0,0 +1,5 @@
import test.List;
public static void m<caret>ain(String[] args) {
List a = null;
}

View File

@@ -0,0 +1,3 @@
public static void m<caret>ain(String[] args) {
List<String> a = new ArrayList<>();
}

View File

@@ -0,0 +1,8 @@
import module java<caret>.base;
public class ModuleImport {
int method() {
List list = null;
return 0;
}
}

View File

@@ -0,0 +1,8 @@
import java.util.L<caret>ist;
public class ModuleImport {
int method() {
List list = null;
return 0;
}
}

View File

@@ -1,26 +1,67 @@
// Copyright 2000-2023 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.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInspection;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.ExplicitToImplicitClassMigrationInspection;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.pom.java.JavaFeature;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.java.JavaBundle;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public class ExplicitToImplicitClassMigrationInspectionInspectionTest extends LightQuickFixParameterizedTestCase {
public class ExplicitToImplicitClassMigrationInspectionInspectionTest extends LightJavaCodeInsightFixtureTestCase {
@NotNull
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new ExplicitToImplicitClassMigrationInspection()};
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_23;
}
@Override
protected String getBasePath() {
return "/inspection/explicitToImplicitClassMigration/";
protected String getTestDataPath() {
return JavaTestUtil.getJavaTestDataPath() + "/inspection/explicitToImplicitClassMigration/";
}
@Override
protected LanguageLevel getLanguageLevel() {
return JavaFeature.IMPLICIT_CLASSES.getMinimumLevel();
public void testAnotherFile() { doNotFind(); }
public void testCaretAtClass() { doTest(); }
public void testExtendsObject() { doTest(); }
public void testInterface() { doNotFind(); }
public void testSeveralSimple() { doNotFind(); }
public void testSimple() { doTest(); }
public void testSimpleWithComments() { doTest(); }
public void testTestClass() { doNotFind(); }
public void testWithAnnotation() { doNotFind(); }
public void testWithConstructor() { doNotFind(); }
public void testWithEnhancedMain() { doTest(); }
public void testWithEnhancedMain2() { doTest(); }
public void testWithExtendList() { doNotFind(); }
public void testWithGeneric() { doNotFind(); }
public void testWithPackage() { doNotFind(); }
public void testWithSyntaxError() { doNotFind(); }
public void testWithUsages() { doNotFind(); }
public void testWithImportConflict() {
myFixture.addClass(
"""
package p;
public class List{}
"""
);
doTest();
}
private void doNotFind() {
myFixture.enableInspections(new ExplicitToImplicitClassMigrationInspection());
myFixture.testHighlighting(true, false, true, "before" + getTestName(false) + ".java");
IntentionAction intention = myFixture.getAvailableIntention(
JavaBundle.message("inspection.explicit.to.implicit.class.migration.fix.name"));
assertNull(intention);
}
private void doTest() {
myFixture.enableInspections(new ExplicitToImplicitClassMigrationInspection());
myFixture.testHighlighting(true, false, true, "before" + getTestName(false) + ".java");
myFixture.checkPreviewAndLaunchAction(myFixture.findSingleIntention(
JavaBundle.message("inspection.explicit.to.implicit.class.migration.fix.name")));
myFixture.checkResultByFile("after" + getTestName(false) + ".java");
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2000-2024 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.
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInspection;
import com.intellij.JavaTestUtil;
@@ -44,4 +44,14 @@ public class ImplicitToExplicitClassBackwardMigrationInspectionTest extends Ligh
public void testAdjustComments() { doTest(); }
public void testWithPrint() { doTest(); }
public void testSimpleModuleImport() { doTest(); }
public void testConflictModuleImport() {
myFixture.addClass("""
package test;
public class List {}
""");
doTest();
}
}

View File

@@ -1,12 +1,20 @@
// Copyright 2000-2018 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.
// 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.ipp.imports;
import com.intellij.testFramework.LightProjectDescriptor;
import com.siyeh.IntentionPowerPackBundle;
import com.siyeh.ipp.IPPTestCase;
import org.jetbrains.annotations.NotNull;
public class ReplaceOnDemandImportIntentionTest extends IPPTestCase {
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return JAVA_23;
}
public void testStaticImport() { doTest(); }
public void testModuleImport() { doTest(); }
@Override
protected String getIntentionName() {