intention to extract lambda body to method and convert to method reference (IDEA-123262)

This commit is contained in:
Anna.Kozlova
2017-04-07 09:51:10 +02:00
parent 412d071d08
commit acffacf0ad
19 changed files with 437 additions and 0 deletions
@@ -522,5 +522,10 @@
<bundleName>com.siyeh.IntentionPowerPackBundle</bundleName>
<categoryKey>intention.category.other</categoryKey>
</intentionAction>
<intentionAction>
<className>com.siyeh.ipp.functional.ExtractToMethodReferenceIntention</className>
<bundleName>com.siyeh.IntentionPowerPackBundle</bundleName>
<categoryKey>intention.category.control.flow</categoryKey>
</intentionAction>
</extensions>
</idea-plugin>
@@ -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
@@ -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<PsiVariable> 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<String> suggestedNames = new ArrayList<>();
suggestedNames.add(method.getName());
processor.substituteElementToRename(method, editor, new Pass<PsiElement>() {
@Override
public void pass(PsiElement substitutedElement) {
final MemberInplaceRenamer renamer = new MemberInplaceRenamer(method, substitutedElement, editor);
final LinkedHashSet<String> 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;
}
}
}
@@ -0,0 +1,10 @@
class X {
{
JButton b = new JButton();
b.addActionListener(this::clicked);
}
private void clicked(ActionEvent e) {
System.out.println("clicked");
}
}
@@ -0,0 +1,8 @@
class X {
{
JButton b = new JButton();
b.addActionListener(e -> {
<spot>System.out.println("clicked");</spot>
});
}
}
@@ -0,0 +1,5 @@
<html>
<body>
<p>Intention to extract the lambda body to a method and convert lambda to method reference.</p>
</body>
</html>
@@ -0,0 +1,8 @@
interface I {
void m(int i);
}
class B {
{
I i = i1 -> System.out.prin<caret>tln(i1);
}
}
@@ -0,0 +1,9 @@
interface I {
void m(int i);
}
class B {
void m() {}
{
I i = i1 -> System.out.prin<caret>tln(i1 + "");
}
}
@@ -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;
}
}
@@ -0,0 +1,11 @@
interface I<T> {
void m(T t);
}
class B {
<N> void n(){
I<N> i = i1 -> {
System.out.prin<caret>tln(i1);
System.out.println(i1);
}
}
}
@@ -0,0 +1,13 @@
interface I<T> {
void m(T t);
}
class B {
private static <N> void m(N i1) {
System.out.println(i1);
System.out.println(i1);
}
<N> void n(){
I<N> i = B::m
}
}
@@ -0,0 +1,9 @@
interface I {
void m(int i);
}
class B {
void m(int i) {}
{
I i = i1 -> System.out.prin<caret>tln(i1 + "");
}
}
@@ -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;
}
}
@@ -0,0 +1,8 @@
interface I {
void m(int i);
}
class B {
class X {
I i = i1 -> System.out.prin<caret>tln("");
}
}
@@ -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;
}
}
@@ -0,0 +1,8 @@
interface I {
void m(int i);
}
class B {
{
I i = i1 -> System.out.prin<caret>tln("");
}
}
@@ -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;
}
}
@@ -0,0 +1,9 @@
interface I {
void m(int i);
}
class B {
{
int a = 0;
I i = i1 -> System.out.prin<caret>tln(i1 + a);
}
}
@@ -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";
}
}