IDEA-229694 Generate constructor for records: suggest generation of compact and canonical constructors

GitOrigin-RevId: 95e23c12711c867a1e1f41d42ee5f3cc87876b3d
This commit is contained in:
Tagir Valeev
2019-12-24 08:41:04 +00:00
committed by intellij-monorepo-bot
parent 39fc484b5d
commit 84c1a10be9
10 changed files with 225 additions and 1 deletions
@@ -2,6 +2,7 @@
package com.intellij.codeInsight.generation;
import com.intellij.CommonBundle;
import com.intellij.codeInsight.AnnotationTargetUtil;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.daemon.ImplicitUsageProvider;
@@ -12,11 +13,14 @@ import com.intellij.openapi.ui.Messages;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.util.JavaPsiRecordUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -63,6 +67,15 @@ public class GenerateConstructorHandler extends GenerateMembersHandlerBase {
return null;
}
if (aClass.isRecord() && JavaPsiRecordUtil.findCanonicalConstructor(aClass) == null) {
RecordConstructorChooserDialog dialog = new RecordConstructorChooserDialog(aClass);
if (!dialog.showAndGet()) return null;
ClassMember member = dialog.getClassMember();
if (member != null) {
return new ClassMember[]{member};
}
}
myCopyJavadoc = false;
PsiMethod[] baseConstructors = null;
PsiClass baseClass = aClass.getSuperClass();
@@ -146,10 +159,15 @@ public class GenerateConstructorHandler extends GenerateMembersHandlerBase {
@Override
@NotNull
protected List<? extends GenerationInfo> generateMemberPrototypes(PsiClass aClass, ClassMember[] members) throws IncorrectOperationException {
if (members.length == 1 && members[0] instanceof RecordConstructorMember) {
boolean compact = ((RecordConstructorMember)members[0]).isCompact();
return Collections.singletonList(new PsiGenerationInfo<>(generateRecordConstructor(aClass, compact)));
}
List<PsiMethod> baseConstructors = new ArrayList<>();
List<PsiField> fieldsVector = new ArrayList<>();
for (ClassMember member1 : members) {
PsiElement member = ((PsiElementClassMember)member1).getElement();
PsiElement member = ((PsiElementClassMember<?>)member1).getElement();
if (member instanceof PsiMethod) {
baseConstructors.add((PsiMethod)member);
}
@@ -179,6 +197,43 @@ public class GenerateConstructorHandler extends GenerateMembersHandlerBase {
return filterOutAlreadyInsertedConstructors(aClass, constructors);
}
@NotNull
private static PsiMethod generateRecordConstructor(PsiClass aClass, boolean compact) {
String constructor;
if (compact) {
constructor = "public " + aClass.getName() + "{\n}";
}
else {
PsiRecordComponent[] components = aClass.getRecordComponents();
String parameters = StreamEx.of(components).map(PsiRecordComponent::getText).joining(",", "(", ")");
String body =
StreamEx.of(components).map(PsiRecordComponent::getName).map(name -> "this." + name + "=" + name + ";\n").joining("", "{", "}");
constructor = "public " + aClass.getName() + parameters + body;
}
Project project = aClass.getProject();
PsiMethod ctor = JavaPsiFacade.getElementFactory(project).createMethodFromText(constructor, aClass);
if (!compact) {
JavaCodeStyleSettings settings = JavaCodeStyleSettings.getInstance(aClass.getContainingFile());
boolean finalParameters = settings.isGenerateFinalParameters();
PsiParameterList parameterList = ctor.getParameterList();
for (PsiParameter parameter : parameterList.getParameters()) {
PsiModifierList modifierList = parameter.getModifierList();
if (modifierList != null) {
modifierList.setModifierProperty(PsiModifier.FINAL, finalParameters);
PsiAnnotation.TargetType[] targets = AnnotationTargetUtil.getTargetsForLocation(modifierList);
for (PsiAnnotation annotation : parameter.getAnnotations()) {
PsiAnnotation.TargetType applicable = AnnotationTargetUtil.findAnnotationTarget(annotation, targets);
if (applicable == null) {
annotation.delete();
}
}
}
}
}
CodeStyleManager.getInstance(project).reformat(ctor);
return ctor;
}
private static List<? extends GenerationInfo> filterOutAlreadyInsertedConstructors(PsiClass aClass, List<? extends GenerationInfo> constructors) {
boolean alreadyExist = true;
for (GenerationInfo constructor : constructors) {
@@ -0,0 +1,59 @@
// Copyright 2000-2019 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.codeInsight.generation;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.daemon.JavaErrorMessages;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.VerticalFlowLayout;
import com.intellij.psi.PsiClass;
import com.intellij.ui.components.JBRadioButton;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
final class RecordConstructorChooserDialog extends DialogWrapper {
@NotNull private final JBRadioButton myCompact;
@NotNull private final JBRadioButton myCanonical;
@NotNull private final JBRadioButton myCustom;
@NotNull private final PsiClass myRecordClass;
RecordConstructorChooserDialog(@NotNull PsiClass recordClass) {
super(recordClass.getProject());
myRecordClass = recordClass;
setTitle(CodeInsightBundle.message("generate.record.constructor.title"));
setOKButtonText(CodeInsightBundle.message("generate.button.title"));
myCompact = new JBRadioButton(JavaErrorMessages.message("record.compact.constructor"), true);
myCanonical = new JBRadioButton(JavaErrorMessages.message("record.canonical.constructor"), false);
myCustom = new JBRadioButton(ApplicationBundle.message("custom.option"), false);
init();
}
@Override
public JComponent getPreferredFocusedComponent() {
return myCompact;
}
@Override
protected JComponent createCenterPanel() {
JPanel panel = new JPanel(new VerticalFlowLayout());
ButtonGroup group = new ButtonGroup();
group.add(myCompact);
group.add(myCanonical);
group.add(myCustom);
panel.add(myCompact);
panel.add(myCanonical);
panel.add(myCustom);
return panel;
}
ClassMember getClassMember() {
if (myCompact.isSelected()) {
return new RecordConstructorMember(myRecordClass, true);
}
if (myCanonical.isSelected()) {
return new RecordConstructorMember(myRecordClass, false);
}
return null;
}
}
@@ -0,0 +1,41 @@
// Copyright 2000-2019 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.codeInsight.generation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.util.PsiFormatUtil;
import com.intellij.psi.util.PsiFormatUtilBase;
import com.intellij.ui.SimpleColoredComponent;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public class RecordConstructorMember implements ClassMember {
private final PsiClass myRecord;
private final boolean myCompact;
public RecordConstructorMember(PsiClass aRecord, boolean compact) {
myRecord = aRecord;
myCompact = compact;
}
@Override
public MemberChooserObject getParentNodeDelegate() {
final String text = PsiFormatUtil.formatClass(myRecord, PsiFormatUtilBase.SHOW_NAME | PsiFormatUtilBase.SHOW_FQ_NAME);
return new PsiDocCommentOwnerMemberChooserObject(myRecord, text, myRecord.getIcon(0));
}
@Override
public void renderTreeNode(SimpleColoredComponent component, JTree tree) {
}
public boolean isCompact() {
return myCompact;
}
@NotNull
@Override
public String getText() {
return myCompact ? "Compact constructor" : "Canonical constructor";
}
}
@@ -0,0 +1,21 @@
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
record Test(final int foo, @Anno1 @Anno2 @Anno3 @Anno4 double bar) {
public Test(int foo, @Anno2 @Anno3 @Anno4 double bar) {<caret>
this.foo = foo;
this.bar = bar;
}
public int foo() {
return foo;
}
}
@Target(ElementType.RECORD_COMPONENT)
@interface Anno1 { }
@Target({ElementType.RECORD_COMPONENT, ElementType.PARAMETER})
@interface Anno2 { }
@Target({ElementType.PARAMETER})
@interface Anno3 { }
@Target({ElementType.TYPE_USE})
@interface Anno4 { }
@@ -0,0 +1,4 @@
record R(int x, int y) {
public R {<caret>
}
}
@@ -0,0 +1,17 @@
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
record Test(final int foo, @Anno1 @Anno2 @Anno3 @Anno4 double bar, String... varArg) {
<caret>
public int foo() {
return foo;
}
}
@Target(ElementType.RECORD_COMPONENT)
@interface Anno1 { }
@Target({ElementType.RECORD_COMPONENT, ElementType.PARAMETER})
@interface Anno2 { }
@Target({ElementType.PARAMETER})
@interface Anno3 { }
@Target({ElementType.TYPE_USE})
@interface Anno4 { }
@@ -0,0 +1,3 @@
record R(int x, int y) {
<caret>
}
@@ -19,17 +19,20 @@ import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.NullableNotNullManager;
import com.intellij.codeInsight.generation.ClassMember;
import com.intellij.codeInsight.generation.GenerateConstructorHandler;
import com.intellij.codeInsight.generation.RecordConstructorMember;
import com.intellij.java.codeInspection.DataFlowInspectionTest;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.psi.PsiClass;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -104,10 +107,27 @@ public class GenerateConstructorTest extends LightJavaCodeInsightFixtureTestCase
}
public void testNullableField() { doTest(); }
public void testRecordCompactConstructor() { doTestRecordConstructor(true); }
public void testRecordCanonicalConstructor() { doTestRecordConstructor(false); }
private void doTest() {
doTest(false);
}
private void doTestRecordConstructor(boolean compact) {
String name = getTestName(false);
myFixture.configureByFile("before" + name + ".java");
new GenerateConstructorHandler() {
@Nullable
@Override
protected ClassMember[] chooseOriginalMembers(PsiClass aClass, Project project) {
return new ClassMember[]{new RecordConstructorMember(aClass, compact)};
}
}.invoke(getProject(), getEditor(), getFile());
myFixture.checkResultByFile("after" + name + ".java");
}
private void doTest(boolean preSelect) {
String name = getTestName(false);
@@ -898,3 +898,5 @@ light.edit.close.cancel=Cancel
light.edit.exit.title=Save Documents
light.edit.exit.message=There are unsaved documents.\nDo you want to save the changes?
custom.option=Custom...
@@ -50,6 +50,8 @@ dialog.paste.on.import.text3=<html>The code fragment which you have pasted intro
copy.paste.reference.notification=<html>{0} {0, choice, 1#import was|2#imports were} added<p><span><a href='show'>Review Added Imports...</a></span></html>
paste.data.flavor.folding=FoldingData
paste.dataflavor.referencedata=ReferenceData
generate.button.title=Generate
generate.record.constructor.title=Generate Records Constructor
generate.constructor.fields.chooser.title=Choose Fields to Initialize by Constructor
error.attempt.to.generate.constructor.for.anonymous.class=Cannot add constructor to an anonymous class
generate.constructor.super.constructor.chooser.title=Choose Super Class Constructor