run intentions under transaction (IDEA-CR-15393)
[idea/community.git] / java / java-impl / src / com / intellij / codeInsight / intention / impl / BindFieldsFromParametersAction.java
1 /*
2  * Copyright 2000-2014 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.codeInsight.intention.impl;
17
18 import com.intellij.codeInsight.CodeInsightBundle;
19 import com.intellij.codeInsight.FileModificationService;
20 import com.intellij.codeInsight.intention.HighPriorityAction;
21 import com.intellij.ide.util.MemberChooser;
22 import com.intellij.lang.java.JavaLanguage;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.diagnostic.Logger;
25 import com.intellij.openapi.editor.Editor;
26 import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
27 import com.intellij.openapi.project.Project;
28 import com.intellij.openapi.ui.DialogWrapper;
29 import com.intellij.openapi.util.Key;
30 import com.intellij.psi.*;
31 import com.intellij.psi.codeStyle.*;
32 import com.intellij.psi.search.LocalSearchScope;
33 import com.intellij.psi.search.searches.ReferencesSearch;
34 import com.intellij.psi.util.PsiTreeUtil;
35 import com.intellij.psi.util.PsiUtil;
36 import com.intellij.util.IncorrectOperationException;
37 import com.intellij.util.containers.ContainerUtil;
38 import com.intellij.util.containers.MultiMap;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import java.util.*;
43
44 /**
45  * @author Danila Ponomarenko
46  */
47 public class BindFieldsFromParametersAction extends BaseIntentionAction implements HighPriorityAction {
48   private static final Logger LOG = Logger.getInstance(CreateFieldFromParameterAction.class);
49   private static final Key<Map<SmartPsiElementPointer<PsiParameter>, Boolean>> PARAMS = Key.create("FIELDS_FROM_PARAMS");
50
51   private static final Object LOCK = new Object();
52
53   @Override
54   public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
55     PsiParameter psiParameter = FieldFromParameterUtils.findParameterAtCursor(file, editor);
56     PsiMethod method = findMethod(psiParameter, editor, file);
57     if (method == null) return false;
58
59     final List<PsiParameter> parameters = getAvailableParameters(method);
60
61     synchronized (LOCK) {
62       final Collection<SmartPsiElementPointer<PsiParameter>> params = getUnboundedParams(method);
63       params.clear();
64       for (PsiParameter parameter : parameters) {
65         params.add(SmartPointerManager.getInstance(project).createSmartPsiElementPointer(parameter));
66       }
67       if (params.isEmpty()) return false;
68       if (params.size() == 1 && psiParameter != null) return false;
69       if (psiParameter == null) {
70         psiParameter = params.iterator().next().getElement();
71         LOG.assertTrue(psiParameter != null);
72       }
73
74       setText(CodeInsightBundle.message("intention.bind.fields.from.parameters.text", method.isConstructor() ? "constructor" : "method"));
75     }
76     return isAvailable(psiParameter);
77   }
78
79   @Nullable
80   private static PsiMethod findMethod(@Nullable PsiParameter parameter, @NotNull Editor editor, @NotNull PsiFile file) {
81     if (parameter == null) {
82       final PsiElement elementAt = file.findElementAt(editor.getCaretModel().getOffset());
83       if (elementAt instanceof PsiIdentifier) {
84         final PsiElement parent = elementAt.getParent();
85         if (parent instanceof PsiMethod) {
86           return (PsiMethod)parent;
87         }
88       }
89     }
90     else {
91       final PsiElement declarationScope = parameter.getDeclarationScope();
92       if (declarationScope instanceof PsiMethod) {
93         return (PsiMethod)declarationScope;
94       }
95     }
96
97     return null;
98   }
99
100   @NotNull
101   private static List<PsiParameter> getAvailableParameters(@NotNull PsiMethod method) {
102     final List<PsiParameter> parameters = new ArrayList<>();
103     for (PsiParameter parameter : method.getParameterList().getParameters()) {
104       if (isAvailable(parameter)) {
105         parameters.add(parameter);
106       }
107     }
108     return parameters;
109   }
110
111   private static boolean isAvailable(PsiParameter psiParameter) {
112     final PsiType type = FieldFromParameterUtils.getSubstitutedType(psiParameter);
113     final PsiClass targetClass = PsiTreeUtil.getParentOfType(psiParameter, PsiClass.class);
114     return FieldFromParameterUtils.isAvailable(psiParameter, type, targetClass) &&
115            psiParameter.getLanguage().isKindOf(JavaLanguage.INSTANCE);
116   }
117
118   @NotNull
119   private static Collection<SmartPsiElementPointer<PsiParameter>> getUnboundedParams(PsiMethod psiMethod) {
120     Map<SmartPsiElementPointer<PsiParameter>, Boolean> params = psiMethod.getUserData(PARAMS);
121     if (params == null) psiMethod.putUserData(PARAMS, params = ContainerUtil.createConcurrentWeakMap());
122     final Map<SmartPsiElementPointer<PsiParameter>, Boolean> finalParams = params;
123     return new AbstractCollection<SmartPsiElementPointer<PsiParameter>>() {
124       @Override
125       public boolean add(SmartPsiElementPointer<PsiParameter> psiVariable) {
126         return finalParams.put(psiVariable, Boolean.TRUE) == null;
127       }
128
129       @Override
130       public Iterator<SmartPsiElementPointer<PsiParameter>> iterator() {
131         return finalParams.keySet().iterator();
132       }
133
134       @Override
135       public int size() {
136         return finalParams.size();
137       }
138
139       @Override
140       public void clear() {
141         finalParams.clear();
142       }
143     };
144   }
145
146   @Override
147   @NotNull
148   public String getFamilyName() {
149     return CodeInsightBundle.message("intention.bind.fields.from.parameters.family");
150   }
151
152   @Override
153   public void invoke(@NotNull Project project, Editor editor, PsiFile file) {
154     invoke(project, editor, file, !ApplicationManager.getApplication().isUnitTestMode());
155   }
156
157   private static void invoke(final Project project, Editor editor, PsiFile file, boolean isInteractive) {
158     PsiParameter psiParameter = FieldFromParameterUtils.findParameterAtCursor(file, editor);
159     if (!FileModificationService.getInstance().prepareFileForWrite(file)) return;
160     final PsiMethod method = psiParameter != null ? (PsiMethod)psiParameter.getDeclarationScope() : PsiTreeUtil.getParentOfType(file.findElementAt(editor.getCaretModel().getOffset()), PsiMethod.class);
161     LOG.assertTrue(method != null);
162
163     final HashSet<String> usedNames = new HashSet<>();
164     final Iterable<PsiParameter> parameters = selectParameters(project, method, copyUnboundedParamsAndClearOriginal(method), isInteractive);
165     final MultiMap<PsiType, PsiParameter> types = new MultiMap<>();
166     for (PsiParameter parameter : parameters) {
167       types.putValue(parameter.getType(), parameter);
168     }
169     final CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(project);
170     final boolean preferLongerNames = settings.PREFER_LONGER_NAMES;
171     for (PsiParameter selected : parameters) {
172       try {
173         settings.PREFER_LONGER_NAMES = preferLongerNames || types.get(selected.getType()).size() > 1;
174         processParameter(project, selected, usedNames);
175       } finally {
176         settings.PREFER_LONGER_NAMES = preferLongerNames;
177       }
178     }
179   }
180
181   @NotNull
182   private static Iterable<PsiParameter> selectParameters(@NotNull Project project,
183                                                          @NotNull PsiMethod method,
184                                                          @NotNull Collection<SmartPsiElementPointer<PsiParameter>> unboundedParams,
185                                                          boolean isInteractive) {
186     if (unboundedParams.size() < 2 || !isInteractive) {
187       return revealPointers(unboundedParams);
188     }
189
190     final ParameterClassMember[] members = sortByParameterIndex(toClassMemberArray(unboundedParams), method);
191
192     final MemberChooser<ParameterClassMember> chooser = showChooser(project, method, members);
193
194     final List<ParameterClassMember> selectedElements = chooser.getSelectedElements();
195     if (chooser.getExitCode() != DialogWrapper.OK_EXIT_CODE || selectedElements == null) {
196       return Collections.emptyList();
197     }
198
199     return revealParameterClassMembers(selectedElements);
200   }
201
202   @NotNull
203   private static MemberChooser<ParameterClassMember> showChooser(@NotNull Project project,
204                                            @NotNull PsiMethod method,
205                                            @NotNull ParameterClassMember[] members) {
206     final MemberChooser<ParameterClassMember> chooser = new MemberChooser<>(members, false, true, project);
207     chooser.selectElements(getInitialSelection(method, members));
208     chooser.setTitle("Choose " + (method.isConstructor() ? "Constructor" : "Method") + " Parameters");
209     chooser.show();
210     return chooser;
211   }
212
213   /**
214    * Exclude parameters passed to super() or this() calls from initial selection
215    */
216   private static ParameterClassMember[] getInitialSelection(@NotNull PsiMethod method,
217                                                             @NotNull ParameterClassMember[] members) {
218     final Set<PsiElement> resolvedInSuperOrThis = new HashSet<>();
219     final PsiCodeBlock body = method.getBody();
220     LOG.assertTrue(body != null);
221     final PsiStatement[] statements = body.getStatements();
222     if (statements.length > 0 && statements[0] instanceof PsiExpressionStatement) {
223       final PsiExpression expression = ((PsiExpressionStatement)statements[0]).getExpression();
224       if (expression instanceof PsiMethodCallExpression) {
225         final PsiMethod calledMethod = ((PsiMethodCallExpression)expression).resolveMethod();
226         if (calledMethod != null && calledMethod.isConstructor()) {
227           for (PsiExpression arg : ((PsiMethodCallExpression)expression).getArgumentList().getExpressions()) {
228             if (arg instanceof PsiReferenceExpression) {
229               ContainerUtil.addIfNotNull(((PsiReferenceExpression)arg).resolve(), resolvedInSuperOrThis);
230             }
231           }
232         }
233       }
234     }
235     return ContainerUtil.findAll(members, member -> !resolvedInSuperOrThis.contains(member.getParameter())).toArray(ParameterClassMember.EMPTY_ARRAY);
236   }
237
238   @NotNull
239   private static ParameterClassMember[] sortByParameterIndex(@NotNull ParameterClassMember[] members, @NotNull PsiMethod method) {
240     final PsiParameterList parameterList = method.getParameterList();
241     Arrays.sort(members, (o1, o2) -> parameterList.getParameterIndex(o1.getParameter()) -
242                                  parameterList.getParameterIndex(o2.getParameter()));
243     return members;
244   }
245
246   @NotNull
247   private static <T extends PsiElement> List<T> revealPointers(@NotNull Iterable<SmartPsiElementPointer<T>> pointers) {
248     final List<T> result = new ArrayList<>();
249     for (SmartPsiElementPointer<T> pointer : pointers) {
250       result.add(pointer.getElement());
251     }
252     return result;
253   }
254
255   @NotNull
256   private static List<PsiParameter> revealParameterClassMembers(@NotNull Iterable<ParameterClassMember> parameterClassMembers) {
257     final List<PsiParameter> result = new ArrayList<>();
258     for (ParameterClassMember parameterClassMember : parameterClassMembers) {
259       result.add(parameterClassMember.getParameter());
260     }
261     return result;
262   }
263
264   @NotNull
265   private static ParameterClassMember[] toClassMemberArray(@NotNull Collection<SmartPsiElementPointer<PsiParameter>> unboundedParams) {
266     final ParameterClassMember[] result = new ParameterClassMember[unboundedParams.size()];
267     int i = 0;
268     for (SmartPsiElementPointer<PsiParameter> pointer : unboundedParams) {
269       result[i++] = new ParameterClassMember(pointer.getElement());
270     }
271     return result;
272   }
273
274   @NotNull
275   private static Collection<SmartPsiElementPointer<PsiParameter>> copyUnboundedParamsAndClearOriginal(@NotNull PsiMethod method) {
276     synchronized (LOCK) {
277       final Collection<SmartPsiElementPointer<PsiParameter>> unboundedParams = getUnboundedParams(method);
278       final Collection<SmartPsiElementPointer<PsiParameter>> result = new ArrayList<>(unboundedParams);
279       unboundedParams.clear();
280       return result;
281     }
282   }
283
284   private static void processParameter(final Project project,
285                                        final PsiParameter parameter,
286                                        final Set<String> usedNames) {
287     IdeDocumentHistory.getInstance(project).includeCurrentPlaceAsChangePlace();
288     final PsiType type = FieldFromParameterUtils.getSubstitutedType(parameter);
289     final JavaCodeStyleManager styleManager = JavaCodeStyleManager.getInstance(project);
290     final String parameterName = parameter.getName();
291     String propertyName = styleManager.variableNameToPropertyName(parameterName, VariableKind.PARAMETER);
292
293     final PsiClass targetClass = PsiTreeUtil.getParentOfType(parameter, PsiClass.class);
294     final PsiElement declarationScope = parameter.getDeclarationScope();
295     if (!(declarationScope instanceof PsiMethod)) return;
296     final PsiMethod method = (PsiMethod)declarationScope;
297
298     final boolean isMethodStatic = method.hasModifierProperty(PsiModifier.STATIC);
299
300     VariableKind kind = isMethodStatic ? VariableKind.STATIC_FIELD : VariableKind.FIELD;
301     SuggestedNameInfo suggestedNameInfo = styleManager.suggestVariableName(kind, propertyName, null, type);
302     String[] names = suggestedNameInfo.names;
303
304     final boolean isFinal = !isMethodStatic && method.isConstructor();
305     String name = names[0];
306     if (targetClass != null) {
307       for (String curName : names) {
308         if (!usedNames.contains(curName)) {
309           final PsiField fieldByName = targetClass.findFieldByName(curName, false);
310           if (fieldByName != null && (!method.isConstructor() || !isFieldAssigned(fieldByName, method)) && fieldByName.getType().isAssignableFrom(parameter.getType())) {
311             name = curName;
312             break;
313           }
314         }
315       }
316     }
317
318     if (usedNames.contains(name)) {
319       for (String curName : names) {
320         if (!usedNames.contains(curName)) {
321           name = curName;
322           break;
323         }
324       }
325     }
326     
327     final String fieldName = usedNames.add(name) ? name
328                                                  : JavaCodeStyleManager.getInstance(project).suggestUniqueVariableName(name, parameter, true);
329
330     ApplicationManager.getApplication().runWriteAction(() -> {
331       try {
332         FieldFromParameterUtils.createFieldAndAddAssignment(
333           project,
334           targetClass,
335           method,
336           parameter,
337           type,
338           fieldName,
339           isMethodStatic,
340           isFinal);
341       }
342       catch (IncorrectOperationException e) {
343         LOG.error(e);
344       }
345     });
346   }
347
348   private static boolean isFieldAssigned(PsiField field, PsiMethod method) {
349     for (PsiReference reference : ReferencesSearch.search(field, new LocalSearchScope(method))) {
350       if (reference instanceof PsiReferenceExpression && PsiUtil.isOnAssignmentLeftHand((PsiReferenceExpression)reference)) {
351         return true;
352       }
353     }
354     return false;
355   }
356
357   @Override
358   public boolean startInWriteAction() {
359     return false;
360   }
361 }