[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
This commit is contained in:
Mikhail Pyltsin
2024-01-04 10:44:59 +00:00
committed by intellij-monorepo-bot
parent 164ad4178b
commit 36bab0a817
21 changed files with 352 additions and 2 deletions
@@ -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);
}
}
}
@@ -1534,6 +1534,13 @@
implementationClass="com.intellij.codeInspection.ImplicitToExplicitClassBackwardMigrationInspection"
bundle="messages.JavaBundle"
key="inspection.implicit.to.explicit.class.backward.migration.name"/>
<localInspection groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids" language="JAVA"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids21" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.ExplicitToImplicitClassMigrationInspection"
bundle="messages.JavaBundle"
editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
key="inspection.explicit.to.implicit.class.migration.name"/>
<localInspection groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids" language="JAVA" shortName="TextBlockBackwardMigration"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.language.level.specific.issues.and.migration.aids15" enabledByDefault="true" level="INFORMATION"
@@ -0,0 +1,24 @@
<html>
<body>
Reports ordinary classes, which can be converted into implicitly declared classes
<p><b>Example:</b></p>
<pre><code>
public class Sample {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
public static void main(String[] args) {
System.out.println("Hello, world!");
}
</code></pre>
<!-- tooltip end -->
<p>
Implicitly declared classes appeared in Java 21 (Preview).
</p>
<p><small>New in 2024.1</small></p>
</body>
</html>
@@ -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();
@@ -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).
*
@@ -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 + "() { }");
@@ -0,0 +1,5 @@
// "Convert into implicitly declared class" "true-preview"
public static void main(String[] args) {
System.out.println("Hello, world!");
}
@@ -0,0 +1,7 @@
// "Convert into implicitly declared class" "true-preview"
/**
* comments
*/ /*comments2*/ public static void main(String[] args) {
System.out.println("Hello, world!");
}
@@ -0,0 +1,6 @@
// "Convert into implicitly declared class" "false"
public class AnotherFil<caret>eName {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,7 @@
// "Convert into implicitly declared class" "false"
public interface beforeInterfa<caret>ce {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,11 @@
// "Convert into implicitly declared class" "false"
public class beforeSeveralSi<caret>mple {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
class SecondClass{
}
@@ -0,0 +1,7 @@
// "Convert into implicitly declared class" "true-preview"
public class beforeSi<caret>mple {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,12 @@
// "Convert into implicitly declared class" "true-preview"
/**
* comments
*/
public /*comments2*/ class beforeSi<caret>mpleWithComments {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,10 @@
// "Convert into implicitly declared class" "false"
public class beforeWithConstructo<caret>r {
public beforeWithConstructor(String t) {
}
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,6 @@
// "Convert into implicitly declared class" "false"
public class beforeWithExtendLis<caret>t extends Something {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,6 @@
// "Convert into implicitly declared class" "false"
public class be<caret>foreWithGeneric<T> {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,8 @@
// "Convert into implicitly declared class" "false"
package a;
public class beforeWithPackag<caret>e {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,8 @@
// "Convert into implicitly declared class" "false"
public class beforeWi<caret>thSyntaxError {
public static void main(String[] args) {
error error error;
System.out.println("Hello, world!");
}
}
@@ -0,0 +1,8 @@
// "Convert into implicitly declared class" "false"
public class beforeWithUsage<caret>s {
public static void main(String[] args) {
new beforeWithUsages();
System.out.println("Hello, world!");
}
}
@@ -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;
}
}
@@ -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)