EA-72540 - AIOOBE: ExtractMethodSignatureSuggester.detectTopLevelExpressionsToReplace...
[idea/community.git] / java / java-impl / src / com / intellij / refactoring / extractMethod / ExtractMethodSignatureSuggester.java
1 /*
2  * Copyright 2000-2015 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.extractMethod;
17
18 import com.intellij.codeInsight.JavaPsiEquivalenceUtil;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.command.WriteCommandAction;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.diff.DiffManager;
23 import com.intellij.openapi.diff.SimpleContent;
24 import com.intellij.openapi.diff.SimpleDiffRequest;
25 import com.intellij.openapi.diff.ex.DiffPanelEx;
26 import com.intellij.openapi.diff.ex.DiffPanelOptions;
27 import com.intellij.openapi.diff.impl.ComparisonPolicy;
28 import com.intellij.openapi.diff.impl.processing.HighlightMode;
29 import com.intellij.openapi.project.Project;
30 import com.intellij.openapi.ui.DialogWrapper;
31 import com.intellij.psi.*;
32 import com.intellij.psi.codeStyle.JavaCodeStyleManager;
33 import com.intellij.psi.codeStyle.SuggestedNameInfo;
34 import com.intellij.psi.codeStyle.VariableKind;
35 import com.intellij.psi.search.LocalSearchScope;
36 import com.intellij.psi.search.searches.ReferencesSearch;
37 import com.intellij.psi.util.PsiTreeUtil;
38 import com.intellij.psi.util.PsiUtil;
39 import com.intellij.refactoring.util.RefactoringUtil;
40 import com.intellij.refactoring.util.VariableData;
41 import com.intellij.refactoring.util.duplicates.DuplicatesFinder;
42 import com.intellij.refactoring.util.duplicates.Match;
43 import com.intellij.refactoring.util.duplicates.MethodDuplicatesHandler;
44 import com.intellij.ui.IdeBorderFactory;
45 import com.intellij.util.text.UniqueNameGenerator;
46 import gnu.trove.THashMap;
47 import gnu.trove.THashSet;
48 import gnu.trove.TObjectHashingStrategy;
49 import org.jetbrains.annotations.Nullable;
50
51 import javax.swing.*;
52 import java.awt.*;
53 import java.util.*;
54 import java.util.List;
55
56 public class ExtractMethodSignatureSuggester {
57   private static final Logger LOG = Logger.getInstance("#" + ExtractMethodSignatureSuggester.class.getName());
58   private static final TObjectHashingStrategy<PsiExpression> ourEquivalenceStrategy = new TObjectHashingStrategy<PsiExpression>() {
59     @Override
60     public int computeHashCode(PsiExpression object) {
61       return RefactoringUtil.unparenthesizeExpression(object).getClass().hashCode();
62     }
63
64     @Override
65     public boolean equals(PsiExpression o1, PsiExpression o2) {
66       return JavaPsiEquivalenceUtil
67         .areExpressionsEquivalent(RefactoringUtil.unparenthesizeExpression(o1), RefactoringUtil.unparenthesizeExpression(o2));
68     }
69   };
70
71   private final Project myProject;
72   private final PsiElementFactory myElementFactory;
73
74   private PsiMethod myExtractedMethod;
75   private PsiMethodCallExpression myMethodCall;
76   private VariableData[] myVariableData;
77
78   public ExtractMethodSignatureSuggester(Project project,
79                                          PsiMethod extractedMethod,
80                                          PsiMethodCallExpression methodCall,
81                                          VariableData[] variableDatum) {
82     myProject = project;
83     myElementFactory = JavaPsiFacade.getElementFactory(project);
84
85     final PsiClass containingClass = extractedMethod.getContainingClass();
86     LOG.assertTrue(containingClass != null);
87     myExtractedMethod = myElementFactory.createMethodFromText(extractedMethod.getText(), containingClass.getLBrace());
88     myMethodCall = methodCall;
89     myVariableData = variableDatum;
90   }
91
92   public List<Match> getDuplicates(final PsiMethod method, final PsiMethodCallExpression methodCall, ParametersFolder folder) {
93     final List<Match> duplicates = findDuplicatesSignature(method, folder);
94     if (duplicates != null && !duplicates.isEmpty()) {
95       if (ApplicationManager.getApplication().isUnitTestMode() || 
96           new PreviewDialog(method, myExtractedMethod, methodCall, myMethodCall, duplicates.size()).showAndGet()) {
97         WriteCommandAction.runWriteCommandAction(myProject, new Runnable() {
98           @Override
99           public void run() {
100             myMethodCall = (PsiMethodCallExpression)methodCall.replace(myMethodCall);
101             myExtractedMethod = (PsiMethod)method.replace(myExtractedMethod);
102           }
103         });
104
105         final DuplicatesFinder finder = MethodDuplicatesHandler.createDuplicatesFinder(myExtractedMethod);
106         if (finder != null) {
107           final List<VariableData> datas = finder.getParameters().getInputVariables();
108           myVariableData = datas.toArray(new VariableData[datas.size()]);
109           return finder.findDuplicates(myExtractedMethod.getContainingClass());
110         }
111       }
112     }
113     return null;
114   }
115
116
117   public PsiMethod getExtractedMethod() {
118     return myExtractedMethod;
119   }
120
121   public PsiMethodCallExpression getMethodCall() {
122     return myMethodCall;
123   }
124
125   public VariableData[] getVariableData() {
126     return myVariableData;
127   }
128
129   @Nullable
130   public List<Match> findDuplicatesSignature(final PsiMethod method, ParametersFolder folder) {
131     final List<PsiExpression> copies = new ArrayList<PsiExpression>();
132     final InputVariables variables = detectTopLevelExpressionsToReplaceWithParameters(copies);
133     if (variables == null) {
134       return null;
135     }
136
137     final DuplicatesFinder defaultFinder = MethodDuplicatesHandler.createDuplicatesFinder(myExtractedMethod);
138     if (defaultFinder == null) {
139       return null; 
140     }
141
142     final DuplicatesFinder finder = new DuplicatesFinder(defaultFinder.getPattern(), variables, defaultFinder.getReturnValue(), new ArrayList<PsiVariable>()) {
143       @Override
144       protected boolean isSelf(PsiElement candidate) {
145         return PsiTreeUtil.isAncestor(method, candidate, true);
146       }
147     };
148     List<Match> duplicates = finder.findDuplicates(method.getContainingClass());
149
150     if (duplicates != null && !duplicates.isEmpty()) {
151       restoreRenamedParams(copies, folder);
152       if (!myMethodCall.isValid()) {
153         return null;
154       }
155       myMethodCall = (PsiMethodCallExpression)myMethodCall.copy();
156       inlineSameArguments(method, copies, variables, duplicates);
157       for (PsiExpression expression : copies) {
158         myMethodCall.getArgumentList().add(expression);
159       }
160       return duplicates;
161     }
162     else {
163       return null;
164     }
165   }
166
167   private void inlineSameArguments(PsiMethod method, List<PsiExpression> copies, InputVariables variables, List<Match> duplicates) {
168     final List<VariableData> variableDatum = variables.getInputVariables();
169     final Map<PsiVariable, PsiExpression> toInline = new HashMap<PsiVariable, PsiExpression>();
170     final int strongParamsCound = method.getParameterList().getParametersCount();
171     for (int i = strongParamsCound; i < variableDatum.size(); i++) {
172       VariableData variableData = variableDatum.get(i);
173       final THashSet<PsiExpression> map = new THashSet<PsiExpression>(ourEquivalenceStrategy);
174       if (!collectParamValues(duplicates, variableData, map)) {
175         continue;
176       }
177
178       final PsiExpression currentExpression = copies.get(i - strongParamsCound);
179       map.add(currentExpression);
180
181       if (map.size() == 1) {
182         toInline.put(variableData.variable, currentExpression);
183       }
184     }
185
186     if (!toInline.isEmpty()) {
187       copies.removeAll(toInline.values());
188       inlineArgumentsInMethodBody(toInline);
189       removeRedundantParametersFromMethodSignature(toInline);
190     }
191
192     removeUnusedStongParams(strongParamsCound);
193   }
194
195   private void removeUnusedStongParams(int strongParamsCound) {
196     final PsiExpression[] expressions = myMethodCall.getArgumentList().getExpressions();
197     final PsiParameter[] parameters = myExtractedMethod.getParameterList().getParameters();
198     final PsiCodeBlock body = myExtractedMethod.getBody();
199     if (body != null) {
200       final LocalSearchScope scope = new LocalSearchScope(body);
201       for(int i = strongParamsCound - 1; i >= 0; i--) {
202         final PsiParameter parameter = parameters[i];
203         if (ReferencesSearch.search(parameter, scope).findFirst() == null) {
204           parameter.delete();
205           expressions[i].delete();
206         }
207       }
208     }
209   }
210
211   private void removeRedundantParametersFromMethodSignature(Map<PsiVariable, PsiExpression> param2ExprMap) {
212     for (PsiParameter parameter : myExtractedMethod.getParameterList().getParameters()) {
213       if (param2ExprMap.containsKey(parameter)) {
214         parameter.delete();
215       }
216     }
217   }
218
219   private void inlineArgumentsInMethodBody(final Map<PsiVariable, PsiExpression> param2ExprMap) {
220     final Map<PsiExpression, PsiExpression> replacement = new HashMap<PsiExpression, PsiExpression>();
221     myExtractedMethod.accept(new JavaRecursiveElementWalkingVisitor() {
222       @Override
223       public void visitReferenceExpression(PsiReferenceExpression expression) {
224         super.visitReferenceExpression(expression);
225         final PsiElement resolve = expression.resolve();
226         if (resolve instanceof PsiVariable) {
227           final PsiExpression toInlineExpr = param2ExprMap.get((PsiVariable)resolve);
228           if (toInlineExpr != null) {
229             replacement.put(expression, toInlineExpr);
230           }
231         }
232       }
233     });
234     for (PsiExpression expression : replacement.keySet()) {
235       expression.replace(replacement.get(expression));
236     }
237   }
238
239   private static boolean collectParamValues(List<Match> duplicates, VariableData variableData, THashSet<PsiExpression> map) {
240     for (Match duplicate : duplicates) {
241       final List<PsiElement> values = duplicate.getParameterValues(variableData.variable);
242       if (values == null || values.isEmpty()) {
243         return false;
244       }
245       boolean found = false;
246       for (PsiElement value : values) {
247         if (value instanceof PsiExpression) {
248           map.add((PsiExpression)value);
249           found = true;
250           break;
251         }
252       }
253       if (!found) return false;
254     }
255     return true;
256   }
257
258   private void restoreRenamedParams(List<PsiExpression> copies, ParametersFolder folder) {
259     final Map<String, String> renameMap = new HashMap<String, String>();
260     for (VariableData data : myVariableData) {
261       final String replacement = folder.getGeneratedCallArgument(data);
262       if (!data.name.equals(replacement)) {
263         renameMap.put(data.name, replacement);
264       }
265     }
266
267     if (!renameMap.isEmpty()) {
268       for (PsiExpression currentExpression : copies) {
269         final Map<PsiReferenceExpression, String> params = new HashMap<PsiReferenceExpression, String>();
270         currentExpression.accept(new JavaRecursiveElementWalkingVisitor() {
271           @Override
272           public void visitReferenceExpression(PsiReferenceExpression expression) {
273             super.visitReferenceExpression(expression);
274             final PsiElement resolve = expression.resolve();
275             if (resolve instanceof PsiParameter && myExtractedMethod.equals(((PsiParameter)resolve).getDeclarationScope())) {
276               final String name = ((PsiParameter)resolve).getName();
277               final String variable = renameMap.get(name);
278               if (renameMap.containsKey(name)) {
279                 params.put(expression, variable);
280               }
281             }
282           }
283         });
284         for (PsiReferenceExpression expression : params.keySet()) {
285           final String var = params.get(expression);
286           expression.replace(myElementFactory.createExpressionFromText(var, expression));
287         }
288       }
289     }
290   }
291
292
293   @Nullable
294   private InputVariables detectTopLevelExpressionsToReplaceWithParameters(List<PsiExpression> copies) {
295     final PsiParameter[] parameters = myExtractedMethod.getParameterList().getParameters();
296     final List<PsiVariable> inputVariables = new ArrayList<PsiVariable>(Arrays.asList(parameters));
297     final PsiCodeBlock body = myExtractedMethod.getBody();
298     LOG.assertTrue(body != null);
299     final PsiStatement[] pattern = body.getStatements();
300     final List<PsiExpression> exprs = new ArrayList<PsiExpression>();
301     for (PsiStatement statement : pattern) {
302       if (statement instanceof PsiExpressionStatement) {
303         final PsiExpression expression = ((PsiExpressionStatement)statement).getExpression();
304         if (expression instanceof PsiIfStatement || expression instanceof PsiLoopStatement) {
305           continue;
306         }
307       }
308       statement.accept(new JavaRecursiveElementWalkingVisitor() {
309         @Override
310         public void visitCallExpression(PsiCallExpression callExpression) {
311           final PsiExpressionList list = callExpression.getArgumentList();
312           if (list != null) {
313             for (PsiExpression expression : list.getExpressions()) {
314               if (expression instanceof PsiReferenceExpression) {
315                 final PsiElement resolve = ((PsiReferenceExpression)expression).resolve();
316                 if (resolve instanceof PsiField) {
317                   exprs.add(expression);
318                 }
319               } else {
320                 exprs.add(expression);
321               }
322             }
323           }
324         }
325       });
326     }
327
328     if (exprs.isEmpty()) {
329       return null;
330     }
331
332     final UniqueNameGenerator uniqueNameGenerator = new UniqueNameGenerator();
333     for (PsiParameter parameter : parameters) {
334       uniqueNameGenerator.addExistingName(parameter.getName());
335     }
336     final THashMap<PsiExpression, String> unique = new THashMap<PsiExpression, String>(ourEquivalenceStrategy);
337     final Map<PsiExpression, String> replacement = new HashMap<PsiExpression, String>();
338     for (PsiExpression expr : exprs) {
339       String name = unique.get(expr);
340       if (name == null) {
341
342         final PsiType type = GenericsUtil.getVariableTypeByExpressionType(expr.getType());
343         if (type == null || type == PsiType.NULL || PsiUtil.resolveClassInType(type) instanceof PsiAnonymousClass) return null;
344
345         copies.add(myElementFactory.createExpressionFromText(expr.getText(), body));
346
347         final SuggestedNameInfo info = JavaCodeStyleManager.getInstance(myProject).suggestVariableName(VariableKind.PARAMETER, null, expr, null);
348         final String paramName = info.names.length > 0 ? info.names[0] : "p";
349         name = uniqueNameGenerator.generateUniqueName(paramName);
350
351         final PsiParameter parameter = (PsiParameter)myExtractedMethod.getParameterList().add(myElementFactory.createParameter(name, type));
352         inputVariables.add(parameter);
353         unique.put(expr, name);
354       }
355       replacement.put(expr, name);
356     }
357
358     for (PsiExpression expression : replacement.keySet()) {
359       expression.replace(myElementFactory.createExpressionFromText(replacement.get(expression), null));
360     }
361
362     return new InputVariables(inputVariables, myExtractedMethod.getProject(), new LocalSearchScope(myExtractedMethod), false);
363   }
364
365   private static class PreviewDialog extends DialogWrapper {
366     private final PsiMethod myOldMethod;
367     private final PsiMethod myNewMethod;
368     private final PsiMethodCallExpression myOldCall;
369     private final PsiMethodCallExpression myNewCall;
370     private final int myDuplicatesNumber;
371
372     public PreviewDialog(PsiMethod oldMethod,
373                          PsiMethod newMethod,
374                          PsiMethodCallExpression oldMethodCall,
375                          PsiMethodCallExpression newMethodCall,
376                          int duplicatesNumber) {
377       super(oldMethod.getProject());
378       myOldMethod = oldMethod;
379       myNewMethod = newMethod;
380       myOldCall = oldMethodCall;
381       myNewCall = newMethodCall;
382       myDuplicatesNumber = duplicatesNumber;
383       setTitle("Extract Parameters to Replace Duplicates");
384       setOKButtonText("Accept Signature Change");
385       setCancelButtonText("Keep Original Signature");
386       init();
387     }
388
389     @Nullable
390     @Override
391     protected JComponent createNorthPanel() {
392       return new JLabel("<html><b>No exact method duplicates were found</b>, though changed method as shown below has " + myDuplicatesNumber + " duplicate" + (myDuplicatesNumber > 1 ? "s" : "") + " </html>");
393     }
394
395     @Nullable
396     @Override
397     protected JComponent createCenterPanel() {
398       final Project project = myOldMethod.getProject();
399       final DiffPanelEx diffPanel = (DiffPanelEx)DiffManager.getInstance().createDiffPanel(null, project, getDisposable(), null);
400       diffPanel.setComparisonPolicy(ComparisonPolicy.IGNORE_SPACE);
401       diffPanel.setHighlightMode(HighlightMode.BY_WORD);
402       DiffPanelOptions diffPanelOptions = diffPanel.getOptions();
403       diffPanelOptions.setShowSourcePolicy(DiffPanelOptions.ShowSourcePolicy.OPEN_EDITOR);
404       diffPanelOptions.setRequestFocusOnNewContent(false);
405       SimpleDiffRequest request = new SimpleDiffRequest(project, null);
406       final String oldContent = myOldMethod.getText() + "\n\n\nmethod call:\n " + myOldCall.getText();
407       final String newContent = myNewMethod.getText() + "\n\n\nmethod call:\n " + myNewCall.getText();
408       request.setContents(new SimpleContent(oldContent), new SimpleContent(newContent));
409       request.setContentTitles("Before", "After");
410       diffPanel.setDiffRequest(request);
411       
412       final JPanel panel = new JPanel(new BorderLayout());
413       panel.add(diffPanel.getComponent(), BorderLayout.CENTER);
414       panel.setBorder(IdeBorderFactory.createEmptyBorder(new Insets(5, 0, 0, 0)));
415       return panel;
416     }
417   }
418 }