diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml
index 44d339157ab3..cda5eb6d923c 100644
--- a/java/java-impl/src/META-INF/JavaPlugin.xml
+++ b/java/java-impl/src/META-INF/JavaPlugin.xml
@@ -1959,6 +1959,10 @@
com.intellij.codeInsight.intention.impl.lists.JavaJoinArgumentsAction
Java/Other
+
+ com.intellij.codeInsight.intention.impl.ConvertCompactConstructorToCanonicalAction
+ Java/Declaration
+
diff --git a/java/java-impl/src/com/intellij/codeInsight/intention/impl/ConvertCompactConstructorToCanonicalAction.java b/java/java-impl/src/com/intellij/codeInsight/intention/impl/ConvertCompactConstructorToCanonicalAction.java
new file mode 100644
index 000000000000..a1c439f8158c
--- /dev/null
+++ b/java/java-impl/src/com/intellij/codeInsight/intention/impl/ConvertCompactConstructorToCanonicalAction.java
@@ -0,0 +1,75 @@
+// Copyright 2000-2020 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.intention.impl;
+
+import com.intellij.codeInsight.CodeInsightBundle;
+import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
+import com.intellij.codeInsight.generation.RecordConstructorMember;
+import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.*;
+import com.intellij.psi.util.JavaPsiRecordUtil;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public class ConvertCompactConstructorToCanonicalAction extends PsiElementBaseIntentionAction {
+ @Override
+ public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
+ PsiMethod method = getMethod(element);
+ if (method == null) return;
+ PsiClass recordClass = method.getContainingClass();
+ if (recordClass == null) return;
+ PsiMethod prototype = new RecordConstructorMember(recordClass, false).generateRecordConstructor();
+ PsiModifierList modifierList = method.getModifierList();
+ prototype.getModifierList().replace(modifierList);
+ PsiElement beforeModifier = modifierList.getPrevSibling();
+ if (beforeModifier != null) {
+ prototype.addRangeBefore(method.getFirstChild(), beforeModifier, prototype.getModifierList());
+ }
+ PsiCodeBlock oldBody = Objects.requireNonNull(method.getBody());
+ PsiCodeBlock body = (PsiCodeBlock)Objects.requireNonNull(prototype.getBody()).replace(oldBody);
+ PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
+ for (PsiRecordComponent component : recordClass.getRecordComponents()) {
+ PsiField field = JavaPsiRecordUtil.getFieldForComponent(component);
+ if (field != null && !HighlightControlFlowUtil.variableDefinitelyAssignedIn(field, body)) {
+ body.add(factory.createStatementFromText("this." + field.getName() + "=" + field.getName() + ";", body));
+ }
+ }
+ int offset = editor.getCaretModel().getOffset();
+ if (oldBody.getTextRange().contains(offset)) {
+ offset += body.getTextRangeInParent().getStartOffset() - oldBody.getTextRangeInParent().getStartOffset();
+ }
+ method.replace(prototype);
+ editor.getCaretModel().moveToOffset(offset);
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @Override
+ public @NotNull String getText() {
+ return getFamilyName();
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
+ PsiMethod method = getMethod(element);
+ return method != null &&
+ JavaPsiRecordUtil.isCompactConstructor(method) &&
+ method.getBody() != null &&
+ method.getContainingClass() != null &&
+ method.getContainingClass().isRecord();
+ }
+
+ private static PsiMethod getMethod(@NotNull PsiElement element) {
+ return PsiTreeUtil.getParentOfType(element, PsiMethod.class, true, PsiLambdaExpression.class, PsiMember.class);
+ }
+
+ @Nls(capitalization = Nls.Capitalization.Sentence)
+ @Override
+ public @NotNull String getFamilyName() {
+ return CodeInsightBundle.message("intention.convert.compact.constructor.to.canonical");
+ }
+}
diff --git a/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/after.java.template b/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/after.java.template
new file mode 100644
index 000000000000..c133d56f8cd5
--- /dev/null
+++ b/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/after.java.template
@@ -0,0 +1,9 @@
+record Range(int from, int to) {
+ public Range(int from, int to) {
+ if (from > to) {
+ from = to;
+ }
+ this.from = from;
+ this.to = to;
+ }
+}
\ No newline at end of file
diff --git a/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/before.java.template b/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/before.java.template
new file mode 100644
index 000000000000..9fd05b6b7fe7
--- /dev/null
+++ b/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/before.java.template
@@ -0,0 +1,7 @@
+record Range(int from, int to) {
+ public Range {
+ if (from > to) {
+ from = to;
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/description.html b/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/description.html
new file mode 100644
index 000000000000..6044319b9384
--- /dev/null
+++ b/java/java-impl/src/intentionDescriptions/ConvertCompactConstructorToCanonicalAction/description.html
@@ -0,0 +1,6 @@
+
+
+Converts Java record compact constructor to explicit canonical form.
+
+
+
\ No newline at end of file
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/afterAnnotations.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/afterAnnotations.java
new file mode 100644
index 000000000000..9e6d426e355a
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/afterAnnotations.java
@@ -0,0 +1,22 @@
+// "Convert compact constructor to canonical" "true"
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+public record Test(int x, @ParamAnno int y, @FieldAnno int... other) {
+ @ConstructorAnno
+ public Test(int x, @ParamAnno int y, int... other) {
+ this.x = Math.abs(x);
+ if (other == null) other = new int[0];
+ this.y = y;
+ this.other = other;
+ }
+}
+
+@Target(ElementType.PARAMETER)
+@interface ParamAnno {}
+
+@Target(ElementType.FIELD)
+@interface FieldAnno {}
+
+@Target(ElementType.CONSTRUCTOR)
+@interface ConstructorAnno {}
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/afterSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/afterSimple.java
new file mode 100644
index 000000000000..afbc4ee6d9dc
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/afterSimple.java
@@ -0,0 +1,10 @@
+// "Convert compact constructor to canonical" "true"
+record R(int x,int y) {
+ /*
+ hello
+ */
+ public R(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+}
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/beforeAnnotations.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/beforeAnnotations.java
new file mode 100644
index 000000000000..704098b10ca6
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/beforeAnnotations.java
@@ -0,0 +1,20 @@
+// "Convert compact constructor to canonical" "true"
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+public record Test(int x, @ParamAnno int y, @FieldAnno int... other) {
+ @ConstructorAnno
+ public Test {
+ this.x = Math.abs(x);
+ if (other == null) other = new int[0];
+ }
+}
+
+@Target(ElementType.PARAMETER)
+@interface ParamAnno {}
+
+@Target(ElementType.FIELD)
+@interface FieldAnno {}
+
+@Target(ElementType.CONSTRUCTOR)
+@interface ConstructorAnno {}
diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/beforeSimple.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/beforeSimple.java
new file mode 100644
index 000000000000..ee7b156ce917
--- /dev/null
+++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical/beforeSimple.java
@@ -0,0 +1,8 @@
+// "Convert compact constructor to canonical" "true"
+record R(int x,int y) {
+ /*
+ hello
+ */
+ public R {
+ }
+}
diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/ConvertCompactConstructorToCanonicalActionTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/ConvertCompactConstructorToCanonicalActionTest.java
new file mode 100644
index 000000000000..f1aad4e5d08c
--- /dev/null
+++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/intention/ConvertCompactConstructorToCanonicalActionTest.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.intellij.java.codeInsight.intention;
+
+import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
+
+public class ConvertCompactConstructorToCanonicalActionTest extends LightIntentionActionTestCase {
+ @Override
+ protected String getBasePath() {
+ return "/codeInsight/daemonCodeAnalyzer/quickFix/compactToCanonical";
+ }
+}
diff --git a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties
index bb679621dce9..3b4824ea273c 100644
--- a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties
+++ b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties
@@ -270,6 +270,8 @@ intention.wrap.with.unmodifiable.list=Wrap with unmodifiable list
intention.wrap.with.unmodifiable.set=Wrap with unmodifiable set
intention.wrap.with.unmodifiable.map=Wrap with unmodifiable map
+intention.convert.compact.constructor.to.canonical=Convert compact constructor to canonical
+
intention.preview.adv.show.text=Press {0} to open preview
intention.preview.adv.hide.text=Press {0} to hide preview
intention.preview.no.available.text=Preview isn't available