Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
FallThroughCheck |
|
| 4.133333333333334;4.133 |
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 | package com.puppycrawl.tools.checkstyle.checks.coding; | |
20 | ||
21 | import java.util.regex.Matcher; | |
22 | import java.util.regex.Pattern; | |
23 | ||
24 | import com.puppycrawl.tools.checkstyle.api.Check; | |
25 | import com.puppycrawl.tools.checkstyle.api.DetailAST; | |
26 | import com.puppycrawl.tools.checkstyle.api.TokenTypes; | |
27 | import com.puppycrawl.tools.checkstyle.api.Utils; | |
28 | ||
29 | /** | |
30 | * Checks for fall through in switch statements | |
31 | * Finds locations where a case contains Java code - | |
32 | * but lacks a break, return, throw or continue statement. | |
33 | * | |
34 | * <p> | |
35 | * The check honors special comments to suppress warnings about | |
36 | * the fall through. By default the comments "fallthru", | |
37 | * "fall through", "falls through" and "fallthrough" are recognized. | |
38 | * </p> | |
39 | * <p> | |
40 | * The following fragment of code will NOT trigger the check, | |
41 | * because of the comment "fallthru". | |
42 | * </p> | |
43 | * <pre> | |
44 | * case 3: | |
45 | * x = 2; | |
46 | * // fallthru | |
47 | * case 4: | |
48 | * </pre> | |
49 | * <p> | |
50 | * The recognized relief comment can be configured with the property | |
51 | * <code>reliefPattern</code>. Default value of this regular expression | |
52 | * is "fallthru|fall through|fallthrough|falls through". | |
53 | * </p> | |
54 | * <p> | |
55 | * An example of how to configure the check is: | |
56 | * </p> | |
57 | * <pre> | |
58 | * <module name="FallThrough"> | |
59 | * <property name="reliefPattern" | |
60 | * value="Fall Through"/> | |
61 | * </module> | |
62 | * </pre> | |
63 | * | |
64 | * @author o_sukhodolsky | |
65 | */ | |
66 | public class FallThroughCheck extends Check | |
67 | { | |
68 | /** Do we need to check last case group. */ | |
69 | private boolean mCheckLastGroup; | |
70 | ||
71 | /** Relief pattern to allow fall throught to the next case branch. */ | |
72 | 3 | private String mReliefPattern = "fallthru|falls? ?through"; |
73 | ||
74 | /** Relief regexp. */ | |
75 | private Pattern mRegExp; | |
76 | ||
77 | /** Creates new instance of the check. */ | |
78 | public FallThroughCheck() | |
79 | 3 | { |
80 | // do nothing | |
81 | 3 | } |
82 | ||
83 | @Override | |
84 | public int[] getDefaultTokens() | |
85 | { | |
86 | 3 | return new int[]{TokenTypes.CASE_GROUP}; |
87 | } | |
88 | ||
89 | @Override | |
90 | public int[] getRequiredTokens() | |
91 | { | |
92 | 0 | return getDefaultTokens(); |
93 | } | |
94 | ||
95 | /** | |
96 | * Set the relief pattern. | |
97 | * | |
98 | * @param aPattern | |
99 | * The regular expression pattern. | |
100 | */ | |
101 | public void setReliefPattern(String aPattern) | |
102 | { | |
103 | 1 | mReliefPattern = aPattern; |
104 | 1 | } |
105 | ||
106 | /** | |
107 | * Configures whether we need to check last case group or not. | |
108 | * @param aValue new value of the property. | |
109 | */ | |
110 | public void setCheckLastCaseGroup(boolean aValue) | |
111 | { | |
112 | 1 | mCheckLastGroup = aValue; |
113 | 1 | } |
114 | ||
115 | @Override | |
116 | public void init() | |
117 | { | |
118 | 3 | super.init(); |
119 | 3 | mRegExp = Utils.getPattern(mReliefPattern); |
120 | 3 | } |
121 | ||
122 | @Override | |
123 | public void visitToken(DetailAST aAST) | |
124 | { | |
125 | 285 | final DetailAST nextGroup = aAST.getNextSibling(); |
126 | 285 | final boolean isLastGroup = |
127 | ((nextGroup == null) | |
128 | || (nextGroup.getType() != TokenTypes.CASE_GROUP)); | |
129 | 285 | if (isLastGroup && !mCheckLastGroup) { |
130 | // we do not need to check last group | |
131 | 30 | return; |
132 | } | |
133 | ||
134 | 255 | final DetailAST slist = aAST.findFirstToken(TokenTypes.SLIST); |
135 | ||
136 | 255 | if (!isTerminated(slist, true, true) |
137 | && !hasFallTruComment(aAST, nextGroup)) | |
138 | { | |
139 | 50 | if (!isLastGroup) { |
140 | 48 | log(nextGroup, "fall.through"); |
141 | } | |
142 | else { | |
143 | 2 | log(aAST, "fall.through.last"); |
144 | } | |
145 | } | |
146 | 255 | } |
147 | ||
148 | /** | |
149 | * Checks if a given subtree terminated by return, throw or, | |
150 | * if allowed break, continue. | |
151 | * @param aAST root of given subtree | |
152 | * @param aUseBreak should we consider break as terminator. | |
153 | * @param aUseContinue should we consider continue as terminator. | |
154 | * @return true if the subtree is terminated. | |
155 | */ | |
156 | private boolean isTerminated(final DetailAST aAST, boolean aUseBreak, | |
157 | boolean aUseContinue) | |
158 | { | |
159 | 764 | switch (aAST.getType()) { |
160 | case TokenTypes.LITERAL_RETURN: | |
161 | case TokenTypes.LITERAL_THROW: | |
162 | 82 | return true; |
163 | case TokenTypes.LITERAL_BREAK: | |
164 | 93 | return aUseBreak; |
165 | case TokenTypes.LITERAL_CONTINUE: | |
166 | 36 | return aUseContinue; |
167 | case TokenTypes.SLIST: | |
168 | 396 | return checkSlist(aAST, aUseBreak, aUseContinue); |
169 | case TokenTypes.LITERAL_IF: | |
170 | 18 | return checkIf(aAST, aUseBreak, aUseContinue); |
171 | case TokenTypes.LITERAL_FOR: | |
172 | case TokenTypes.LITERAL_WHILE: | |
173 | case TokenTypes.LITERAL_DO: | |
174 | 24 | return checkLoop(aAST); |
175 | case TokenTypes.LITERAL_TRY: | |
176 | 24 | return checkTry(aAST, aUseBreak, aUseContinue); |
177 | case TokenTypes.LITERAL_SWITCH: | |
178 | 12 | return checkSwitch(aAST, aUseContinue); |
179 | default: | |
180 | 79 | return false; |
181 | } | |
182 | } | |
183 | ||
184 | /** | |
185 | * Checks if a given SLIST terminated by return, throw or, | |
186 | * if allowed break, continue. | |
187 | * @param aAST SLIST to check | |
188 | * @param aUseBreak should we consider break as terminator. | |
189 | * @param aUseContinue should we consider continue as terminator. | |
190 | * @return true if SLIST is terminated. | |
191 | */ | |
192 | private boolean checkSlist(final DetailAST aAST, boolean aUseBreak, | |
193 | boolean aUseContinue) | |
194 | { | |
195 | 396 | DetailAST lastStmt = aAST.getLastChild(); |
196 | 396 | if (lastStmt == null) { |
197 | // if last case in switch is empty then slist is empty | |
198 | // since this is a last case it is not a fall-through | |
199 | 1 | return true; |
200 | } | |
201 | ||
202 | 395 | if (lastStmt.getType() == TokenTypes.RCURLY) { |
203 | 111 | lastStmt = lastStmt.getPreviousSibling(); |
204 | } | |
205 | ||
206 | 395 | return (lastStmt != null) |
207 | && isTerminated(lastStmt, aUseBreak, aUseContinue); | |
208 | } | |
209 | ||
210 | /** | |
211 | * Checks if a given IF terminated by return, throw or, | |
212 | * if allowed break, continue. | |
213 | * @param aAST IF to check | |
214 | * @param aUseBreak should we consider break as terminator. | |
215 | * @param aUseContinue should we consider continue as terminator. | |
216 | * @return true if IF is terminated. | |
217 | */ | |
218 | private boolean checkIf(final DetailAST aAST, boolean aUseBreak, | |
219 | boolean aUseContinue) | |
220 | { | |
221 | 18 | final DetailAST thenStmt = aAST.findFirstToken(TokenTypes.RPAREN) |
222 | .getNextSibling(); | |
223 | 18 | final DetailAST elseStmt = thenStmt.getNextSibling(); |
224 | 18 | boolean isTerminated = isTerminated(thenStmt, aUseBreak, aUseContinue); |
225 | ||
226 | 18 | if (isTerminated && (elseStmt != null)) { |
227 | 12 | isTerminated = isTerminated(elseStmt.getFirstChild(), |
228 | aUseBreak, aUseContinue); | |
229 | } | |
230 | 18 | return isTerminated; |
231 | } | |
232 | ||
233 | /** | |
234 | * Checks if a given loop terminated by return, throw or, | |
235 | * if allowed break, continue. | |
236 | * @param aAST loop to check | |
237 | * @return true if loop is terminated. | |
238 | */ | |
239 | private boolean checkLoop(final DetailAST aAST) | |
240 | { | |
241 | 24 | DetailAST loopBody = null; |
242 | 24 | if (aAST.getType() == TokenTypes.LITERAL_DO) { |
243 | 6 | final DetailAST lparen = aAST.findFirstToken(TokenTypes.DO_WHILE); |
244 | 6 | loopBody = lparen.getPreviousSibling(); |
245 | 6 | } |
246 | else { | |
247 | 18 | final DetailAST rparen = aAST.findFirstToken(TokenTypes.RPAREN); |
248 | 18 | loopBody = rparen.getNextSibling(); |
249 | } | |
250 | 24 | return isTerminated(loopBody, false, false); |
251 | } | |
252 | ||
253 | /** | |
254 | * Checks if a given try/catch/finally block terminated by return, throw or, | |
255 | * if allowed break, continue. | |
256 | * @param aAST loop to check | |
257 | * @param aUseBreak should we consider break as terminator. | |
258 | * @param aUseContinue should we consider continue as terminator. | |
259 | * @return true if try/cath/finally block is terminated. | |
260 | */ | |
261 | private boolean checkTry(final DetailAST aAST, boolean aUseBreak, | |
262 | boolean aUseContinue) | |
263 | { | |
264 | 24 | final DetailAST finalStmt = aAST.getLastChild(); |
265 | 24 | if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) { |
266 | 12 | return isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), |
267 | aUseBreak, aUseContinue); | |
268 | } | |
269 | ||
270 | 12 | boolean isTerminated = isTerminated(aAST.getFirstChild(), |
271 | aUseBreak, aUseContinue); | |
272 | ||
273 | 12 | DetailAST catchStmt = aAST.findFirstToken(TokenTypes.LITERAL_CATCH); |
274 | 30 | while ((catchStmt != null) && isTerminated) { |
275 | 18 | final DetailAST catchBody = |
276 | catchStmt.findFirstToken(TokenTypes.SLIST); | |
277 | 18 | isTerminated &= isTerminated(catchBody, aUseBreak, aUseContinue); |
278 | 18 | catchStmt = catchStmt.getNextSibling(); |
279 | 18 | } |
280 | 12 | return isTerminated; |
281 | } | |
282 | ||
283 | /** | |
284 | * Checks if a given switch terminated by return, throw or, | |
285 | * if allowed break, continue. | |
286 | * @param aAST loop to check | |
287 | * @param aUseContinue should we consider continue as terminator. | |
288 | * @return true if switch is terminated. | |
289 | */ | |
290 | private boolean checkSwitch(final DetailAST aAST, boolean aUseContinue) | |
291 | { | |
292 | 12 | DetailAST caseGroup = aAST.findFirstToken(TokenTypes.CASE_GROUP); |
293 | 12 | boolean isTerminated = (caseGroup != null); |
294 | while (isTerminated && (caseGroup != null) | |
295 | 42 | && (caseGroup.getType() != TokenTypes.RCURLY)) |
296 | { | |
297 | 30 | final DetailAST caseBody = |
298 | caseGroup.findFirstToken(TokenTypes.SLIST); | |
299 | 30 | isTerminated &= isTerminated(caseBody, false, aUseContinue); |
300 | 30 | caseGroup = caseGroup.getNextSibling(); |
301 | 30 | } |
302 | 12 | return isTerminated; |
303 | } | |
304 | ||
305 | /** | |
306 | * Determines if the fall through case between <code>aCurrentCase</code> and | |
307 | * <code>aNextCase</code> is reliefed by a appropriate comment. | |
308 | * | |
309 | * @param aCurrentCase AST of the case that falls through to the next case. | |
310 | * @param aNextCase AST of the next case. | |
311 | * @return True if a relief comment was found | |
312 | */ | |
313 | private boolean hasFallTruComment(DetailAST aCurrentCase, | |
314 | DetailAST aNextCase) | |
315 | { | |
316 | ||
317 | 103 | final int startLineNo = aCurrentCase.getLineNo(); |
318 | 103 | final int endLineNo = aNextCase.getLineNo(); |
319 | 103 | final int endColNo = aNextCase.getColumnNo(); |
320 | ||
321 | /* | |
322 | * Remember: The lines number returned from the AST is 1-based, but | |
323 | * the lines number in this array are 0-based. So you will often | |
324 | * see a "lineNo-1" etc. | |
325 | */ | |
326 | 103 | final String[] lines = getLines(); |
327 | ||
328 | /* | |
329 | * Handle: | |
330 | * case 1: | |
331 | * /+ FALLTHRU +/ case 2: | |
332 | * .... | |
333 | * and | |
334 | * switch(i) { | |
335 | * default: | |
336 | * /+ FALLTHRU +/} | |
337 | */ | |
338 | 103 | final String linepart = lines[endLineNo - 1].substring(0, endColNo); |
339 | 103 | if (commentMatch(mRegExp, linepart, endLineNo)) { |
340 | 12 | return true; |
341 | } | |
342 | ||
343 | /* | |
344 | * Handle: | |
345 | * case 1: | |
346 | * ..... | |
347 | * // FALLTHRU | |
348 | * case 2: | |
349 | * .... | |
350 | * and | |
351 | * switch(i) { | |
352 | * default: | |
353 | * // FALLTHRU | |
354 | * } | |
355 | */ | |
356 | 106 | for (int i = endLineNo - 2; i > startLineNo - 1; i--) { |
357 | 106 | if (lines[i].trim().length() != 0) { |
358 | 91 | return commentMatch(mRegExp, lines[i], i + 1); |
359 | } | |
360 | } | |
361 | ||
362 | // Well -- no relief comment found. | |
363 | 0 | return false; |
364 | } | |
365 | ||
366 | /** | |
367 | * Does a regular expression match on the given line and checks that a | |
368 | * possible match is within a comment. | |
369 | * @param aPattern The regular expression pattern to use. | |
370 | * @param aLine The line of test to do the match on. | |
371 | * @param aLineNo The line number in the file. | |
372 | * @return True if a match was found inside a comment. | |
373 | */ | |
374 | private boolean commentMatch(Pattern aPattern, String aLine, int aLineNo | |
375 | ) | |
376 | { | |
377 | 194 | final Matcher matcher = aPattern.matcher(aLine); |
378 | ||
379 | 194 | final boolean hit = matcher.find(); |
380 | ||
381 | 194 | if (hit) { |
382 | 53 | final int startMatch = matcher.start(); |
383 | // -1 because it returns the char position beyond the match | |
384 | 53 | final int endMatch = matcher.end() - 1; |
385 | 53 | return getFileContents().hasIntersectionWithComment(aLineNo, |
386 | startMatch, aLineNo, endMatch); | |
387 | } | |
388 | 141 | return false; |
389 | } | |
390 | } |