From 84c1a10be94dcd6c0750ddcbf7195b191663bd34 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Tue, 24 Dec 2019 11:35:51 +0700 Subject: [PATCH] IDEA-229694 Generate constructor for records: suggest generation of compact and canonical constructors GitOrigin-RevId: 95e23c12711c867a1e1f41d42ee5f3cc87876b3d --- .../GenerateConstructorHandler.java | 57 +++++++++++++++++- .../RecordConstructorChooserDialog.java | 59 +++++++++++++++++++ .../generation/RecordConstructorMember.java | 41 +++++++++++++ .../afterRecordCanonicalConstructor.java | 21 +++++++ .../afterRecordCompactConstructor.java | 4 ++ .../beforeRecordCanonicalConstructor.java | 17 ++++++ .../beforeRecordCompactConstructor.java | 3 + .../codeInsight/GenerateConstructorTest.java | 20 +++++++ .../src/messages/ApplicationBundle.properties | 2 + .../src/messages/CodeInsightBundle.properties | 2 + 10 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorChooserDialog.java create mode 100644 java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorMember.java create mode 100644 java/java-tests/testData/codeInsight/generateConstructor/afterRecordCanonicalConstructor.java create mode 100644 java/java-tests/testData/codeInsight/generateConstructor/afterRecordCompactConstructor.java create mode 100644 java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCanonicalConstructor.java create mode 100644 java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCompactConstructor.java diff --git a/java/java-impl/src/com/intellij/codeInsight/generation/GenerateConstructorHandler.java b/java/java-impl/src/com/intellij/codeInsight/generation/GenerateConstructorHandler.java index 85a76209bb8e..7b065cdd14a6 100644 --- a/java/java-impl/src/com/intellij/codeInsight/generation/GenerateConstructorHandler.java +++ b/java/java-impl/src/com/intellij/codeInsight/generation/GenerateConstructorHandler.java @@ -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 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 baseConstructors = new ArrayList<>(); List 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 filterOutAlreadyInsertedConstructors(PsiClass aClass, List constructors) { boolean alreadyExist = true; for (GenerationInfo constructor : constructors) { diff --git a/java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorChooserDialog.java b/java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorChooserDialog.java new file mode 100644 index 000000000000..555d66f4cacb --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorChooserDialog.java @@ -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; + } +} diff --git a/java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorMember.java b/java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorMember.java new file mode 100644 index 000000000000..39b235b4b07c --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/generation/RecordConstructorMember.java @@ -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"; + } +} diff --git a/java/java-tests/testData/codeInsight/generateConstructor/afterRecordCanonicalConstructor.java b/java/java-tests/testData/codeInsight/generateConstructor/afterRecordCanonicalConstructor.java new file mode 100644 index 000000000000..b4af046a0716 --- /dev/null +++ b/java/java-tests/testData/codeInsight/generateConstructor/afterRecordCanonicalConstructor.java @@ -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) { + 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 { } diff --git a/java/java-tests/testData/codeInsight/generateConstructor/afterRecordCompactConstructor.java b/java/java-tests/testData/codeInsight/generateConstructor/afterRecordCompactConstructor.java new file mode 100644 index 000000000000..ccbe23bb4408 --- /dev/null +++ b/java/java-tests/testData/codeInsight/generateConstructor/afterRecordCompactConstructor.java @@ -0,0 +1,4 @@ +record R(int x, int y) { + public R { + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCanonicalConstructor.java b/java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCanonicalConstructor.java new file mode 100644 index 000000000000..fa32e24ce1c4 --- /dev/null +++ b/java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCanonicalConstructor.java @@ -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) { + + 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 { } diff --git a/java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCompactConstructor.java b/java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCompactConstructor.java new file mode 100644 index 000000000000..f1a4931a24f3 --- /dev/null +++ b/java/java-tests/testData/codeInsight/generateConstructor/beforeRecordCompactConstructor.java @@ -0,0 +1,3 @@ +record R(int x, int y) { + +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/GenerateConstructorTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/GenerateConstructorTest.java index 43a4062de2d5..cdb3f36ab48d 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/GenerateConstructorTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/GenerateConstructorTest.java @@ -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); diff --git a/platform/platform-resources-en/src/messages/ApplicationBundle.properties b/platform/platform-resources-en/src/messages/ApplicationBundle.properties index 47659818558b..42081cdd5d9c 100644 --- a/platform/platform-resources-en/src/messages/ApplicationBundle.properties +++ b/platform/platform-resources-en/src/messages/ApplicationBundle.properties @@ -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... \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties index 84fc783cadee..3614bbbbaeeb 100644 --- a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties +++ b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties @@ -50,6 +50,8 @@ dialog.paste.on.import.text3=The code fragment which you have pasted intro copy.paste.reference.notification={0} {0, choice, 1#import was|2#imports were} added

Review Added Imports... 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