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