Java: "Generate overloaded constructor with default parameter values" for records (IDEA-360071)

GitOrigin-RevId: a20fd5478241de404df3243f1b8b9a7d1a73829e
This commit is contained in:
Bas Leijdekkers
2024-10-02 20:13:41 +00:00
committed by intellij-monorepo-bot
parent 95ed423db6
commit dfae4a26c2
8 changed files with 119 additions and 30 deletions
@@ -18,6 +18,7 @@ import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.*;
import com.intellij.refactoring.util.RefactoringUIUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.CommonJavaRefactoringUtil;
import com.intellij.util.containers.ContainerUtil;
@@ -42,31 +43,52 @@ public final class DefineParamsDefaultValueAction extends PsiBasedModCommandActi
@Override
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull PsiElement element) {
if (!JavaLanguage.INSTANCE.equals(element.getLanguage())) return null;
final PsiElement parent = PsiTreeUtil.getParentOfType(element, PsiMethod.class, PsiCodeBlock.class);
if (!(parent instanceof PsiMethod method)) return null;
final PsiParameterList parameterList = method.getParameterList();
if (parameterList.isEmpty()) return null;
final PsiClass containingClass = method.getContainingClass();
if (containingClass == null || (containingClass.isInterface() && !PsiUtil.isAvailable(JavaFeature.EXTENSION_METHODS, method))) {
final PsiElement parent = PsiTreeUtil.getParentOfType(element, PsiMethod.class, PsiClass.class, PsiCodeBlock.class);
String message;
if (parent instanceof PsiMethod method) {
final PsiParameterList parameterList = method.getParameterList();
if (parameterList.isEmpty()) return null;
final PsiClass containingClass = method.getContainingClass();
if (containingClass == null || (containingClass.isInterface() && !PsiUtil.isAvailable(JavaFeature.EXTENSION_METHODS, method))) {
return null;
}
if ((containingClass instanceof PsiImplicitClass || containingClass instanceof PsiAnonymousClass) && method.isConstructor()) {
return null; // constructors can't be declared here, code is broken so don't suggest generating more broken code
}
if (containingClass.isAnnotationType()) {
// Method with parameters in annotation is a compilation error; there's no sense to create overload
return null;
}
message = QuickFixBundle.message("generate.overloaded.method.or.constructor.with.default.parameter.values",
JavaElementKind.fromElement(method).lessDescriptive().object());
}
else if (parent instanceof PsiClass aClass && aClass.isRecord()) {
PsiRecordHeader header = aClass.getRecordHeader();
if (header == null || header.getTextOffset() + header.getTextLength() < element.getTextOffset()) return null;
if (header.getRecordComponents().length == 0) return null;
message = QuickFixBundle.message("generate.overloaded.method.or.constructor.with.default.parameter.values",
JavaElementKind.CONSTRUCTOR.lessDescriptive().object());
}
else {
return null;
}
if ((containingClass instanceof PsiImplicitClass || containingClass instanceof PsiAnonymousClass) && method.isConstructor()) {
return null; // constructors can't be declared here, code is broken so don't suggest generating more broken code
}
if (containingClass.isAnnotationType()) {
// Method with parameters in annotation is a compilation error; there's no sense to create overload
return null;
}
return Presentation.of(QuickFixBundle.message("generate.overloaded.method.or.constructor.with.default.parameter.values",
JavaElementKind.fromElement(method).lessDescriptive().object()))
return Presentation.of(message)
.withIcon(AllIcons.Actions.RefactoringBulb)
.withPriority(PriorityAction.Priority.LOW);
}
@Override
protected @NotNull ModCommand perform(@NotNull ActionContext context, @NotNull PsiElement element) {
PsiMethod method = PsiTreeUtil.getParentOfType(element, PsiMethod.class);
assert method != null;
PsiElement parent = PsiTreeUtil.getParentOfType(element, PsiMethod.class, PsiClass.class);
PsiMethod method;
if (parent instanceof PsiMethod) {
method = (PsiMethod)parent;
}
else if (parent instanceof PsiClass aClass && aClass.isRecord()) {
method = JavaPsiRecordUtil.findCanonicalConstructor(aClass);
assert method != null;
}
else throw new AssertionError();
PsiParameterList parameterList = method.getParameterList();
PsiParameter[] parameters = parameterList.getParameters();
if (parameters.length == 1) {
@@ -74,23 +96,35 @@ public final class DefineParamsDefaultValueAction extends PsiBasedModCommandActi
updater.getWritable(m).getParameterList().getParameters()));
}
List<ParameterClassMember> members = ContainerUtil.map(parameters, ParameterClassMember::new);
PsiParameter selectedParam = PsiTreeUtil.getParentOfType(element, PsiParameter.class);
int idx = selectedParam != null ? ArrayUtil.find(parameters, selectedParam) : -1;
int idx = getSelectedIndex(element);
List<ParameterClassMember> defaultSelection = idx >= 0 ? List.of(members.get(idx)) : members;
return ModCommand.chooseMultipleMembers(
QuickFixBundle.message("choose.default.value.parameters.popup.title"),
members, defaultSelection,
sel -> ModCommand.psiUpdate(context, updater -> {
invoke(context.project(), updater.getWritable(element), updater,
invoke(context.project(), updater.getWritable(method), updater,
ContainerUtil.map2Array(sel, PsiParameter.EMPTY_ARRAY,
s -> updater.getWritable(((ParameterClassMember)s).getParameter())));
}));
}
private static void invoke(@NotNull Project project, @NotNull PsiElement element,
private static int getSelectedIndex(@NotNull PsiElement element) {
PsiVariable selected = PsiTreeUtil.getParentOfType(element, PsiParameter.class, PsiRecordComponent.class);
if (selected instanceof PsiParameter parameter) {
PsiParameterList parameterList = (PsiParameterList)parameter.getParent();
return parameterList.getParameterIndex(parameter);
}
else if (selected instanceof PsiRecordComponent recordComponent) {
PsiRecordHeader recordHeader = (PsiRecordHeader)recordComponent.getParent();
return ArrayUtil.find(recordHeader.getRecordComponents(), recordComponent);
}
else {
return -1;
}
}
private static void invoke(@NotNull Project project, @NotNull PsiMethod method,
@NotNull ModPsiUpdater updater, @NotNull PsiParameter @NotNull [] parameters) {
final PsiMethod method = PsiTreeUtil.getNonStrictParentOfType(element, PsiMethod.class);
assert method != null;
PsiParameterList parameterList = method.getParameterList();
if (parameters.length == 0) return;
final PsiMethod methodPrototype = generateMethodPrototype(method, parameters);
@@ -99,8 +133,8 @@ public final class DefineParamsDefaultValueAction extends PsiBasedModCommandActi
for (PsiMethod existingMethod : containingClass.findMethodsByName(method.getName(), false)) {
if (MethodSignatureUtil.areParametersErasureEqual(existingMethod, methodPrototype)) {
updater.moveCaretTo(existingMethod.getTextOffset());
updater.message(JavaBundle.message("default.param.value.warning",
existingMethod.isConstructor() ? 0 : 1));
String description = RefactoringUIUtil.getDescription(existingMethod, false);
updater.message(StringUtil.capitalize(JavaBundle.message("default.param.value.warning", description)));
return;
}
}
@@ -1,10 +1,8 @@
// 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.psi.impl.light;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiRecordComponent;
import com.intellij.psi.PsiType;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
@@ -46,6 +44,17 @@ public class LightCompactConstructorParameter extends LightParameter implements
return myRecordComponent.getNavigationElement();
}
@Override
public @NotNull PsiElement findSameElementInCopy(@NotNull PsiFile copy) {
PsiMethod constructor = (PsiMethod)getDeclarationScope();
PsiMethod copyConstructor = PsiTreeUtil.findSameElementInCopy(constructor, copy);
PsiParameterList parameterList = constructor.getParameterList();
int index = parameterList.getParameterIndex(this);
PsiParameter parameter = copyConstructor.getParameterList().getParameter(index);
assert parameter != null;
return parameter;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -3,6 +3,7 @@ package com.intellij.psi.impl.light;
import com.intellij.psi.*;
import com.intellij.psi.impl.ElementPresentationUtil;
import com.intellij.psi.util.JavaPsiRecordUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.ui.IconManager;
import com.intellij.ui.PlatformIcons;
@@ -80,6 +81,12 @@ public class LightRecordCanonicalConstructor extends LightMethod implements Synt
};
}
@Override
public @NotNull PsiElement findSameElementInCopy(@NotNull PsiFile copy) {
PsiClass copyClass = PsiTreeUtil.findSameElementInCopy(myContainingClass, copy);
return Objects.requireNonNull(JavaPsiRecordUtil.findCanonicalConstructor(copyClass));
}
public static class LightRecordConstructorParameter extends LightParameterWrapper {
private final @NotNull LightParameterListWrapper myWrapper;
@@ -137,6 +144,20 @@ public class LightRecordCanonicalConstructor extends LightMethod implements Synt
return getParent().getContainingFile();
}
@Override
public @NotNull PsiElement findSameElementInCopy(@NotNull PsiFile copy) {
PsiParameterList parameterList = (PsiParameterList)getParent();
int index = parameterList.getParameterIndex(this);
PsiClass recordClass = PsiTreeUtil.getParentOfType(this, PsiClass.class);
PsiClass copyClass = PsiTreeUtil.findSameElementInCopy(recordClass, copy);
assert copyClass != null;
PsiMethod copyConstructor = JavaPsiRecordUtil.findCanonicalConstructor(copyClass);
assert copyConstructor != null;
PsiParameter copyParameter = copyConstructor.getParameterList().getParameter(index);
assert copyParameter != null;
return copyParameter;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -0,0 +1,6 @@
// "Generate overloaded constructor with default parameter values" "true"
record Point(int x, int y) {
Point() {
this(0, 0);
}
}
@@ -0,0 +1,10 @@
// "Generate overloaded constructor with default parameter values" "true"
record Point(int x, int y) {
Point() {
this(0, 0);
}
public Point {
}
}
@@ -0,0 +1,3 @@
// "Generate overloaded constructor with default parameter values" "true"
record <caret>Point(int x, int y) {
}
@@ -0,0 +1,6 @@
// "Generate overloaded constructor with default parameter values" "true"
record<caret> Point(int x, int y) {
public Point {
}
}
@@ -1519,7 +1519,7 @@ simplify.stream.inspection.iterate.take.while.fix.message=Can be replaced with t
side.effects.pattern.message=<html>\n<body>\nThere are possible side effects found in {0}<br>\nYou can:\n<br>\n-\\&nbsp;<b>Remove</b> variable usages along with all expressions involved, or<br>\n-\\&nbsp;<b>Transform</b> expressions assigned to variable into the statements on their own.<br>\n<div style="padding-left: 0.6cm;">\n That is,<br>\n <table border="0">\n <tr>\n <td><code>{1};</code></td>\n </tr>\n </table>\n becomes: <br>\n <table border="0">\n <tr>\n <td><code>{2};</code></td>\n </tr>\n </table>\n</div>\n</body>\n</html>
side.effects.expression.presentation=expression ''{0}''
change.signature.from.usage.short.name=<html> Change signature of {0}({1})</html>
default.param.value.warning={0, choice, 0#Constructor|1#Method} with the chosen signature already exists
default.param.value.warning={0} already exists
qualify.static.constant.access=Qualify static constant access
qualify.static.access.command.name=Qualify Static Access
qualify.static.call.fix.text=Qualify static call