From 36bab0a817ea3539bd5f004404070c5f28e581c4 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Thu, 4 Jan 2024 10:42:18 +0100 Subject: [PATCH] [java-inspections] IDEA-341641 Intention to convert implicit class to explicit and vice versa - new inspection to convert ordinary classes into implicitly declared classes GitOrigin-RevId: 4a1756d52b672edafcb43a5f9f725a64bb19fa6d --- ...citToImplicitClassMigrationInspection.java | 165 ++++++++++++++++++ java/java-impl/src/META-INF/JavaPlugin.xml | 7 + .../ExplicitToImplicitClassMigration.html | 24 +++ .../src/com/intellij/psi/PsiClass.java | 4 +- .../com/intellij/psi/PsiJavaParserFacade.java | 12 ++ .../psi/impl/PsiJavaParserFacadeImpl.java | 14 ++ .../afterSimple.java | 5 + .../afterSimpleWithComments.java | 7 + .../beforeAnotherFile.java | 6 + .../beforeInterface.java | 7 + .../beforeSeveralSimple.java | 11 ++ .../beforeSimple.java | 7 + .../beforeSimpleWithComments.java | 12 ++ .../beforeWithConstructor.java | 10 ++ .../beforeWithExtendList.java | 6 + .../beforeWithGeneric.java | 6 + .../beforeWithPackage.java | 8 + .../beforeWithSyntaxError.java | 8 + .../beforeWithUsages.java | 8 + ...lassMigrationInspectionInspectionTest.java | 25 +++ .../resources/messages/JavaBundle.properties | 2 + 21 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 java/java-impl-inspections/src/com/intellij/codeInspection/ExplicitToImplicitClassMigrationInspection.java create mode 100644 java/java-impl/src/inspectionDescriptions/ExplicitToImplicitClassMigration.html create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimple.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimpleWithComments.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeAnotherFile.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeInterface.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSeveralSimple.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimple.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimpleWithComments.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithConstructor.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithExtendList.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithGeneric.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithPackage.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithSyntaxError.java create mode 100644 java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithUsages.java create mode 100644 java/java-tests/testSrc/com/intellij/java/codeInspection/ExplicitToImplicitClassMigrationInspectionInspectionTest.java diff --git a/java/java-impl-inspections/src/com/intellij/codeInspection/ExplicitToImplicitClassMigrationInspection.java b/java/java-impl-inspections/src/com/intellij/codeInspection/ExplicitToImplicitClassMigrationInspection.java new file mode 100644 index 000000000000..71c9f5828409 --- /dev/null +++ b/java/java-impl-inspections/src/com/intellij/codeInspection/ExplicitToImplicitClassMigrationInspection.java @@ -0,0 +1,165 @@ +// 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.codeInspection; + +import com.intellij.codeInsight.daemon.impl.analysis.HighlightingFeature; +import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil; +import com.intellij.java.JavaBundle; +import com.intellij.modcommand.ModPsiUpdater; +import com.intellij.modcommand.PsiUpdateModCommandQuickFix; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.search.PackageScope; +import com.intellij.psi.search.PsiSearchHelper; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.util.PsiMethodUtil; +import com.intellij.psi.util.PsiTreeUtil; +import com.siyeh.ig.psiutils.CommentTracker; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +public final class ExplicitToImplicitClassMigrationInspection extends AbstractBaseJavaLocalInspectionTool { + + private static final String JAVA_SUFFIX = ".java"; + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { + if (!HighlightingFeature.IMPLICIT_CLASSES.isAvailable(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR; + return new JavaElementVisitor() { + @Override + public void visitClass(@NotNull PsiClass aClass) { + if (aClass.isInterface() || aClass.isRecord() || aClass.isEnum()) { + return; + } + + if (aClass.getContainingClass() != null) { + return; + } + PsiJavaFile file = (PsiJavaFile)aClass.getContainingFile(); + if (file.getPackageStatement() != null) { + return; + } + + if (file.getClasses().length != 1) { + return; + } + + PsiJavaModule javaModule = JavaModuleGraphUtil.findDescriptorByElement(aClass); + if (javaModule != null) { + return; + } + + String fileName = file.getName(); + if (!fileName.endsWith(JAVA_SUFFIX)) { + return; + } + + String className = aClass.getName(); + if (className == null || !className.equals(fileName.substring(0, fileName.length() - JAVA_SUFFIX.length()))) { + return; + } + + if (aClass.hasTypeParameters()) { + return; + } + + if (!PsiMethodUtil.hasMainMethod(aClass)) { + return; + } + + if (aClass.getExtendsListTypes().length != 0 || aClass.getImplementsListTypes().length != 0) { + return; + } + + if (aClass.hasModifierProperty(PsiModifier.SEALED) || aClass.hasModifierProperty(PsiModifier.ABSTRACT)) { + return; + } + + PsiMethod[] constructors = aClass.getConstructors(); + if (constructors.length > 0) { + if (constructors.length > 1) { + return; + } + + PsiMethod constructor = constructors[0]; + if (constructor.hasParameters() || + constructor.hasModifierProperty(PsiModifier.PRIVATE) || + (constructor.getBody() != null && constructor.getBody().getStatements().length > 0)) { + return; + } + } + + Project project = aClass.getProject(); + PsiPackage aPackage = JavaPsiFacade.getInstance(project).findPackage(file.getPackageName()); + if (aPackage == null) { + return; + } + PsiIdentifier classIdentifier = aClass.getNameIdentifier(); + if (classIdentifier == null) { + return; + } + + PackageScope scope = new PackageScope(aPackage, false, false); + if (isOnTheFly) { + final PsiSearchHelper searchHelper = PsiSearchHelper.getInstance(project); + final PsiSearchHelper.SearchCostResult cost = + searchHelper.isCheapEnoughToSearch(className, scope, null, null); + if (cost == PsiSearchHelper.SearchCostResult.TOO_MANY_OCCURRENCES) { + return; + } + } + + PsiReference first = ReferencesSearch.search(aClass, scope).findFirst(); + if (first != null) { + return; + } + PsiElement lBrace = aClass.getLBrace(); + PsiElement rBrace = aClass.getRBrace(); + if (lBrace == null || rBrace == null) { + return; + } + + if (PsiTreeUtil.hasErrorElements(aClass)) { + return; + } + + holder.registerProblem(classIdentifier, JavaBundle.message("inspection.explicit.to.implicit.class.migration.name"), + new ReplaceWithImplicitClassFix()); + } + }; + } + + + private static class ReplaceWithImplicitClassFix extends PsiUpdateModCommandQuickFix { + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return JavaBundle.message("inspection.explicit.to.implicit.class.migration.fix.name"); + } + + @Override + protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) { + PsiClass psiClass = PsiTreeUtil.getParentOfType(element, PsiClass.class); + if (psiClass == null) { + return; + } + StringBuilder builder = new StringBuilder(); + PsiElement lBrace = psiClass.getLBrace(); + PsiElement rBrace = psiClass.getRBrace(); + if (lBrace == null || rBrace == null) { + return; + } + PsiElement psiElement = lBrace.getNextSibling(); + CommentTracker tracker = new CommentTracker(); + while (psiElement != null && psiElement != rBrace) { + builder.append(tracker.text(psiElement)); + psiElement = psiElement.getNextSibling(); + } + PsiImplicitClass newClass = PsiElementFactory.getInstance(project).createImplicitClassFromText(builder.toString(), psiClass); + PsiElement replaced = tracker.replace(psiClass, newClass); + tracker.insertCommentsBefore(replaced); + } + } +} diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 748b2c01eb30..f65d39795a90 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -1534,6 +1534,13 @@ implementationClass="com.intellij.codeInspection.ImplicitToExplicitClassBackwardMigrationInspection" bundle="messages.JavaBundle" key="inspection.implicit.to.explicit.class.backward.migration.name"/> + + +Reports ordinary classes, which can be converted into implicitly declared classes +

Example:

+

+  public class Sample {
+      public static void main(String[] args) {
+          System.out.println("Hello, world!");
+      }
+  }
+
+

After the quick-fix is applied:

+

+  public static void main(String[] args) {
+      System.out.println("Hello, world!");
+  }
+
+ +

+ Implicitly declared classes appeared in Java 21 (Preview). +

+

New in 2024.1

+ + \ No newline at end of file diff --git a/java/java-psi-api/src/com/intellij/psi/PsiClass.java b/java/java-psi-api/src/com/intellij/psi/PsiClass.java index 7d4a9939bf1c..fc9008b7bcbf 100644 --- a/java/java-psi-api/src/com/intellij/psi/PsiClass.java +++ b/java/java-psi-api/src/com/intellij/psi/PsiClass.java @@ -90,7 +90,7 @@ public interface PsiClass /** * Returns the array of class types for the classes that this class or interface extends. * - * @return the array of extended class types, or an empty list for anonymous classes and unnamed classes. + * @return the array of extended class types, or an empty list for anonymous classes and implicitly declared classes. */ PsiClassType @NotNull [] getExtendsListTypes(); @@ -98,7 +98,7 @@ public interface PsiClass * Returns the array of class types for the interfaces that this class implements. * * @return the array of extended class types, or an empty list for anonymous classes, - * enums, annotation types and unnamed classes + * enums, annotation types and implicitly declared classes */ PsiClassType @NotNull [] getImplementsListTypes(); diff --git a/java/java-psi-api/src/com/intellij/psi/PsiJavaParserFacade.java b/java/java-psi-api/src/com/intellij/psi/PsiJavaParserFacade.java index e68b4c988316..ba3d59fb716e 100644 --- a/java/java-psi-api/src/com/intellij/psi/PsiJavaParserFacade.java +++ b/java/java-psi-api/src/com/intellij/psi/PsiJavaParserFacade.java @@ -98,6 +98,18 @@ public interface PsiJavaParserFacade { @NotNull PsiParameter createParameterFromText(@NotNull @NonNls String text, @Nullable PsiElement context) throws IncorrectOperationException; + /** + * Creates an implicit class from the specified body text (the text between the braces). + * + * @param body the body text of the class to create. + * @param context the PSI element used as context for resolving references which cannot be resolved + * within the class. + * @return created class instance. + * @throws IncorrectOperationException if the text is not a valid class body. + */ + @NotNull + PsiImplicitClass createImplicitClassFromText(@NotNull String body, @Nullable PsiElement context) throws IncorrectOperationException; + /** * Creates a Java record header from the specified text (excluding parentheses). * diff --git a/java/java-psi-impl/src/com/intellij/psi/impl/PsiJavaParserFacadeImpl.java b/java/java-psi-impl/src/com/intellij/psi/impl/PsiJavaParserFacadeImpl.java index 1413b5d73701..7c2a6f739f04 100644 --- a/java/java-psi-impl/src/com/intellij/psi/impl/PsiJavaParserFacadeImpl.java +++ b/java/java-psi-impl/src/com/intellij/psi/impl/PsiJavaParserFacadeImpl.java @@ -139,6 +139,20 @@ public class PsiJavaParserFacadeImpl implements PsiJavaParserFacade { return classes[0]; } + @NotNull + @Override + public PsiImplicitClass createImplicitClassFromText(@NotNull String body, @Nullable PsiElement context) throws IncorrectOperationException { + PsiJavaFile aFile = createDummyJavaFile(body); + PsiClass[] classes = aFile.getClasses(); + if (classes.length != 1) { + throw new IncorrectOperationException("Incorrect class '" + body + "'"); + } + if (classes[0] instanceof PsiImplicitClass) { + return (PsiImplicitClass)classes[0]; + } + throw new IncorrectOperationException("Incorrect implicit class '" + body + "'"); + } + @NotNull public PsiClass createRecord(@NotNull String name) throws IncorrectOperationException { return createRecordFromText("public record " + name + "() { }"); diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimple.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimple.java new file mode 100644 index 000000000000..1b48f9906514 --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimple.java @@ -0,0 +1,5 @@ +// "Convert into implicitly declared class" "true-preview" + +public static void main(String[] args) { + System.out.println("Hello, world!"); +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimpleWithComments.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimpleWithComments.java new file mode 100644 index 000000000000..f14b169c1297 --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/afterSimpleWithComments.java @@ -0,0 +1,7 @@ +// "Convert into implicitly declared class" "true-preview" + +/** + * comments + */ /*comments2*/ public static void main(String[] args) { + System.out.println("Hello, world!"); +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeAnotherFile.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeAnotherFile.java new file mode 100644 index 000000000000..55b5f1ac8c4b --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeAnotherFile.java @@ -0,0 +1,6 @@ +// "Convert into implicitly declared class" "false" +public class AnotherFileName { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeInterface.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeInterface.java new file mode 100644 index 000000000000..cb09f551610c --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeInterface.java @@ -0,0 +1,7 @@ +// "Convert into implicitly declared class" "false" + +public interface beforeInterface { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSeveralSimple.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSeveralSimple.java new file mode 100644 index 000000000000..5d3b85ce0c3e --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSeveralSimple.java @@ -0,0 +1,11 @@ +// "Convert into implicitly declared class" "false" + +public class beforeSeveralSimple { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} + +class SecondClass{ + +} \ No newline at end of file diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimple.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimple.java new file mode 100644 index 000000000000..06e850aa4df6 --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimple.java @@ -0,0 +1,7 @@ +// "Convert into implicitly declared class" "true-preview" + +public class beforeSimple { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimpleWithComments.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimpleWithComments.java new file mode 100644 index 000000000000..0c855232fd19 --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeSimpleWithComments.java @@ -0,0 +1,12 @@ +// "Convert into implicitly declared class" "true-preview" + +/** + * comments + */ + +public /*comments2*/ class beforeSimpleWithComments { + + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithConstructor.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithConstructor.java new file mode 100644 index 000000000000..9bdfcfd52a0c --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithConstructor.java @@ -0,0 +1,10 @@ +// "Convert into implicitly declared class" "false" +public class beforeWithConstructor { + + public beforeWithConstructor(String t) { + } + + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithExtendList.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithExtendList.java new file mode 100644 index 000000000000..69c709d87167 --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithExtendList.java @@ -0,0 +1,6 @@ +// "Convert into implicitly declared class" "false" +public class beforeWithExtendList extends Something { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithGeneric.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithGeneric.java new file mode 100644 index 000000000000..f032b6f88232 --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithGeneric.java @@ -0,0 +1,6 @@ +// "Convert into implicitly declared class" "false" +public class beforeWithGeneric { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithPackage.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithPackage.java new file mode 100644 index 000000000000..da201be6470a --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithPackage.java @@ -0,0 +1,8 @@ +// "Convert into implicitly declared class" "false" +package a; + +public class beforeWithPackage { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithSyntaxError.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithSyntaxError.java new file mode 100644 index 000000000000..463861875443 --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithSyntaxError.java @@ -0,0 +1,8 @@ +// "Convert into implicitly declared class" "false" +public class beforeWithSyntaxError { + + public static void main(String[] args) { + error error error; + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithUsages.java b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithUsages.java new file mode 100644 index 000000000000..8ae601f67d5c --- /dev/null +++ b/java/java-tests/testData/inspection/explicitToImplicitClassMigration/beforeWithUsages.java @@ -0,0 +1,8 @@ +// "Convert into implicitly declared class" "false" +public class beforeWithUsages { + + public static void main(String[] args) { + new beforeWithUsages(); + System.out.println("Hello, world!"); + } +} diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/ExplicitToImplicitClassMigrationInspectionInspectionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/ExplicitToImplicitClassMigrationInspectionInspectionTest.java new file mode 100644 index 000000000000..bc8ff16dafa8 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/ExplicitToImplicitClassMigrationInspectionInspectionTest.java @@ -0,0 +1,25 @@ +// 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. +package com.intellij.java.codeInspection; + +import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase; +import com.intellij.codeInspection.ExplicitToImplicitClassMigrationInspection; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.pom.java.LanguageLevel; +import org.jetbrains.annotations.NotNull; + +public class ExplicitToImplicitClassMigrationInspectionInspectionTest extends LightQuickFixParameterizedTestCase { + @Override + protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() { + return new LocalInspectionTool[]{new ExplicitToImplicitClassMigrationInspection()}; + } + + @Override + protected String getBasePath() { + return "/inspection/explicitToImplicitClassMigration/"; + } + + @Override + protected LanguageLevel getLanguageLevel() { + return LanguageLevel.JDK_22_PREVIEW; + } +} \ No newline at end of file diff --git a/java/openapi/resources/messages/JavaBundle.properties b/java/openapi/resources/messages/JavaBundle.properties index ef61c44a2ef6..1f60b3c92750 100644 --- a/java/openapi/resources/messages/JavaBundle.properties +++ b/java/openapi/resources/messages/JavaBundle.properties @@ -748,6 +748,8 @@ inspection.string.template.migration.concatenation.message=Concatenation can be inspection.string.template.migration.name=String template can be used inspection.implicit.to.explicit.class.backward.migration.name=Implicitly declared class can be replaced with ordinary class inspection.implicit.to.explicit.class.backward.migration.fix.name=Convert implicitly declared class into regular class +inspection.explicit.to.implicit.class.migration.name=Explicit class declaration can be converted into implicitly declared class +inspection.explicit.to.implicit.class.migration.fix.name=Convert into implicitly declared class inspection.inconsistent.text.block.indent.name=Inconsistent whitespace indentation in text block inspection.inconsistent.text.block.indent.message=Text block indent consists of tabs and spaces inspection.inconsistent.text.block.indent.spaces.to.tabs.one.to.one.fix=Replace spaces with tabs (1 space = 1 tab)