diff --git a/java/java-impl-refactorings/src/com/intellij/refactoring/convertToInstanceMethod/ConvertToInstanceMethodHandler.java b/java/java-impl-refactorings/src/com/intellij/refactoring/convertToInstanceMethod/ConvertToInstanceMethodHandler.java index a2ae284e6e2f..540df5dbcc9d 100644 --- a/java/java-impl-refactorings/src/com/intellij/refactoring/convertToInstanceMethod/ConvertToInstanceMethodHandler.java +++ b/java/java-impl-refactorings/src/com/intellij/refactoring/convertToInstanceMethod/ConvertToInstanceMethodHandler.java @@ -5,23 +5,27 @@ import com.intellij.java.refactoring.JavaRefactoringBundle; import com.intellij.lang.ContextAwareActionHandler; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.ScrollType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.NlsContexts; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiIdentifier; -import com.intellij.psi.PsiMethod; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiUtil; import com.intellij.refactoring.HelpID; import com.intellij.refactoring.RefactoringActionHandler; import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.actions.BaseRefactoringAction; import com.intellij.refactoring.util.CommonRefactoringUtil; +import com.intellij.util.containers.ContainerUtil; import com.siyeh.ig.psiutils.MethodUtils; +import com.siyeh.ig.psiutils.VariableAccessUtils; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; + public class ConvertToInstanceMethodHandler implements RefactoringActionHandler, ContextAwareActionHandler { private static final Logger LOG = Logger.getInstance(ConvertToInstanceMethodHandler.class); @@ -50,16 +54,72 @@ public class ConvertToInstanceMethodHandler implements RefactoringActionHandler, @Override public void invoke(@NotNull Project project, PsiElement @NotNull [] elements, DataContext dataContext) { if (elements.length != 1 || !(elements[0] instanceof PsiMethod method)) return; - @NotNull PossibleInstanceQualifiers result = PossibleInstanceQualifiers.build(method); - if (result instanceof PossibleInstanceQualifiers.Invalid error) { + try { + new ConvertToInstanceMethodDialog(method, calculatePossibleInstanceQualifiers(method)).show(); + } + catch (CommonRefactoringUtil.RefactoringErrorHintException e) { + if (ApplicationManager.getApplication().isUnitTestMode()) throw e; Editor editor = CommonDataKeys.EDITOR.getData(dataContext); - CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage(error.message()), - getRefactoringName(), - HelpID.CONVERT_TO_INSTANCE_METHOD); + CommonRefactoringUtil.showErrorHint(project, editor, RefactoringBundle.getCannotRefactorMessage(e.getMessage()), + getRefactoringName(), HelpID.CONVERT_TO_INSTANCE_METHOD); } - else if (result instanceof PossibleInstanceQualifiers.Valid targetQualifiers) { - new ConvertToInstanceMethodDialog(method, targetQualifiers.toArray()).show(); + } + + private static Object @NotNull [] calculatePossibleInstanceQualifiers(@NotNull PsiMethod method) { + if (!method.hasModifierProperty(PsiModifier.STATIC)) { + throw new CommonRefactoringUtil.RefactoringErrorHintException( + JavaRefactoringBundle.message("convertToInstanceMethod.method.is.not.static", method.getName())); } + List qualifiers = new ArrayList<>(); + final PsiParameter[] parameters = method.getParameterList().getParameters(); + boolean classTypesFound = false; + boolean resolvableClassesFound = false; + for (final PsiParameter parameter : parameters) { + if (VariableAccessUtils.variableIsAssigned(parameter, parameter.getDeclarationScope())) continue; + final PsiType type = parameter.getType(); + if (type instanceof PsiClassType classType) { + classTypesFound = true; + final PsiClass psiClass = classType.resolve(); + if (psiClass != null && !(psiClass instanceof PsiTypeParameter)) { + resolvableClassesFound = true; + if (method.getManager().isInProject(psiClass)) { + qualifiers.add(parameter); + } + } + } + } + PsiClass containingClass = method.getContainingClass(); + boolean canHaveUsableConstructor = containingClass != null && + containingClass.getQualifiedName() != null && + !containingClass.isEnum() && + !PsiUtil.isInnerClass(containingClass) && + !(containingClass instanceof PsiImplicitClass); + if (canHaveUsableConstructor) { + PsiMethod[] constructors = containingClass.getConstructors(); + boolean noArgConstructor = + constructors.length == 0 || ContainerUtil.exists(constructors, constructor -> constructor.getParameterList().isEmpty()); + if (noArgConstructor) { + qualifiers.add("this / new " + containingClass.getName() + "()"); + } + } + if (!qualifiers.isEmpty()) { + return qualifiers.toArray(); + } + + String message; + if (!classTypesFound) { + message = JavaRefactoringBundle.message("convertToInstanceMethod.no.parameters.with.reference.type"); + } + else if (!resolvableClassesFound) { + message = JavaRefactoringBundle.message("convertToInstanceMethod.all.reference.type.parameters.have.unknown.types"); + } + else { + message = JavaRefactoringBundle.message("convertToInstanceMethod.all.reference.type.parameters.are.not.in.project"); + } + if (canHaveUsableConstructor) { + message += " " + JavaRefactoringBundle.message("convertToInstanceMethod.no.default.ctor"); + } + throw new CommonRefactoringUtil.RefactoringErrorHintException(message); } @Override diff --git a/java/java-impl-refactorings/src/com/intellij/refactoring/convertToInstanceMethod/PossibleInstanceQualifiers.java b/java/java-impl-refactorings/src/com/intellij/refactoring/convertToInstanceMethod/PossibleInstanceQualifiers.java deleted file mode 100644 index d3e96157c9f0..000000000000 --- a/java/java-impl-refactorings/src/com/intellij/refactoring/convertToInstanceMethod/PossibleInstanceQualifiers.java +++ /dev/null @@ -1,105 +0,0 @@ -// 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.refactoring.convertToInstanceMethod; - -import com.intellij.java.refactoring.JavaRefactoringBundle; -import com.intellij.openapi.util.NlsContexts; -import com.intellij.psi.*; -import com.intellij.util.ArrayUtil; -import com.intellij.util.containers.ContainerUtil; -import com.siyeh.ig.psiutils.VariableAccessUtils; -import one.util.streamex.StreamEx; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; - -/** - * A container for possible qualifiers for 'Convert to instance method' refactoring - */ -@ApiStatus.Internal -public sealed interface PossibleInstanceQualifiers { - /** - * Represents valid list of qualifiers - * - * @param qualifiers list of qualifier parameters - * @param thisOrNewQualifierName name of 'this/new Object' qualifier, or null if such a name is impossible to use in this context - */ - record Valid(@NotNull List<@NotNull PsiParameter> qualifiers, @Nullable String thisOrNewQualifierName) implements PossibleInstanceQualifiers { - Object[] toArray() { - if (thisOrNewQualifierName == null) { - return ArrayUtil.toObjectArray(qualifiers); - } - return StreamEx.of(qualifiers).append(thisOrNewQualifierName).toArray(); - } - } - - /** - * Represents invalid context in case if conversion to an instance method is not supported - * - * @param message message explaining why conversion is not possible - */ - record Invalid(@NlsContexts.DialogMessage String message) implements PossibleInstanceQualifiers { - } - - /** - * @param method method to convert - * @return PossibleInstanceQualifiers instance - */ - static @NotNull PossibleInstanceQualifiers build(@NotNull PsiMethod method) { - if (!method.hasModifierProperty(PsiModifier.STATIC)) { - return new Invalid(JavaRefactoringBundle.message("convertToInstanceMethod.method.is.not.static", method.getName())); - } - List targetQualifiers = new ArrayList<>(); - String thisOrNewQualifierName = null; - final PsiParameter[] parameters = method.getParameterList().getParameters(); - boolean classTypesFound = false; - boolean resolvableClassesFound = false; - for (final PsiParameter parameter : parameters) { - if (VariableAccessUtils.variableIsAssigned(parameter, parameter.getDeclarationScope())) continue; - final PsiType type = parameter.getType(); - if (type instanceof PsiClassType) { - classTypesFound = true; - final PsiClass psiClass = ((PsiClassType)type).resolve(); - if (psiClass != null && !(psiClass instanceof PsiTypeParameter)) { - resolvableClassesFound = true; - if (method.getManager().isInProject(psiClass)) { - targetQualifiers.add(parameter); - } - } - } - } - PsiClass containingClass = method.getContainingClass(); - if (containingClass == null || containingClass.getQualifiedName() == null) { - return new Invalid(JavaRefactoringBundle.message("convertToInstanceMethod.unsupported.containing.class")); - } - String className = containingClass.getName(); - if (!containingClass.isEnum() && !(containingClass instanceof PsiImplicitClass)) { - PsiMethod[] constructors = containingClass.getConstructors(); - boolean noArgConstructor = - constructors.length == 0 || ContainerUtil.exists(constructors, constructor -> constructor.getParameterList().isEmpty()); - if (noArgConstructor) { - thisOrNewQualifierName = "this / new " + className + "()"; - } - } - - if (!targetQualifiers.isEmpty() || thisOrNewQualifierName != null) { - return new Valid(targetQualifiers, thisOrNewQualifierName); - } - String message; - if (!classTypesFound) { - message = JavaRefactoringBundle.message("convertToInstanceMethod.no.parameters.with.reference.type"); - } - else if (!resolvableClassesFound) { - message = JavaRefactoringBundle.message("convertToInstanceMethod.all.reference.type.parameters.have.unknown.types"); - } - else { - message = JavaRefactoringBundle.message("convertToInstanceMethod.all.reference.type.parameters.are.not.in.project"); - } - if (!(containingClass instanceof PsiImplicitClass)) { - message += " " + JavaRefactoringBundle.message("convertToInstanceMethod.no.default.ctor"); - } - return new Invalid(message); - } -} diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/AnonymousClass.java b/java/java-tests/testData/refactoring/convertToInstance8Method/AnonymousClass.java new file mode 100644 index 000000000000..324e2bce7d28 --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/AnonymousClass.java @@ -0,0 +1,8 @@ +class X { + + void foo() { + new Object() { + static void x(X x) {} + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/AnonymousClass.java.after b/java/java-tests/testData/refactoring/convertToInstance8Method/AnonymousClass.java.after new file mode 100644 index 000000000000..b892d15d60c9 --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/AnonymousClass.java.after @@ -0,0 +1,9 @@ +class X { + + void x() {} + + void foo() { + new Object() { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/Enum.java b/java/java-tests/testData/refactoring/convertToInstance8Method/Enum.java new file mode 100644 index 000000000000..0b83f3f17c89 --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/Enum.java @@ -0,0 +1,6 @@ +enum E { + A, B; + static boolean x(E e) { + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/Enum.java.after b/java/java-tests/testData/refactoring/convertToInstance8Method/Enum.java.after new file mode 100644 index 000000000000..d4ca2385d9e2 --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/Enum.java.after @@ -0,0 +1,7 @@ +enum E { + A, B; + + boolean x() { + return true; + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/ImplicitClass.java b/java/java-tests/testData/refactoring/convertToInstance8Method/ImplicitClass.java new file mode 100644 index 000000000000..d82140acbbe6 --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/ImplicitClass.java @@ -0,0 +1,2 @@ + +static void x() \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/InnerClass.java b/java/java-tests/testData/refactoring/convertToInstance8Method/InnerClass.java new file mode 100644 index 000000000000..85a5189c4573 --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/InnerClass.java @@ -0,0 +1,12 @@ +class X { + + class Inner { + static void print(X x) { + System.out.println(x); + } + } + + public static void main(String[] args) { + Inner.print(new X()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/InnerClass.java.after b/java/java-tests/testData/refactoring/convertToInstance8Method/InnerClass.java.after new file mode 100644 index 000000000000..05b481ead15e --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/InnerClass.java.after @@ -0,0 +1,13 @@ +class X { + + void print() { + System.out.println(this); + } + + class Inner { + } + + public static void main(String[] args) { + new X().print(); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/NestedClass.java b/java/java-tests/testData/refactoring/convertToInstance8Method/NestedClass.java new file mode 100644 index 000000000000..2c2178d6c52d --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/NestedClass.java @@ -0,0 +1,12 @@ +class X { + + static class Nested { + static void print(X x) { + System.out.println(x); + } + } + + public static void main(String[] args) { + Nested.print(new X()); + } +} \ No newline at end of file diff --git a/java/java-tests/testData/refactoring/convertToInstance8Method/NestedClass.java.after b/java/java-tests/testData/refactoring/convertToInstance8Method/NestedClass.java.after new file mode 100644 index 000000000000..b009c142d214 --- /dev/null +++ b/java/java-tests/testData/refactoring/convertToInstance8Method/NestedClass.java.after @@ -0,0 +1,12 @@ +class X { + + static class Nested { + void print(X x) { + System.out.println(x); + } + } + + public static void main(String[] args) { + new Nested().print(new X()); + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstance8MethodTest.java b/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstance8MethodTest.java index cf38cb51d859..f5161cdc1e8d 100644 --- a/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstance8MethodTest.java +++ b/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstance8MethodTest.java @@ -1,9 +1,11 @@ // 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.refactoring.convertToInstanceMethod; +import com.intellij.java.refactoring.JavaRefactoringBundle; import com.intellij.openapi.util.text.StringUtil; import com.intellij.pom.java.LanguageLevel; import com.intellij.refactoring.BaseRefactoringProcessor; +import com.intellij.refactoring.util.CommonRefactoringUtil; public class ConvertToInstance8MethodTest extends ConvertToInstanceMethodTest { @Override @@ -17,7 +19,21 @@ public class ConvertToInstance8MethodTest extends ConvertToInstanceMethodTest { public void testThisInsteadOfNoQualifier() { doTest(0); } public void testMethodReferenceAcceptableBySecondSearch() { doTest(0); } public void testConvertToInstanceMethodOfTheSameClass() { doTest(0); } - public void testStaticMethodOfInterfaceWithNonAccessibleInheritor() { doTest(0); } + public void testStaticMethodOfInterfaceWithNonAccessibleInheritor() { doTest(0, null, "I i", "this / new I()"); } + public void testEnum() { doTest(0, null, "E e"); } + public void testAnonymousClass() { doTest(0, null, "X x"); } + public void testInnerClass() { doTest(0, null, "X x"); } + public void testNestedClass() { doTest(1, null, "X x", "this / new Nested()"); } + + public void testImplicitClass() { + try { + doTestException(); + fail(); + } + catch (CommonRefactoringUtil.RefactoringErrorHintException e) { + assertEquals(JavaRefactoringBundle.message("convertToInstanceMethod.no.parameters.with.reference.type"), e.getMessage()); + } + } public void testConvertToInstanceMethodOfTheSameClassWithTypeParams() { try { diff --git a/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstanceMethodTest.java b/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstanceMethodTest.java index fd7c9e6184dc..598661ccddaa 100644 --- a/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstanceMethodTest.java +++ b/java/java-tests/testSrc/com/intellij/java/refactoring/convertToInstanceMethod/ConvertToInstanceMethodTest.java @@ -4,6 +4,7 @@ package com.intellij.java.refactoring.convertToInstanceMethod; import com.intellij.JavaTestUtil; import com.intellij.java.refactoring.LightRefactoringTestCase; import com.intellij.pom.java.LanguageLevel; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiModifier; import com.intellij.refactoring.BaseRefactoringProcessor; import com.intellij.refactoring.convertToInstanceMethod.ConvertToInstanceMethodDialog; @@ -14,6 +15,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; +import java.util.Arrays; public class ConvertToInstanceMethodTest extends LightRefactoringTestCase { @NotNull @@ -46,14 +48,19 @@ public class ConvertToInstanceMethodTest extends LightRefactoringTestCase { doTest(targetParameter, VisibilityUtil.ESCALATE_VISIBILITY); } - private void doTest(int targetParameter, String visibility) { + protected void doTest(int targetParameter, String visibility, String... options) { final String filePath = getBasePath() + getTestName(false) + ".java"; configureByFile(filePath); - UiInterceptors.register(new ConvertToInstanceMethodDialogUiInterceptor(targetParameter, visibility)); + UiInterceptors.register(new ConvertToInstanceMethodDialogUiInterceptor(targetParameter, visibility, options)); new ConvertToInstanceMethodHandler().invoke(getProject(), getEditor(), getFile(), getCurrentEditorDataContext()); checkResultByFile(filePath + ".after"); } + protected void doTestException() { + configureByFile(getBasePath() + getTestName(false) + ".java"); + new ConvertToInstanceMethodHandler().invoke(getProject(), getEditor(), getFile(), getCurrentEditorDataContext()); + } + protected String getBasePath() { return "/refactoring/convertToInstanceMethod/"; } @@ -66,11 +73,13 @@ public class ConvertToInstanceMethodTest extends LightRefactoringTestCase { private static class ConvertToInstanceMethodDialogUiInterceptor extends UiInterceptors.UiInterceptor { private final int myTargetParameter; private final String myVisibility; + private final String[] myOptions; - ConvertToInstanceMethodDialogUiInterceptor(int targetParameter, @Nullable String visibility) { + ConvertToInstanceMethodDialogUiInterceptor(int targetParameter, @Nullable String visibility, String... options) { super(ConvertToInstanceMethodDialog.class); myTargetParameter = targetParameter; myVisibility = visibility; + myOptions = options; } @Override @@ -81,9 +90,22 @@ public class ConvertToInstanceMethodTest extends LightRefactoringTestCase { if (myTargetParameter < 0 || myTargetParameter >= size) { fail("targetParameter out of bounds: " + myTargetParameter); } + if (myOptions.length > 0) { + assertEquals(Arrays.toString(myOptions), toString(model)); + } list.setSelectedIndex(myTargetParameter); dialog.setVisibility(myVisibility == null ? VisibilityUtil.ESCALATE_VISIBILITY : myVisibility); dialog.performOKAction(); } + + private static String toString(ListModel model) { + int size = model.getSize(); + String[] result = new String[size]; + for (int i = 0; i < size; i++) { + Object o = model.getElementAt(i); + result[i] = (o instanceof PsiElement e) ? e.getText() : o.toString(); + } + return Arrays.toString(result); + } } } diff --git a/java/openapi/resources/messages/JavaRefactoringBundle.properties b/java/openapi/resources/messages/JavaRefactoringBundle.properties index 4d7295ced412..403099323cf6 100644 --- a/java/openapi/resources/messages/JavaRefactoringBundle.properties +++ b/java/openapi/resources/messages/JavaRefactoringBundle.properties @@ -138,7 +138,6 @@ convert.to.instance.method.title=Convert To Instance Method convertToInstanceMethod.all.reference.type.parameters.are.not.in.project=No target class for the instance method is found: no method parameter with a type referencing a class in the project found. convertToInstanceMethod.all.reference.type.parameters.have.unknown.types=No target class for the instance method is found: method parameter types are unknown. convertToInstanceMethod.method.is.not.static=Method {0} is not static -convertToInstanceMethod.unsupported.containing.class=Containing class is not supported convertToInstanceMethod.no.default.ctor=Additionally, the containing class doesn't have a default constructor. convertToInstanceMethod.no.parameters.with.reference.type=There are no parameters that have reference type. convert.to.record.title=Convert To Record Class