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