diff --git a/plugins/IntentionPowerPak/src/META-INF/IntentionPowerPack.xml b/plugins/IntentionPowerPak/src/META-INF/IntentionPowerPack.xml index c620ebf524b8..b7613144d272 100644 --- a/plugins/IntentionPowerPak/src/META-INF/IntentionPowerPack.xml +++ b/plugins/IntentionPowerPak/src/META-INF/IntentionPowerPack.xml @@ -522,5 +522,10 @@ com.siyeh.IntentionPowerPackBundle intention.category.other + + com.siyeh.ipp.functional.ExtractToMethodReferenceIntention + com.siyeh.IntentionPowerPackBundle + intention.category.control.flow + diff --git a/plugins/IntentionPowerPak/src/com/siyeh/IntentionPowerPackBundle.properties b/plugins/IntentionPowerPak/src/com/siyeh/IntentionPowerPackBundle.properties index 47a682d8c0a6..29f54d3b2858 100644 --- a/plugins/IntentionPowerPak/src/com/siyeh/IntentionPowerPackBundle.properties +++ b/plugins/IntentionPowerPak/src/com/siyeh/IntentionPowerPackBundle.properties @@ -2,6 +2,7 @@ plugin.IntentionPowerPack.description=Adds over 80 new intention actions for IDE replace.fully.qualified.name.with.import.intention.name=Replace qualified name with 'import' replace.fully.qualified.name.with.import.intention.family.name=Replace Qualified Name with Import replace.if.with.switch.intention.name=Replace 'if' with 'switch' +extract.to.method.reference.intention.name=Extract to method reference replace.if.with.switch.intention.family.name=Replace If with Switch merge.parallel.ifs.intention.name=Merge 'if's merge.parallel.ifs.intention.family.name=Merge Parallel Ifs diff --git a/plugins/IntentionPowerPak/src/com/siyeh/ipp/functional/ExtractToMethodReferenceIntention.java b/plugins/IntentionPowerPak/src/com/siyeh/ipp/functional/ExtractToMethodReferenceIntention.java new file mode 100644 index 000000000000..d81abab94969 --- /dev/null +++ b/plugins/IntentionPowerPak/src/com/siyeh/ipp/functional/ExtractToMethodReferenceIntention.java @@ -0,0 +1,222 @@ +/* + * 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.siyeh.ipp.functional; + +import com.intellij.codeInsight.CodeInsightUtilCore; +import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction; +import com.intellij.codeInspection.LambdaCanBeMethodReferenceInspection; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pass; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.extractMethod.AbstractExtractDialog; +import com.intellij.refactoring.extractMethod.ControlFlowWrapper; +import com.intellij.refactoring.extractMethod.ExtractMethodProcessor; +import com.intellij.refactoring.extractMethod.PrepareFailedException; +import com.intellij.refactoring.rename.RenamePsiElementProcessor; +import com.intellij.refactoring.rename.inplace.MemberInplaceRenamer; +import com.intellij.refactoring.util.VariableData; +import com.intellij.util.IncorrectOperationException; +import com.intellij.util.ObjectUtils; +import com.intellij.util.text.UniqueNameGenerator; +import com.siyeh.IntentionPowerPackBundle; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; + +public class ExtractToMethodReferenceIntention extends BaseElementAtCaretIntentionAction { + @NotNull + @Override + public String getText() { + return getFamilyName(); + } + + @NotNull + @Override + public String getFamilyName() { + return IntentionPowerPackBundle.message("extract.to.method.reference.intention.name"); + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { + final PsiLambdaExpression lambdaExpression = PsiTreeUtil.getParentOfType(element, PsiLambdaExpression.class, false); + if (lambdaExpression != null) { + PsiElement body = lambdaExpression.getBody(); + if (body == null) return false; + PsiType functionalInterfaceType = lambdaExpression.getFunctionalInterfaceType(); + if (functionalInterfaceType == null) return false; + PsiExpression asMethodReference = LambdaCanBeMethodReferenceInspection + .canBeMethodReferenceProblem(body, lambdaExpression.getParameterList().getParameters(), functionalInterfaceType, null); + if (asMethodReference != null) return false; + try { + PsiElement[] toExtract = body instanceof PsiCodeBlock ? ((PsiCodeBlock)body).getStatements() : new PsiElement[] {body}; + ControlFlowWrapper wrapper = new ControlFlowWrapper(project, body, toExtract); + wrapper.prepareExitStatements(toExtract, body); + PsiVariable[] outputVariables = wrapper.getOutputVariables(); + List inputVariables = wrapper.getInputVariables(body, toExtract, outputVariables); + return inputVariables.stream().allMatch(variable -> variable instanceof PsiParameter && ((PsiParameter)variable).getDeclarationScope() == lambdaExpression); + } + catch (PrepareFailedException ignored) { } + catch (ControlFlowWrapper.ExitStatementsNotSameException ignored) { } + } + return false; + } + + @Override + public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException { + final PsiLambdaExpression lambdaExpression = PsiTreeUtil.getParentOfType(element, PsiLambdaExpression.class, false); + if (lambdaExpression != null) { + PsiElement body = lambdaExpression.getBody(); + if (body != null) { + PsiElement[] elements = body instanceof PsiCodeBlock ? ((PsiCodeBlock)body).getStatements() : new PsiElement[] {body}; + PsiMethod interfaceMethod = LambdaUtil.getFunctionalInterfaceMethod(lambdaExpression.getFunctionalInterfaceType()); + String initialMethodName = interfaceMethod != null ? interfaceMethod.getName() : "name"; + ExtractMethodProcessor methodProcessor = new ExtractMethodProcessor(project, editor, elements, null, getFamilyName(), null, null) { + @Override + public boolean showDialog() { + apply(new MyExtractMethodDialog(myTargetClass, lambdaExpression, myCanBeStatic, initialMethodName)); + return true; + } + }; + + try { + methodProcessor.prepare(); + } + catch (PrepareFailedException e) { + return; + } + methodProcessor.showDialog(); + WriteAction.run(() -> { + methodProcessor.doExtract(); + PsiExpression expression = LambdaCanBeMethodReferenceInspection.replaceLambdaWithMethodReference(lambdaExpression); + if (expression instanceof PsiMethodReferenceExpression) { + PsiMethod method = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(methodProcessor.getExtractedMethod()); + + PsiElement refElement = ((PsiMethodReferenceExpression)expression).getReferenceNameElement(); + PsiIdentifier nameIdentifier = method.getNameIdentifier(); + if (nameIdentifier == null) return; + + //try to navigate to reference name + editor.getCaretModel().moveToOffset(ObjectUtils.notNull(refElement, nameIdentifier).getTextOffset()); + + final RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(method); + if (!processor.isInplaceRenameSupported()) { + return; + } + List suggestedNames = new ArrayList<>(); + suggestedNames.add(method.getName()); + processor.substituteElementToRename(method, editor, new Pass() { + @Override + public void pass(PsiElement substitutedElement) { + final MemberInplaceRenamer renamer = new MemberInplaceRenamer(method, substitutedElement, editor); + final LinkedHashSet nameSuggestions = new LinkedHashSet<>(suggestedNames); + renamer.performInplaceRefactoring(nameSuggestions); + } + }); + } + }); + } + } + } + + @Nullable + @Override + public PsiElement getElementToMakeWritable(@NotNull PsiFile currentFile) { + return currentFile; + } + + @Override + public boolean startInWriteAction() { + return false; + } + + private static class MyExtractMethodDialog implements AbstractExtractDialog { + private final String myTargetMethodName; + private final boolean myCanBeStatic; + private final VariableData[] myVariableData; + + public MyExtractMethodDialog(@NotNull PsiClass targetClass, + PsiLambdaExpression lambdaExpression, + boolean canBeStatic, + String initialMethodName) { + myVariableData = Arrays.stream(lambdaExpression.getParameterList().getParameters()) + .map(parameter -> { + VariableData data = new VariableData(parameter); + data.passAsParameter = true; + data.name = parameter.getName(); + return data; + }) + .toArray(VariableData[]::new); + myCanBeStatic = canBeStatic; + PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(targetClass.getProject()); + String parameters = "(" + StringUtil.join(myVariableData, data -> data.type.getCanonicalText() + " " + data.name, ", ") + "){}"; + myTargetMethodName = UniqueNameGenerator.generateUniqueName(initialMethodName, + methodName -> { + String methodText = "private void " + methodName + parameters; + PsiMethod patternMethod = elementFactory.createMethodFromText(methodText, lambdaExpression); + return targetClass.findMethodBySignature(patternMethod, true) == null; + }); + + } + + @Override + public String getChosenMethodName() { + return myTargetMethodName; + } + + @Override + public VariableData[] getChosenParameters() { + return myVariableData; + } + + @NotNull + @Override + public String getVisibility() { + return PsiModifier.PRIVATE; + } + + @Override + public boolean isMakeStatic() { + return myCanBeStatic; + } + + @Override + public boolean isChainedConstructor() { + return false; + } + + @Override + public PsiType getReturnType() { + return null; + } + + @Override + public void show() {} + + @Override + public boolean isOK() { + return true; + } + } +} + diff --git a/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/after.java.template b/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/after.java.template new file mode 100644 index 000000000000..5ab0b0a67ad9 --- /dev/null +++ b/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/after.java.template @@ -0,0 +1,10 @@ +class X { + { + JButton b = new JButton(); + b.addActionListener(this::clicked); + } + + private void clicked(ActionEvent e) { + System.out.println("clicked"); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/before.java.template b/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/before.java.template new file mode 100644 index 000000000000..2070aa284793 --- /dev/null +++ b/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/before.java.template @@ -0,0 +1,8 @@ +class X { + { + JButton b = new JButton(); + b.addActionListener(e -> { + System.out.println("clicked"); + }); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/description.html b/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/description.html new file mode 100644 index 000000000000..32b71eec8529 --- /dev/null +++ b/plugins/IntentionPowerPak/src/intentionDescriptions/ExtractToMethodReferenceIntention/description.html @@ -0,0 +1,5 @@ + + +

Intention to extract the lambda body to a method and convert lambda to method reference.

+ + \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/ConvertableToMethodReference.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/ConvertableToMethodReference.java new file mode 100644 index 000000000000..1dd9b0534de5 --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/ConvertableToMethodReference.java @@ -0,0 +1,8 @@ +interface I { + void m(int i); +} +class B { + { + I i = i1 -> System.out.println(i1); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/OverloadedMethodSameName.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/OverloadedMethodSameName.java new file mode 100644 index 000000000000..e9aff6e178b6 --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/OverloadedMethodSameName.java @@ -0,0 +1,9 @@ +interface I { + void m(int i); +} +class B { + void m() {} + { + I i = i1 -> System.out.println(i1 + ""); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/OverloadedMethodSameName_after.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/OverloadedMethodSameName_after.java new file mode 100644 index 000000000000..e8ef9a39e56f --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/OverloadedMethodSameName_after.java @@ -0,0 +1,13 @@ +interface I { + void m(int i); +} +class B { + private static void m(int i1) { + System.out.println(i1 + ""); + } + + void m() {} + { + I i = B::m; + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/RequiredTypeParameter.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/RequiredTypeParameter.java new file mode 100644 index 000000000000..0463b280d9fd --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/RequiredTypeParameter.java @@ -0,0 +1,11 @@ +interface I { + void m(T t); +} +class B { + void n(){ + I i = i1 -> { + System.out.println(i1); + System.out.println(i1); + } + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/RequiredTypeParameter_after.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/RequiredTypeParameter_after.java new file mode 100644 index 000000000000..3cb9fc2042d0 --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/RequiredTypeParameter_after.java @@ -0,0 +1,13 @@ +interface I { + void m(T t); +} +class B { + private static void m(N i1) { + System.out.println(i1); + System.out.println(i1); + } + + void n(){ + I i = B::m + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/SameSignatureMethodExists.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/SameSignatureMethodExists.java new file mode 100644 index 000000000000..9eec7e203043 --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/SameSignatureMethodExists.java @@ -0,0 +1,9 @@ +interface I { + void m(int i); +} +class B { + void m(int i) {} + { + I i = i1 -> System.out.println(i1 + ""); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/SameSignatureMethodExists_after.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/SameSignatureMethodExists_after.java new file mode 100644 index 000000000000..5a4e434fb0fa --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/SameSignatureMethodExists_after.java @@ -0,0 +1,13 @@ +interface I { + void m(int i); +} +class B { + private static void m2(int i1) { + System.out.println(i1 + ""); + } + + void m(int i) {} + { + I i = B::m2; + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnableToCreateStatic.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnableToCreateStatic.java new file mode 100644 index 000000000000..d9cd24b639a6 --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnableToCreateStatic.java @@ -0,0 +1,8 @@ +interface I { + void m(int i); +} +class B { + class X { + I i = i1 -> System.out.println(""); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnableToCreateStatic_after.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnableToCreateStatic_after.java new file mode 100644 index 000000000000..4f5accb0fa9a --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnableToCreateStatic_after.java @@ -0,0 +1,12 @@ +interface I { + void m(int i); +} +class B { + class X { + private void m(int i1) { + System.out.println(""); + } + + I i = this::m; + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnusedParameterToConfirmSignature.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnusedParameterToConfirmSignature.java new file mode 100644 index 000000000000..525970547c9a --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnusedParameterToConfirmSignature.java @@ -0,0 +1,8 @@ +interface I { + void m(int i); +} +class B { + { + I i = i1 -> System.out.println(""); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnusedParameterToConfirmSignature_after.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnusedParameterToConfirmSignature_after.java new file mode 100644 index 000000000000..216cd22ca70b --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UnusedParameterToConfirmSignature_after.java @@ -0,0 +1,12 @@ +interface I { + void m(int i); +} +class B { + private static void m(int i1) { + System.out.println(""); + } + + { + I i = B::m; + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UsedLocalVariables.java b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UsedLocalVariables.java new file mode 100644 index 000000000000..355072ceb97c --- /dev/null +++ b/plugins/IntentionPowerPak/test/com/siyeh/ipp/functional/extractToMethodReference/UsedLocalVariables.java @@ -0,0 +1,9 @@ +interface I { + void m(int i); +} +class B { + { + int a = 0; + I i = i1 -> System.out.println(i1 + a); + } +} \ No newline at end of file diff --git a/plugins/IntentionPowerPak/testSrc/com/siyeh/ipp/functional/ExtractToMethodReferenceTest.java b/plugins/IntentionPowerPak/testSrc/com/siyeh/ipp/functional/ExtractToMethodReferenceTest.java new file mode 100644 index 000000000000..0fc88338c1f4 --- /dev/null +++ b/plugins/IntentionPowerPak/testSrc/com/siyeh/ipp/functional/ExtractToMethodReferenceTest.java @@ -0,0 +1,61 @@ +/* + * 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.siyeh.ipp.functional; + +import com.siyeh.IntentionPowerPackBundle; +import com.siyeh.ipp.IPPTestCase; + +public class ExtractToMethodReferenceTest extends IPPTestCase { + + public void testUnusedParameterToConfirmSignature() throws Exception { + doTest(); + } + + public void testOverloadedMethodSameName() throws Exception { + doTest(); + } + + public void testSameSignatureMethodExists() throws Exception { + doTest(); + } + + public void testUnableToCreateStatic() throws Exception { + doTest(); + } + + public void testRequiredTypeParameter() throws Exception { + doTest(); + } + + public void testConvertableToMethodReference() throws Exception { + assertIntentionNotAvailable(); + } + + public void testUsedLocalVariables() throws Exception { + assertIntentionNotAvailable(); + } + + @Override + protected String getIntentionName() { + return IntentionPowerPackBundle.message("extract.to.method.reference.intention.name"); + } + + @Override + protected String getRelativePath() { + return "functional/extractToMethodReference"; + } +} +