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