Merge remote-tracking branch 'origin/master' into develar/is
[idea/community.git] / python / src / com / jetbrains / python / inspections / PyChainedComparisonsInspection.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.jetbrains.python.inspections;
17
18 import com.intellij.codeInspection.LocalInspectionToolSession;
19 import com.intellij.codeInspection.LocalQuickFix;
20 import com.intellij.codeInspection.ProblemDescriptor;
21 import com.intellij.codeInspection.ProblemsHolder;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
24 import com.intellij.psi.PsiElementVisitor;
25 import com.intellij.psi.PsiFile;
26 import com.intellij.util.ui.CheckBox;
27 import com.jetbrains.python.PyBundle;
28 import com.jetbrains.python.PyTokenTypes;
29 import com.jetbrains.python.inspections.quickfix.ChainedComparisonsQuickFix;
30 import com.jetbrains.python.psi.PyBinaryExpression;
31 import com.jetbrains.python.psi.PyElementType;
32 import com.jetbrains.python.psi.PyExpression;
33 import com.jetbrains.python.psi.PyLiteralExpression;
34 import org.jetbrains.annotations.Nls;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import javax.swing.*;
39 import java.awt.*;
40
41 /**
42  * User: catherine
43  *
44  * Inspection to detect chained comparisons which can be simplified
45  * For instance, a < b and b < c  -->  a < b < c
46  */
47 public class PyChainedComparisonsInspection extends PyInspection {
48   private static String INSPECTION_SHORT_NAME = "PyChainedComparisonsInspection";
49   public boolean ignoreConstantInTheMiddle = false;
50   private static String ourIgnoreConstantOptionText = "Ignore statements with a constant in the middle";
51
52   @Nls
53   @NotNull
54   @Override
55   public String getDisplayName() {
56     return PyBundle.message("INSP.NAME.chained.comparisons");
57   }
58
59   @NotNull
60   @Override
61   public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
62                                         boolean isOnTheFly,
63                                         @NotNull LocalInspectionToolSession session) {
64     return new Visitor(holder, session, ignoreConstantInTheMiddle);
65   }
66
67   @Nullable
68   @Override
69   public JComponent createOptionsPanel() {
70     final JPanel rootPanel = new JPanel(new BorderLayout());
71     rootPanel.add(new CheckBox(ourIgnoreConstantOptionText, this, "ignoreConstantInTheMiddle"),
72                   BorderLayout.PAGE_START);
73
74     return rootPanel;
75   }
76
77   private static class Visitor extends PyInspectionVisitor {
78     /**
79      * @see ChainedComparisonsQuickFix#ChainedComparisonsQuickFix(boolean, boolean, boolean)
80      */
81     boolean myIsLeft;
82     boolean myIsRight;
83     PyElementType myOperator;
84     boolean getInnerRight;
85     boolean isConstantInTheMiddle;
86     boolean ignoreConstantInTheMiddle;
87
88     public Visitor(@Nullable ProblemsHolder holder, @NotNull LocalInspectionToolSession session, boolean ignoreConstantInTheMiddle) {
89       super(holder, session);
90       this.ignoreConstantInTheMiddle = ignoreConstantInTheMiddle;
91     }
92
93     @Override
94     public void visitPyBinaryExpression(final PyBinaryExpression node) {
95       myIsLeft = false;
96       myIsRight = false;
97       myOperator = null;
98       getInnerRight = false;
99
100       final PyExpression leftExpression = node.getLeftExpression();
101       final PyExpression rightExpression = node.getRightExpression();
102
103       if (leftExpression instanceof PyBinaryExpression &&
104           rightExpression instanceof PyBinaryExpression) {
105         if (node.getOperator() == PyTokenTypes.AND_KEYWORD) {
106           if (isRightSimplified((PyBinaryExpression)leftExpression, (PyBinaryExpression)rightExpression) ||
107               isLeftSimplified((PyBinaryExpression)leftExpression, (PyBinaryExpression)rightExpression)) {
108             if (isConstantInTheMiddle) {
109               if(!ignoreConstantInTheMiddle) {
110                 registerProblem(node, "Simplify chained comparison", new ChainedComparisonsQuickFix(myIsLeft, myIsRight, getInnerRight),
111                                 new DontSimplifyStatementsWithConstantInTheMiddleQuickFix());
112               }
113             }
114             else {
115               registerProblem(node, "Simplify chained comparison", new ChainedComparisonsQuickFix(myIsLeft, myIsRight, getInnerRight));
116             }
117           }
118         }
119       }
120     }
121
122     private boolean isRightSimplified(@NotNull final PyBinaryExpression leftExpression,
123                                       @NotNull final PyBinaryExpression rightExpression) {
124       final PyExpression leftRight = leftExpression.getRightExpression();
125       if (leftRight instanceof PyBinaryExpression && PyTokenTypes.AND_KEYWORD == leftExpression.getOperator()) {
126         if (isRightSimplified((PyBinaryExpression)leftRight, rightExpression)) {
127           getInnerRight = true;
128           return true;
129         }
130       }
131
132       if (leftRight instanceof PyBinaryExpression &&
133           PyTokenTypes.RELATIONAL_OPERATIONS.contains(((PyBinaryExpression)leftRight).getOperator())) {
134         if (isRightSimplified((PyBinaryExpression)leftRight, rightExpression)) {
135           return true;
136         }
137       }
138
139       myOperator = leftExpression.getOperator();
140       if (PyTokenTypes.RELATIONAL_OPERATIONS.contains(myOperator)) {
141         if (leftRight != null) {
142           if (leftRight.getText().equals(getLeftExpression(rightExpression, true).getText())) {
143             myIsLeft = false;
144             myIsRight = true;
145             isConstantInTheMiddle = leftRight instanceof PyLiteralExpression;
146             return true;
147           }
148
149           final PyExpression right = getSmallestRight(rightExpression, true);
150           if (right != null && leftRight.getText().equals(right.getText())) {
151             myIsLeft = false;
152             myIsRight = false;
153             isConstantInTheMiddle = leftRight instanceof PyLiteralExpression;
154             return true;
155           }
156         }
157       }
158       return false;
159     }
160
161     private static boolean isOpposite(final PyElementType op1, final PyElementType op2) {
162       if ((op1 == PyTokenTypes.GT || op1 == PyTokenTypes.GE) && (op2 == PyTokenTypes.LT || op2 == PyTokenTypes.LE)) {
163         return true;
164       }
165       if ((op2 == PyTokenTypes.GT || op2 == PyTokenTypes.GE) && (op1 == PyTokenTypes.LT || op1 == PyTokenTypes.LE)) {
166         return true;
167       }
168       return false;
169     }
170
171
172     private boolean isLeftSimplified(PyBinaryExpression leftExpression, PyBinaryExpression rightExpression) {
173       final PyExpression leftLeft = leftExpression.getLeftExpression();
174       final PyExpression leftRight = leftExpression.getRightExpression();
175       if (leftRight instanceof PyBinaryExpression
176           && PyTokenTypes.AND_KEYWORD == leftExpression.getOperator()) {
177         if (isLeftSimplified((PyBinaryExpression)leftRight, rightExpression)) {
178           getInnerRight = true;
179           return true;
180         }
181       }
182
183       if (leftLeft instanceof PyBinaryExpression &&
184           PyTokenTypes.RELATIONAL_OPERATIONS.contains(((PyBinaryExpression)leftLeft).getOperator())) {
185         if (isLeftSimplified((PyBinaryExpression)leftLeft, rightExpression)) {
186           return true;
187         }
188       }
189
190       myOperator = leftExpression.getOperator();
191       if (PyTokenTypes.RELATIONAL_OPERATIONS.contains(myOperator)) {
192         if (leftLeft != null) {
193           if (leftLeft.getText().equals(getLeftExpression(rightExpression, false).getText())) {
194             myIsLeft = true;
195             myIsRight = true;
196             isConstantInTheMiddle = leftLeft instanceof PyLiteralExpression;
197             return true;
198           }
199           final PyExpression right = getSmallestRight(rightExpression, false);
200           if (right != null && leftLeft.getText().equals(right.getText())) {
201             myIsLeft = true;
202             myIsRight = false;
203             isConstantInTheMiddle = leftLeft instanceof PyLiteralExpression;
204             return true;
205           }
206         }
207       }
208       return false;
209     }
210
211     private PyExpression getLeftExpression(PyBinaryExpression expression, boolean isRight) {
212       PyExpression result = expression;
213       while (result instanceof PyBinaryExpression &&
214              (PyTokenTypes.RELATIONAL_OPERATIONS.contains(((PyBinaryExpression)result).getOperator()) ||
215               PyTokenTypes.EQUALITY_OPERATIONS.contains(((PyBinaryExpression)result).getOperator()))) {
216
217         final boolean opposite = isOpposite(((PyBinaryExpression)result).getOperator(), myOperator);
218         if ((isRight && opposite) || (!isRight && !opposite)) {
219           break;
220         }
221         result = ((PyBinaryExpression)result).getLeftExpression();
222       }
223       return result;
224     }
225
226     @Nullable
227     private PyExpression getSmallestRight(PyBinaryExpression expression, boolean isRight) {
228       PyExpression result = expression;
229       while (result instanceof PyBinaryExpression &&
230              (PyTokenTypes.RELATIONAL_OPERATIONS.contains(((PyBinaryExpression)result).getOperator()) ||
231               PyTokenTypes.EQUALITY_OPERATIONS.contains(((PyBinaryExpression)result).getOperator()))) {
232
233         final boolean opposite = isOpposite(((PyBinaryExpression)result).getOperator(), myOperator);
234         if ((isRight && !opposite) || (!isRight && opposite)) {
235           break;
236         }
237         result = ((PyBinaryExpression)result).getRightExpression();
238       }
239       return result;
240     }
241   }
242
243   private static class DontSimplifyStatementsWithConstantInTheMiddleQuickFix implements LocalQuickFix {
244
245     @Nls
246     @NotNull
247     @Override
248     public String getName() {
249       return ourIgnoreConstantOptionText;
250     }
251
252     @Nls
253     @NotNull
254     @Override
255     public String getFamilyName() {
256       return ourIgnoreConstantOptionText;
257     }
258
259     @Override
260     public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
261       final PsiFile file = descriptor.getStartElement().getContainingFile();
262       InspectionProjectProfileManager.getInstance(project).getInspectionProfile().modifyProfile(model -> {
263         final PyChainedComparisonsInspection tool = (PyChainedComparisonsInspection)model.getUnwrappedTool(INSPECTION_SHORT_NAME, file);
264         tool.ignoreConstantInTheMiddle = true;
265       });
266     }
267   }
268 }