94606da170a22f6ad64e074b8dff6265260fb277
[idea/community.git] / java / java-impl / src / com / intellij / refactoring / extractMethodObject / ExtractLightMethodObjectHandler.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.refactoring.extractMethodObject;
17
18 import com.intellij.codeInsight.CodeInsightUtil;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.util.TextRange;
22 import com.intellij.openapi.util.text.StringUtil;
23 import com.intellij.psi.*;
24 import com.intellij.psi.codeStyle.CodeStyleManager;
25 import com.intellij.psi.codeStyle.JavaCodeStyleManager;
26 import com.intellij.psi.controlFlow.*;
27 import com.intellij.psi.util.PsiTreeUtil;
28 import com.intellij.psi.util.PsiUtil;
29 import com.intellij.refactoring.extractMethod.AbstractExtractDialog;
30 import com.intellij.refactoring.extractMethod.InputVariables;
31 import com.intellij.refactoring.extractMethod.PrepareFailedException;
32 import com.intellij.refactoring.util.RefactoringUtil;
33 import com.intellij.refactoring.util.VariableData;
34 import com.intellij.usageView.UsageInfo;
35 import com.intellij.util.IncorrectOperationException;
36 import com.intellij.util.VisibilityUtil;
37 import com.intellij.util.containers.ContainerUtil;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.util.List;
42
43 public class ExtractLightMethodObjectHandler {
44   private static final Logger LOG = Logger.getInstance(ExtractLightMethodObjectHandler.class);
45
46   public static class ExtractedData {
47     private final String myGeneratedCallText;
48     private final PsiClass myGeneratedInnerClass;
49     private final PsiElement myAnchor;
50
51     public ExtractedData(String generatedCallText, PsiClass generatedInnerClass, PsiElement anchor) {
52       myGeneratedCallText = generatedCallText;
53       myGeneratedInnerClass = generatedInnerClass;
54       myAnchor = anchor;
55     }
56
57     public PsiElement getAnchor() {
58       return myAnchor;
59     }
60
61     public String getGeneratedCallText() {
62       return myGeneratedCallText;
63     }
64
65     public PsiClass getGeneratedInnerClass() {
66       return myGeneratedInnerClass;
67     }
68   }
69
70   @Nullable
71   public static ExtractedData extractLightMethodObject(final Project project,
72                                                        @Nullable PsiElement originalContext,
73                                                        @NotNull final PsiCodeFragment fragment,
74                                                        final String methodName) throws PrepareFailedException {
75     final PsiElementFactory elementFactory = JavaPsiFacade.getElementFactory(project);
76     PsiElement[] elements = completeToStatementArray(fragment, elementFactory);
77     if (elements == null) {
78       elements = CodeInsightUtil.findStatementsInRange(fragment, 0, fragment.getTextLength());
79     }
80     if (elements.length == 0) {
81       return null;
82     }
83
84     if (originalContext == null) {
85       return null;
86     }
87
88     PsiFile file = originalContext.getContainingFile();
89
90     final PsiFile copy = PsiFileFactory.getInstance(project)
91       .createFileFromText(file.getName(), file.getFileType(), file.getText(), file.getModificationStamp(), false);
92
93     if (originalContext instanceof PsiKeyword && PsiModifier.PRIVATE.equals(originalContext.getText())) {
94       final PsiNameIdentifierOwner identifierOwner = PsiTreeUtil.getParentOfType(originalContext, PsiNameIdentifierOwner.class);
95       if (identifierOwner != null) {
96         final PsiElement identifier = identifierOwner.getNameIdentifier();
97         if (identifier != null) {
98           originalContext = identifier;
99         }
100       }
101     }
102
103     final TextRange range = originalContext.getTextRange();
104     PsiElement originalAnchor = CodeInsightUtil.findElementInRange(copy, range.getStartOffset(), range.getEndOffset(), originalContext.getClass());
105     if (originalAnchor == null) {
106       final PsiElement elementAt = copy.findElementAt(range.getStartOffset());
107       if (elementAt != null && elementAt.getClass() == originalContext.getClass()) {
108         originalAnchor = PsiTreeUtil.skipWhitespacesForward(elementAt);
109       }
110     }
111
112     final PsiClass containingClass = PsiTreeUtil.getParentOfType(originalAnchor, PsiClass.class, false);
113     if (containingClass == null) {
114       return null;
115     }
116
117     // expand lambda to code block if needed
118     PsiElement containingMethod = PsiTreeUtil.getParentOfType(originalAnchor, PsiMember.class, PsiLambdaExpression.class);
119     if (containingMethod instanceof PsiLambdaExpression) {
120       PsiCodeBlock newBody = RefactoringUtil.expandExpressionLambdaToCodeBlock((PsiLambdaExpression)containingMethod);
121       originalAnchor = newBody.getStatements()[0];
122     }
123
124     PsiElement anchor = RefactoringUtil.getParentStatement(originalAnchor, false);
125     if (anchor == null) {
126       if (PsiTreeUtil.getParentOfType(originalAnchor, PsiCodeBlock.class) != null) {
127         anchor = originalAnchor;
128       }
129     }
130
131     PsiElement container;
132     if (anchor == null) {
133       container = ((PsiClassInitializer)containingClass.add(elementFactory.createClassInitializer())).getBody();
134       anchor = container.getLastChild();
135     }
136     else {
137       container = anchor.getParent();
138     }
139
140     // add code blocks for ifs and loops if needed
141     if (anchor instanceof PsiStatement && RefactoringUtil.isLoopOrIf(container)) {
142       PsiBlockStatement codeBlockStatement =
143         (PsiBlockStatement)JavaPsiFacade.getElementFactory(project).createStatementFromText("{}", container);
144       codeBlockStatement.getCodeBlock().add(anchor);
145       PsiCodeBlock codeBlock = ((PsiBlockStatement)anchor.replace(codeBlockStatement)).getCodeBlock();
146       anchor = codeBlock.getStatements()[0];
147       originalAnchor = anchor;
148       container = codeBlock;
149     }
150
151     final PsiElement firstElementCopy = container.addRangeBefore(elements[0], elements[elements.length - 1], anchor);
152     final PsiElement[] elementsCopy = CodeInsightUtil.findStatementsInRange(copy,
153                                                                             firstElementCopy.getTextRange().getStartOffset(),
154                                                                             anchor.getTextRange().getStartOffset());
155     if (elementsCopy.length == 0) {
156       return null;
157     }
158     if (elementsCopy[elementsCopy.length - 1] instanceof PsiExpressionStatement) {
159       final PsiExpression expr = ((PsiExpressionStatement)elementsCopy[elementsCopy.length - 1]).getExpression();
160       if (!(expr instanceof PsiAssignmentExpression)) {
161         PsiType expressionType = GenericsUtil.getVariableTypeByExpressionType(expr.getType());
162         if (expressionType instanceof PsiDisjunctionType) {
163           expressionType = ((PsiDisjunctionType)expressionType).getLeastUpperBound();
164         }
165         if (isValidVariableType(expressionType)) {
166           final String uniqueResultName = JavaCodeStyleManager.getInstance(project).suggestUniqueVariableName("result", elementsCopy[0], true);
167           final String statementText = expressionType.getCanonicalText() + " " + uniqueResultName + " = " + expr.getText() + ";";
168           elementsCopy[elementsCopy.length - 1] = elementsCopy[elementsCopy.length - 1]
169             .replace(elementFactory.createStatementFromText(statementText, elementsCopy[elementsCopy.length - 1]));
170         }
171       }
172     }
173
174     LOG.assertTrue(elementsCopy[0].getParent() == container, "element: " +  elementsCopy[0].getText() + "; container: " + container.getText());
175     final int startOffsetInContainer = elementsCopy[0].getStartOffsetInParent();
176
177     final ControlFlow controlFlow;
178     try {
179       controlFlow = ControlFlowFactory.getInstance(project).getControlFlow(container, LocalsOrMyInstanceFieldsControlFlowPolicy.getInstance(), false, false);
180     }
181     catch (AnalysisCanceledException e) {
182       return null;
183     }
184
185     List<PsiVariable> variables = ControlFlowUtil.getUsedVariables(controlFlow,
186                                                                    controlFlow.getStartOffset(elementsCopy[0]),
187                                                                    controlFlow.getEndOffset(elementsCopy[elementsCopy.length - 1]));
188
189     variables = ContainerUtil.filter(variables, variable -> {
190       PsiElement variableScope = PsiUtil.getVariableCodeBlock(variable, null);
191       return variableScope != null && PsiTreeUtil.isAncestor(variableScope, elementsCopy[elementsCopy.length - 1], true);
192     });
193
194
195     final String outputVariables = StringUtil.join(variables, variable -> "\"variable: \" + " + variable.getName(), " +");
196     PsiStatement outStatement = elementFactory.createStatementFromText("System.out.println(" + outputVariables + ");", anchor);
197     outStatement = (PsiStatement)container.addAfter(outStatement, elementsCopy[elementsCopy.length - 1]);
198
199     copy.accept(new JavaRecursiveElementWalkingVisitor() {
200       private void makePublic(PsiMember method) {
201         if (method.hasModifierProperty(PsiModifier.PRIVATE)) {
202           VisibilityUtil.setVisibility(method.getModifierList(), PsiModifier.PUBLIC);
203         }
204       }
205
206       @Override
207       public void visitMethod(PsiMethod method) {
208         super.visitMethod(method);
209         makePublic(method);
210       }
211
212       @Override
213       public void visitField(PsiField field) {
214         super.visitField(field);
215         makePublic(field);
216       }
217     });
218
219     final ExtractMethodObjectProcessor extractMethodObjectProcessor = new ExtractMethodObjectProcessor(project, null, elementsCopy, "") {
220       @Override
221       protected AbstractExtractDialog createExtractMethodObjectDialog(MyExtractMethodProcessor processor) {
222         return new LightExtractMethodObjectDialog(this, methodName);
223       }
224
225       @Override
226       protected boolean isFoldingApplicable() {
227         return false;
228       }
229     };
230     extractMethodObjectProcessor.getExtractProcessor().setShowErrorDialogs(false);
231
232     final ExtractMethodObjectProcessor.MyExtractMethodProcessor extractProcessor = extractMethodObjectProcessor.getExtractProcessor();
233     if (extractProcessor.prepare()) {
234       if (extractProcessor.showDialog()) {
235         try {
236           extractProcessor.doExtract();
237           final UsageInfo[] usages = extractMethodObjectProcessor.findUsages();
238           extractMethodObjectProcessor.performRefactoring(usages);
239           extractMethodObjectProcessor.runChangeSignature();
240         }
241         catch (IncorrectOperationException e) {
242           LOG.error(e);
243         }
244         if (extractMethodObjectProcessor.isCreateInnerClass()) {
245           extractMethodObjectProcessor.changeInstanceAccess(project);
246         }
247         final PsiElement method = extractMethodObjectProcessor.getMethod();
248         LOG.assertTrue(method != null);
249         method.delete();
250       }
251     } else {
252       return null;
253     }
254
255     final int startOffset = startOffsetInContainer + container.getTextRange().getStartOffset();
256     final String generatedCall = copy.getText().substring(startOffset, outStatement.getTextOffset());
257     return new ExtractedData(generatedCall,
258                              (PsiClass)CodeStyleManager.getInstance(project).reformat(extractMethodObjectProcessor.getInnerClass()),
259                              originalAnchor);
260   }
261
262   @Nullable
263   private static PsiElement[] completeToStatementArray(PsiCodeFragment fragment, PsiElementFactory elementFactory) {
264     PsiExpression expression = CodeInsightUtil.findExpressionInRange(fragment, 0, fragment.getTextLength());
265     if (expression != null) {
266       String completeExpressionText = null;
267       if (expression instanceof PsiArrayInitializerExpression) {
268         final PsiExpression[] initializers = ((PsiArrayInitializerExpression)expression).getInitializers();
269         if (initializers.length > 0) {
270           final PsiType type = initializers[0].getType();
271           if (type != null) {
272             completeExpressionText = "new " + type.getCanonicalText() + "[]" + expression.getText();
273           }
274         }
275       } else {
276         completeExpressionText = expression.getText();
277       }
278
279       if (completeExpressionText != null) {
280         return new PsiElement[] {elementFactory.createStatementFromText(completeExpressionText + ";", expression)};
281       }
282     }
283     return null;
284   }
285
286   private static boolean isValidVariableType(PsiType type) {
287     if (type instanceof PsiClassType ||
288         type instanceof PsiArrayType ||
289         type instanceof PsiPrimitiveType && !PsiType.VOID.equals(type)) {
290       return true;
291     }
292     return false;
293   }
294
295   private static class LightExtractMethodObjectDialog implements AbstractExtractDialog {
296     private final ExtractMethodObjectProcessor myProcessor;
297     private final String myMethodName;
298
299     public LightExtractMethodObjectDialog(ExtractMethodObjectProcessor processor, String methodName) {
300       myProcessor = processor;
301       myMethodName = methodName;
302     }
303
304     @Override
305     public String getChosenMethodName() {
306       return myMethodName;
307     }
308
309     @Override
310     public VariableData[] getChosenParameters() {
311       final InputVariables inputVariables = myProcessor.getExtractProcessor().getInputVariables();
312       return inputVariables.getInputVariables().toArray(new VariableData[0]);
313     }
314
315     @NotNull
316     @Override
317     public String getVisibility() {
318       return PsiModifier.PACKAGE_LOCAL;
319     }
320
321     @Override
322     public boolean isMakeStatic() {
323       return myProcessor.getExtractProcessor().isCanBeStatic() && !myProcessor.getExtractProcessor().getInputVariables().hasInstanceFields();
324     }
325
326     @Override
327     public boolean isChainedConstructor() {
328       return false;
329     }
330
331     @Override
332     public PsiType getReturnType() {
333       return null;
334     }
335
336     @Override
337     public void show() {}
338
339     @Override
340     public boolean isOK() {
341       return true;
342     }
343   }
344 }