EditorConfig documentation test
[idea/community.git] / java / java-impl / src / com / intellij / codeInspection / suspiciousNameCombination / SuspiciousNameCombinationInspection.java
1 /*
2  * Copyright 2000-2013 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
17 package com.intellij.codeInspection.suspiciousNameCombination;
18
19 import com.intellij.codeInsight.daemon.GroupNames;
20 import com.intellij.codeInsight.daemon.JavaErrorMessages;
21 import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
22 import com.intellij.codeInspection.InspectionsBundle;
23 import com.intellij.codeInspection.ProblemsHolder;
24 import com.intellij.codeInspection.ui.ListTable;
25 import com.intellij.codeInspection.ui.ListWrappingTableModel;
26 import com.intellij.openapi.ui.Messages;
27 import com.intellij.openapi.util.InvalidDataException;
28 import com.intellij.openapi.util.WriteExternalException;
29 import com.intellij.openapi.util.text.StringUtil;
30 import com.intellij.psi.*;
31 import com.intellij.psi.codeStyle.NameUtil;
32 import com.intellij.psi.util.PsiTreeUtil;
33 import com.intellij.ui.AddEditDeleteListPanel;
34 import com.intellij.ui.IdeBorderFactory;
35 import com.siyeh.InspectionGadgetsBundle;
36 import com.siyeh.ig.psiutils.MethodMatcher;
37 import com.siyeh.ig.ui.UiUtils;
38 import org.jdom.Element;
39 import org.jetbrains.annotations.NonNls;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import javax.swing.*;
44 import javax.swing.event.ListDataEvent;
45 import javax.swing.event.ListDataListener;
46 import java.awt.*;
47 import java.util.List;
48 import java.util.*;
49
50 /**
51  * @author yole
52  */
53 public class SuspiciousNameCombinationInspection extends AbstractBaseJavaLocalInspectionTool {
54   @NonNls private static final String ELEMENT_GROUPS = "group";
55   @NonNls private static final String ATTRIBUTE_NAMES = "names";
56   @NonNls private static final String ELEMENT_IGNORED_METHODS = "ignored";
57   protected final List<String> myNameGroups = new ArrayList<>();
58   final MethodMatcher myIgnoredMethods = new MethodMatcher()
59     // parameter name is 'x' which is completely unrelated to coordinates
60     .add("java.io.PrintStream", "println")
61     .add("java.io.PrintWriter", "println")
62     .add("java.lang.System", "identityHashCode")
63     .add("java.sql.PreparedStatement", "set.*")
64     .add("java.sql.ResultSet", "update.*")
65     .add("java.sql.SQLOutput", "write.*")
66     // parameters for compare methods are x and y which is also unrelated to coordinates
67     .add("java.lang.Integer", "compare.*")
68     .add("java.lang.Long", "compare.*")
69     .add("java.lang.Short", "compare")
70     .add("java.lang.Byte", "compare")
71     .add("java.lang.Character", "compare")
72     .add("java.lang.Boolean", "compare")
73     // parameter names for addExact, multiplyFull, floorDiv, hypot etc. are x and y,
74     // but either unlikely to be related to coordinates or their order does not matter (like in hypot)
75     .add("java.lang.Math", ".*")
76     .add("java.lang.StrictMath", ".*");
77   private final Map<String, String> myWordToGroupMap = new HashMap<>();
78   private int myLongestWord = 0;
79
80   public SuspiciousNameCombinationInspection() {
81     addNameGroup("x,width,left,right");
82     addNameGroup("y,height,top,bottom");
83   }
84
85   @Override @Nullable
86   public JComponent createOptionsPanel() {
87     NameGroupsPanel nameGroupsPanel = new NameGroupsPanel();
88     ListTable table = new ListTable(new ListWrappingTableModel(
89       Arrays.asList(myIgnoredMethods.getClassNames(), myIgnoredMethods.getMethodNamePatterns()),
90       InspectionGadgetsBundle.message("result.of.method.call.ignored.class.column.title"),
91       InspectionGadgetsBundle.message("result.of.method.call.ignored.method.column.title")));
92     JPanel tablePanel = UiUtils.createAddRemoveTreeClassChooserPanel(table, InspectionGadgetsBundle.message("choose.class"));
93     JPanel panel = new JPanel(new GridLayout(2, 1));
94     panel.add(nameGroupsPanel);
95     tablePanel.setBorder(IdeBorderFactory.createTitledBorder("Ignore methods", false));
96     panel.add(tablePanel);
97     return panel;
98   }
99
100   @Override
101   public boolean isEnabledByDefault() {
102     return true;
103   }
104
105   protected void clearNameGroups() {
106     myNameGroups.clear();
107     myWordToGroupMap.clear();
108     myLongestWord = 0;
109   }
110
111   public void addNameGroup(@NonNls final String group) {
112     myNameGroups.add(group);
113     List<String> words = StringUtil.split(group, ",");
114     for(String word: words) {
115       String canonicalized = canonicalize(word);
116       myLongestWord = Math.max(myLongestWord, canonicalized.length());
117       myWordToGroupMap.put(canonicalized, group);
118     }
119   }
120
121   @Override
122   @NotNull
123   public String getGroupDisplayName() {
124     return GroupNames.BUGS_GROUP_NAME;
125   }
126
127   @Override
128   @NotNull
129   public String getDisplayName() {
130     return InspectionsBundle.message("suspicious.name.combination.display.name");
131   }
132
133   @Override
134   @NotNull
135   @NonNls
136   public String getShortName() {
137     return "SuspiciousNameCombination";
138   }
139
140   @Override
141   @NotNull
142   public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
143     return new MyVisitor(holder);
144   }
145
146   @Override public void readSettings(@NotNull Element node) throws InvalidDataException {
147     clearNameGroups();
148     for(Object o: node.getChildren(ELEMENT_GROUPS)) {
149       Element e = (Element) o;
150       addNameGroup(e.getAttributeValue(ATTRIBUTE_NAMES));
151     }
152     Element ignoredMethods = node.getChild(ELEMENT_IGNORED_METHODS);
153     if (ignoredMethods != null) {
154       myIgnoredMethods.readSettings(ignoredMethods);
155     }
156   }
157
158   @Override public void writeSettings(@NotNull Element node) throws WriteExternalException {
159     for(String group: myNameGroups) {
160       Element e = new Element(ELEMENT_GROUPS);
161       node.addContent(e);
162       e.setAttribute(ATTRIBUTE_NAMES, group);
163     }
164     Element ignoredMethods = new Element(ELEMENT_IGNORED_METHODS);
165     node.addContent(ignoredMethods);
166     myIgnoredMethods.writeSettings(ignoredMethods);
167   }
168
169   @NotNull
170   private static String canonicalize(String word) {
171     return StringUtil.toLowerCase(word.trim());
172   }
173
174   private class NameGroupsPanel extends AddEditDeleteListPanel<String> {
175
176     NameGroupsPanel() {
177       super(InspectionsBundle.message("suspicious.name.combination.options.title"), myNameGroups);
178       myListModel.addListDataListener(new ListDataListener() {
179         @Override
180         public void intervalAdded(ListDataEvent e) {
181           saveChanges();
182         }
183
184         @Override
185         public void intervalRemoved(ListDataEvent e) {
186           saveChanges();
187         }
188
189         @Override
190         public void contentsChanged(ListDataEvent e) {
191           saveChanges();
192         }
193       });
194     }
195
196     @Override
197     protected String findItemToAdd() {
198       return Messages.showInputDialog(this,
199                                       InspectionsBundle.message("suspicious.name.combination.options.prompt"),
200                                       InspectionsBundle.message("suspicious.name.combination.add.titile"),
201                                       Messages.getQuestionIcon(), "", null);
202     }
203
204     @Override
205     protected String editSelectedItem(String inputValue) {
206       return Messages.showInputDialog(this,
207                                       InspectionsBundle.message("suspicious.name.combination.options.prompt"),
208                                       InspectionsBundle.message("suspicious.name.combination.edit.title"),
209                                       Messages.getQuestionIcon(),
210                                       inputValue, null);
211     }
212
213     private void saveChanges() {
214       clearNameGroups();
215       for(int i=0; i<myListModel.getSize(); i++) {
216         addNameGroup(myListModel.getElementAt(i));
217       }
218     }
219   }
220
221   private class MyVisitor extends JavaElementVisitor {
222     private final ProblemsHolder myProblemsHolder;
223
224     MyVisitor(final ProblemsHolder problemsHolder) {
225       myProblemsHolder = problemsHolder;
226     }
227     @Override public void visitVariable(PsiVariable variable) {
228       if (variable.hasInitializer()) {
229         PsiExpression expr = variable.getInitializer();
230         if (expr instanceof PsiReferenceExpression) {
231           PsiReferenceExpression refExpr = (PsiReferenceExpression) expr;
232           PsiIdentifier nameIdentifier = variable.getNameIdentifier();
233           checkCombination(nameIdentifier != null ? nameIdentifier : variable, variable.getName(), refExpr.getReferenceName(), "suspicious.name.assignment");
234         }
235       }
236     }
237
238     @Override public void visitAssignmentExpression(PsiAssignmentExpression expression) {
239       PsiExpression lhs = expression.getLExpression();
240       PsiExpression rhs = expression.getRExpression();
241       if (lhs instanceof PsiReferenceExpression && rhs instanceof PsiReferenceExpression) {
242         PsiReferenceExpression lhsExpr = (PsiReferenceExpression) lhs;
243         PsiReferenceExpression rhsExpr = (PsiReferenceExpression) rhs;
244         checkCombination(lhsExpr, lhsExpr.getReferenceName(), rhsExpr.getReferenceName(), "suspicious.name.assignment");
245       }
246     }
247
248     @Override public void visitCallExpression(PsiCallExpression expression) {
249       final PsiMethod psiMethod = expression.resolveMethod();
250       if (myIgnoredMethods.matches(psiMethod)) return;
251       final PsiExpressionList argList = expression.getArgumentList();
252       if (psiMethod != null && argList != null) {
253         final PsiExpression[] args = argList.getExpressions();
254         final PsiParameter[] parameters = psiMethod.getParameterList().getParameters();
255         for(int i=0; i<parameters.length; i++) {
256           if (i >= args.length) break;
257           if (args [i] instanceof PsiReferenceExpression) {
258             // PsiParameter.getName() can be expensive for compiled class files, so check reference name before
259             // fetching parameter name
260             final String refName = ((PsiReferenceExpression)args[i]).getReferenceName();
261             if (findNameGroup(refName) != null) {
262               checkCombination(args [i], parameters [i].getName(), refName, "suspicious.name.parameter");
263             }
264           }
265         }
266       }
267     }
268
269     @Override
270     public void visitReturnStatement(final PsiReturnStatement statement) {
271       final PsiExpression returnValue = statement.getReturnValue();
272       PsiMethod containingMethod = PsiTreeUtil.getParentOfType(returnValue, PsiMethod.class, true, PsiLambdaExpression.class);
273       if (returnValue instanceof PsiReferenceExpression && containingMethod != null) {
274         final String refName = ((PsiReferenceExpression)returnValue).getReferenceName();
275         checkCombination(returnValue, containingMethod.getName(), refName, "suspicious.name.return");
276       }
277     }
278
279     private void checkCombination(final PsiElement location,
280                                   @Nullable final String name,
281                                   @Nullable final String referenceName,
282                                   final String key) {
283       String nameGroup1 = findNameGroup(name);
284       String nameGroup2 = findNameGroup(referenceName);
285       if (nameGroup1 != null && nameGroup2 != null && !nameGroup1.equals(nameGroup2)) {
286         myProblemsHolder.registerProblem(location, JavaErrorMessages.message(key, referenceName, name));
287       }
288     }
289
290     @Nullable private String findNameGroup(@Nullable final String name) {
291       if (name == null) {
292         return null;
293       }
294       String[] words = NameUtil.splitNameIntoWords(name);
295       Arrays.asList(words).replaceAll(SuspiciousNameCombinationInspection::canonicalize);
296       String result = null;
297       for (int i = 0; i < words.length; i++) {
298         String word = "";
299         for (int j = i; j < words.length; j++) {
300           word += words[j];
301           if (word.length() > myLongestWord) break;
302           String group = myWordToGroupMap.get(word);
303           if (group != null) {
304             if (result == null) {
305               result = group;
306             }
307             else if (!result.equals(group)) {
308               return null;
309             }
310           }
311         }
312       }
313       return result;
314     }
315   }
316 }