Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
EqualsAvoidNullCheck |
|
| 3.875;3.875 |
1 | //////////////////////////////////////////////////////////////////////////////// | |
2 | // checkstyle: Checks Java source code for adherence to a set of rules. | |
3 | // Copyright (C) 2001-2014 Oliver Burn | |
4 | // | |
5 | // This library is free software; you can redistribute it and/or | |
6 | // modify it under the terms of the GNU Lesser General Public | |
7 | // License as published by the Free Software Foundation; either | |
8 | // version 2.1 of the License, or (at your option) any later version. | |
9 | // | |
10 | // This library is distributed in the hope that it will be useful, | |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
13 | // Lesser General Public License for more details. | |
14 | // | |
15 | // You should have received a copy of the GNU Lesser General Public | |
16 | // License along with this library; if not, write to the Free Software | |
17 | // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
18 | //////////////////////////////////////////////////////////////////////////////// | |
19 | ||
20 | package com.puppycrawl.tools.checkstyle.checks.coding; | |
21 | ||
22 | import antlr.collections.AST; | |
23 | import com.puppycrawl.tools.checkstyle.api.Check; | |
24 | import com.puppycrawl.tools.checkstyle.api.DetailAST; | |
25 | import com.puppycrawl.tools.checkstyle.api.TokenTypes; | |
26 | ||
27 | /** | |
28 | * <p> | |
29 | * Checks that any combination of String literals with optional | |
30 | * assignment is on the left side of an equals() comparison. | |
31 | * </p> | |
32 | * | |
33 | * <p> | |
34 | * Rationale: Calling the equals() method on String literals | |
35 | * will avoid a potential NullPointerException. Also, it is | |
36 | * pretty common to see null check right before equals comparisons | |
37 | * which is not necessary in the below example. | |
38 | * | |
39 | * For example: | |
40 | * | |
41 | * <pre> | |
42 | * <code> | |
43 | * String nullString = null; | |
44 | * nullString.equals("My_Sweet_String"); | |
45 | * </code> | |
46 | * </pre> | |
47 | * should be refactored to | |
48 | * | |
49 | * <pre> | |
50 | * <code> | |
51 | * String nullString = null; | |
52 | * "My_Sweet_String".equals(nullString); | |
53 | * </code> | |
54 | * </pre> | |
55 | * </p> | |
56 | * | |
57 | * <p> | |
58 | * Limitations: If the equals method is overridden or | |
59 | * a covariant equals method is defined and the implementation | |
60 | * is incorrect (where s.equals(t) does not return the same result | |
61 | * as t.equals(s)) then rearranging the called on object and | |
62 | * parameter may have unexpected results | |
63 | * | |
64 | * <br/> | |
65 | * | |
66 | * Java's Autoboxing feature has an affect | |
67 | * on how this check is implemented. Pre Java 5 all IDENT + IDENT | |
68 | * object concatenations would not cause a NullPointerException even | |
69 | * if null. Those situations could have been included in this check. | |
70 | * They would simply act as if they surrounded by String.valueOf() | |
71 | * which would concatenate the String null. | |
72 | * </p> | |
73 | * <p> | |
74 | * The following example will cause a | |
75 | * NullPointerException as a result of what autoboxing does. | |
76 | * <pre> | |
77 | * Integer i = null, j = null; | |
78 | * String number = "5" | |
79 | * number.equals(i + j); | |
80 | * </pre> | |
81 | * </p> | |
82 | * | |
83 | * Since, it is difficult to determine what kind of Object is being | |
84 | * concatenated all ident concatenation is considered unsafe. | |
85 | * | |
86 | * @author Travis Schneeberger | |
87 | * version 1.0 | |
88 | */ | |
89 | 2 | public class EqualsAvoidNullCheck extends Check |
90 | { | |
91 | /** Whether to process equalsIgnoreCase() invocations. */ | |
92 | private boolean mIgnoreEqualsIgnoreCase; | |
93 | ||
94 | @Override | |
95 | public int[] getDefaultTokens() | |
96 | { | |
97 | 2 | return new int[] {TokenTypes.METHOD_CALL}; |
98 | } | |
99 | ||
100 | @Override | |
101 | public void visitToken(final DetailAST aMethodCall) | |
102 | { | |
103 | 132 | final DetailAST dot = aMethodCall.getFirstChild(); |
104 | 132 | if (dot.getType() != TokenTypes.DOT) { |
105 | 12 | return; |
106 | } | |
107 | ||
108 | 120 | final DetailAST objCalledOn = dot.getFirstChild(); |
109 | ||
110 | //checks for calling equals on String literal and | |
111 | //anon object which cannot be null | |
112 | //Also, checks if calling using strange inner class | |
113 | //syntax outter.inner.equals(otherObj) by looking | |
114 | //for the dot operator which cannot be improved | |
115 | 120 | if ((objCalledOn.getType() == TokenTypes.STRING_LITERAL) |
116 | || (objCalledOn.getType() == TokenTypes.LITERAL_NEW) | |
117 | || (objCalledOn.getType() == TokenTypes.DOT)) | |
118 | { | |
119 | 20 | return; |
120 | } | |
121 | ||
122 | 100 | final DetailAST method = objCalledOn.getNextSibling(); |
123 | 100 | final DetailAST expr = dot.getNextSibling().getFirstChild(); |
124 | ||
125 | 100 | if ("equals".equals(method.getText()) |
126 | && containsOneArg(expr) && containsAllSafeTokens(expr)) | |
127 | { | |
128 | 24 | log(aMethodCall.getLineNo(), aMethodCall.getColumnNo(), |
129 | "equals.avoid.null"); | |
130 | } | |
131 | ||
132 | 100 | if (!mIgnoreEqualsIgnoreCase |
133 | && "equalsIgnoreCase".equals(method.getText()) | |
134 | && containsOneArg(expr) && containsAllSafeTokens(expr)) | |
135 | { | |
136 | 12 | log(aMethodCall.getLineNo(), aMethodCall.getColumnNo(), |
137 | "equalsIgnoreCase.avoid.null"); | |
138 | } | |
139 | 100 | } |
140 | ||
141 | /** | |
142 | * Checks if a method contains no arguments | |
143 | * starting at with the argument expression. | |
144 | * | |
145 | * @param aExpr the argument expression | |
146 | * @return true if the method contains no args, false if not | |
147 | */ | |
148 | private boolean containsNoArgs(final AST aExpr) | |
149 | { | |
150 | 58 | return (aExpr == null); |
151 | } | |
152 | ||
153 | /** | |
154 | * Checks if a method contains multiple arguments | |
155 | * starting at with the argument expression. | |
156 | * | |
157 | * @param aExpr the argument expression | |
158 | * @return true if the method contains multiple args, false if not | |
159 | */ | |
160 | private boolean containsMultiArgs(final AST aExpr) | |
161 | { | |
162 | 58 | final AST comma = aExpr.getNextSibling(); |
163 | 58 | return (comma != null) && (comma.getType() == TokenTypes.COMMA); |
164 | } | |
165 | ||
166 | /** | |
167 | * Checks if a method contains a single argument | |
168 | * starting at with the argument expression. | |
169 | * | |
170 | * @param aExpr the argument expression | |
171 | * @return true if the method contains a single arg, false if not | |
172 | */ | |
173 | private boolean containsOneArg(final AST aExpr) | |
174 | { | |
175 | 58 | return !containsNoArgs(aExpr) && !containsMultiArgs(aExpr); |
176 | } | |
177 | ||
178 | /** | |
179 | * <p> | |
180 | * Looks for all "safe" Token combinations in the argument | |
181 | * expression branch. | |
182 | * </p> | |
183 | * | |
184 | * <p> | |
185 | * See class documentation for details on autoboxing's affect | |
186 | * on this method implementation. | |
187 | * </p> | |
188 | * | |
189 | * @param aExpr the argument expression | |
190 | * @return - true if any child matches the set of tokens, false if not | |
191 | */ | |
192 | private boolean containsAllSafeTokens(final DetailAST aExpr) | |
193 | { | |
194 | 58 | DetailAST arg = aExpr.getFirstChild(); |
195 | ||
196 | 58 | if (arg.branchContains(TokenTypes.METHOD_CALL)) { |
197 | 4 | return false; |
198 | } | |
199 | 54 | arg = skipVariableAssign(arg); |
200 | ||
201 | //Plus assignment can have ill affects | |
202 | //do not want to recommend moving expression | |
203 | //See example: | |
204 | //String s = "SweetString"; | |
205 | //s.equals(s += "SweetString"); //false | |
206 | //s = "SweetString"; | |
207 | //(s += "SweetString").equals(s); //true | |
208 | //arg = skipVariablePlusAssign(arg); | |
209 | ||
210 | 54 | if ((arg).branchContains(TokenTypes.PLUS_ASSIGN) |
211 | || (arg).branchContains(TokenTypes.IDENT)) | |
212 | { | |
213 | 18 | return false; |
214 | } | |
215 | ||
216 | //must be just String literals if got here | |
217 | 36 | return true; |
218 | } | |
219 | ||
220 | /** | |
221 | * Skips over an inner assign portion of an argument expression. | |
222 | * @param aCurrentAST current token in the argument expression | |
223 | * @return the next relevant token | |
224 | */ | |
225 | private DetailAST skipVariableAssign(final DetailAST aCurrentAST) | |
226 | { | |
227 | 54 | if ((aCurrentAST.getType() == TokenTypes.ASSIGN) |
228 | && (aCurrentAST.getFirstChild().getType() == TokenTypes.IDENT)) | |
229 | { | |
230 | 6 | return aCurrentAST.getFirstChild().getNextSibling(); |
231 | } | |
232 | 48 | return aCurrentAST; |
233 | } | |
234 | ||
235 | /** | |
236 | * Whether to ignore checking {@code String.equalsIgnoreCase(String)}. | |
237 | * @param aNewValue whether to ignore checking | |
238 | * {@code String.equalsIgnoreCase(String)}. | |
239 | */ | |
240 | public void setIgnoreEqualsIgnoreCase(boolean aNewValue) | |
241 | { | |
242 | 1 | mIgnoreEqualsIgnoreCase = aNewValue; |
243 | 1 | } |
244 | } |