001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
030
031/**
032 * <p>
033 * Checks for fall-through in {@code switch} statements.
034 * Finds locations where a {@code case} <b>contains</b> Java code but lacks a
035 * {@code break}, {@code return}, {@code throw} or {@code continue} statement.
036 * </p>
037 * <p>
038 * The check honors special comments to suppress the warning.
039 * By default the texts
040 * "fallthru", "fall thru", "fall-thru",
041 * "fallthrough", "fall through", "fall-through"
042 * "fallsthrough", "falls through", "falls-through" (case sensitive).
043 * The comment containing these words must be all on one line,
044 * and must be on the last non-empty line before the {@code case} triggering
045 * the warning or on the same line before the {@code case}(ugly, but possible).
046 * </p>
047 * <p>
048 * Note: The check assumes that there is no unreachable code in the {@code case}.
049 * </p>
050 * <ul>
051 * <li>
052 * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked.
053 * Type is {@code boolean}.
054 * Default value is {@code false}.
055 * </li>
056 * <li>
057 * Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses
058 * the warning about a fall through.
059 * Type is {@code java.util.regex.Pattern}.
060 * Default value is {@code "falls?[ -]?thr(u|ough)"}.
061 * </li>
062 * </ul>
063 * <p>
064 * To configure the check:
065 * </p>
066 * <pre>
067 * &lt;module name="FallThrough"/&gt;
068 * </pre>
069 * <p>
070 * Example:
071 * </p>
072 * <pre>
073 * public void foo() throws Exception {
074 *   int i = 0;
075 *   while (i &gt;= 0) {
076 *     switch (i) {
077 *       case 1:
078 *         i++;
079 *       case 2: // violation, previous case contains code but lacks
080 *               // break, return, throw or continue statement
081 *         i++;
082 *         break;
083 *       case 3: // OK
084 *         i++;
085 *         return;
086 *       case 4: // OK
087 *         i++;
088 *         throw new Exception();
089 *       case 5: // OK
090 *         i++;
091 *         continue;
092 *       case 6: // OK
093 *       case 7: // Previous case: OK, case does not contain code
094 *               // This case: OK, by default the last case might not have statement
095 *               // that transfer control
096 *         i++;
097 *     }
098 *   }
099 * }
100 * </pre>
101 * <p>
102 * Example how to suppress violations by comment:
103 * </p>
104 * <pre>
105 * switch (i) {
106 *   case 1:
107 *     i++; // fall through
108 *
109 *   case 2: // OK
110 *     i++;
111 *     // fallthru
112 *   case 3: { // OK
113 *     i++;
114 *   }
115 *   &#47;* fall-thru *&#47;
116 *   case 4: // OK
117 *     i++;
118 *     // Fallthru
119 *   case 5: // violation, "Fallthru" in case 4 should be "fallthru"
120 *     i++;
121 *     // fall through
122 *     i++;
123 *   case 6: // violation, the comment must be on the last non-empty line before 'case'
124 *     i++;
125 *   &#47;* fall through *&#47;case 7: // OK, comment can appear on the same line but before 'case'
126 *     i++;
127 * }
128 * </pre>
129 * <p>
130 * To configure the check to enable check for last case group:
131 * </p>
132 * <pre>
133 * &lt;module name=&quot;FallThrough&quot;&gt;
134 *    &lt;property name=&quot;checkLastCaseGroup&quot; value=&quot;true&quot;/&gt;
135 * &lt;/module&gt;
136 * </pre>
137 * <p>
138 * Example:
139 * </p>
140 * <pre>
141 * switch (i) {
142 *   case 1:
143 *     break;
144 *   case 2: // Previous case: OK
145 *           // This case: violation, last case must have statement that transfer control
146 *     i++;
147 * }
148 * </pre>
149 * <p>
150 * To configure the check with custom relief pattern:
151 * </p>
152 * <pre>
153 * &lt;module name=&quot;FallThrough&quot;&gt;
154 *    &lt;property name=&quot;reliefPattern&quot; value=&quot;FALL?[ -]?THROUGH&quot;/&gt;
155 * &lt;/module&gt;
156 * </pre>
157 * <p>
158 * Example:
159 * </p>
160 * <pre>
161 * switch (i) {
162 *   case 1:
163 *     i++;
164 *     // FALL-THROUGH
165 *   case 2: // OK, "FALL-THROUGH" matches the regular expression "FALL?[ -]?THROUGH"
166 *     i++;
167 *     // fall-through
168 *   case 3: // violation, "fall-through" doesn't match
169 *     break;
170 * }
171 * </pre>
172 * <p>
173 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
174 * </p>
175 * <p>
176 * Violation Message Keys:
177 * </p>
178 * <ul>
179 * <li>
180 * {@code fall.through}
181 * </li>
182 * <li>
183 * {@code fall.through.last}
184 * </li>
185 * </ul>
186 *
187 * @since 3.4
188 */
189@StatelessCheck
190public class FallThroughCheck extends AbstractCheck {
191
192    /**
193     * A key is pointing to the warning message text in "messages.properties"
194     * file.
195     */
196    public static final String MSG_FALL_THROUGH = "fall.through";
197
198    /**
199     * A key is pointing to the warning message text in "messages.properties"
200     * file.
201     */
202    public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
203
204    /** Control whether the last case group must be checked. */
205    private boolean checkLastCaseGroup;
206
207    /**
208     * Define the RegExp to match the relief comment that suppresses
209     * the warning about a fall through.
210     */
211    private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
212
213    @Override
214    public int[] getDefaultTokens() {
215        return getRequiredTokens();
216    }
217
218    @Override
219    public int[] getRequiredTokens() {
220        return new int[] {TokenTypes.CASE_GROUP};
221    }
222
223    @Override
224    public int[] getAcceptableTokens() {
225        return getRequiredTokens();
226    }
227
228    /**
229     * Setter to define the RegExp to match the relief comment that suppresses
230     * the warning about a fall through.
231     *
232     * @param pattern
233     *            The regular expression pattern.
234     */
235    public void setReliefPattern(Pattern pattern) {
236        reliefPattern = pattern;
237    }
238
239    /**
240     * Setter to control whether the last case group must be checked.
241     *
242     * @param value new value of the property.
243     */
244    public void setCheckLastCaseGroup(boolean value) {
245        checkLastCaseGroup = value;
246    }
247
248    @Override
249    public void visitToken(DetailAST ast) {
250        final DetailAST nextGroup = ast.getNextSibling();
251        final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
252        if (!isLastGroup || checkLastCaseGroup) {
253            final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
254
255            if (slist != null && !isTerminated(slist, true, true)
256                && !hasFallThroughComment(ast, nextGroup)) {
257                if (isLastGroup) {
258                    log(ast, MSG_FALL_THROUGH_LAST);
259                }
260                else {
261                    log(nextGroup, MSG_FALL_THROUGH);
262                }
263            }
264        }
265    }
266
267    /**
268     * Checks if a given subtree terminated by return, throw or,
269     * if allowed break, continue.
270     *
271     * @param ast root of given subtree
272     * @param useBreak should we consider break as terminator.
273     * @param useContinue should we consider continue as terminator.
274     * @return true if the subtree is terminated.
275     */
276    private boolean isTerminated(final DetailAST ast, boolean useBreak,
277                                 boolean useContinue) {
278        final boolean terminated;
279
280        switch (ast.getType()) {
281            case TokenTypes.LITERAL_RETURN:
282            case TokenTypes.LITERAL_THROW:
283                terminated = true;
284                break;
285            case TokenTypes.LITERAL_BREAK:
286                terminated = useBreak;
287                break;
288            case TokenTypes.LITERAL_CONTINUE:
289                terminated = useContinue;
290                break;
291            case TokenTypes.SLIST:
292                terminated = checkSlist(ast, useBreak, useContinue);
293                break;
294            case TokenTypes.LITERAL_IF:
295                terminated = checkIf(ast, useBreak, useContinue);
296                break;
297            case TokenTypes.LITERAL_FOR:
298            case TokenTypes.LITERAL_WHILE:
299            case TokenTypes.LITERAL_DO:
300                terminated = checkLoop(ast);
301                break;
302            case TokenTypes.LITERAL_TRY:
303                terminated = checkTry(ast, useBreak, useContinue);
304                break;
305            case TokenTypes.LITERAL_SWITCH:
306                terminated = checkSwitch(ast, useContinue);
307                break;
308            case TokenTypes.LITERAL_SYNCHRONIZED:
309                terminated = checkSynchronized(ast, useBreak, useContinue);
310                break;
311            default:
312                terminated = false;
313        }
314        return terminated;
315    }
316
317    /**
318     * Checks if a given SLIST terminated by return, throw or,
319     * if allowed break, continue.
320     *
321     * @param slistAst SLIST to check
322     * @param useBreak should we consider break as terminator.
323     * @param useContinue should we consider continue as terminator.
324     * @return true if SLIST is terminated.
325     */
326    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
327                               boolean useContinue) {
328        DetailAST lastStmt = slistAst.getLastChild();
329
330        if (lastStmt.getType() == TokenTypes.RCURLY) {
331            lastStmt = lastStmt.getPreviousSibling();
332        }
333
334        return lastStmt != null
335            && isTerminated(lastStmt, useBreak, useContinue);
336    }
337
338    /**
339     * Checks if a given IF terminated by return, throw or,
340     * if allowed break, continue.
341     *
342     * @param ast IF to check
343     * @param useBreak should we consider break as terminator.
344     * @param useContinue should we consider continue as terminator.
345     * @return true if IF is terminated.
346     */
347    private boolean checkIf(final DetailAST ast, boolean useBreak,
348                            boolean useContinue) {
349        final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN)
350                .getNextSibling();
351        final DetailAST elseStmt = thenStmt.getNextSibling();
352
353        return elseStmt != null
354                && isTerminated(thenStmt, useBreak, useContinue)
355                && isTerminated(elseStmt.getFirstChild(), useBreak, useContinue);
356    }
357
358    /**
359     * Checks if a given loop terminated by return, throw or,
360     * if allowed break, continue.
361     *
362     * @param ast loop to check
363     * @return true if loop is terminated.
364     */
365    private boolean checkLoop(final DetailAST ast) {
366        final DetailAST loopBody;
367        if (ast.getType() == TokenTypes.LITERAL_DO) {
368            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
369            loopBody = lparen.getPreviousSibling();
370        }
371        else {
372            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
373            loopBody = rparen.getNextSibling();
374        }
375        return isTerminated(loopBody, false, false);
376    }
377
378    /**
379     * Checks if a given try/catch/finally block terminated by return, throw or,
380     * if allowed break, continue.
381     *
382     * @param ast loop to check
383     * @param useBreak should we consider break as terminator.
384     * @param useContinue should we consider continue as terminator.
385     * @return true if try/catch/finally block is terminated.
386     */
387    private boolean checkTry(final DetailAST ast, boolean useBreak,
388                             boolean useContinue) {
389        final DetailAST finalStmt = ast.getLastChild();
390        boolean isTerminated = false;
391        if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) {
392            isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
393                                useBreak, useContinue);
394        }
395
396        if (!isTerminated) {
397            DetailAST firstChild = ast.getFirstChild();
398
399            if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
400                firstChild = firstChild.getNextSibling();
401            }
402
403            isTerminated = isTerminated(firstChild,
404                    useBreak, useContinue);
405
406            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
407            while (catchStmt != null
408                    && isTerminated
409                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
410                final DetailAST catchBody =
411                        catchStmt.findFirstToken(TokenTypes.SLIST);
412                isTerminated = isTerminated(catchBody, useBreak, useContinue);
413                catchStmt = catchStmt.getNextSibling();
414            }
415        }
416        return isTerminated;
417    }
418
419    /**
420     * Checks if a given switch terminated by return, throw or,
421     * if allowed break, continue.
422     *
423     * @param literalSwitchAst loop to check
424     * @param useContinue should we consider continue as terminator.
425     * @return true if switch is terminated.
426     */
427    private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) {
428        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
429        boolean isTerminated = caseGroup != null;
430        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
431            final DetailAST caseBody =
432                caseGroup.findFirstToken(TokenTypes.SLIST);
433            isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
434            caseGroup = caseGroup.getNextSibling();
435        }
436        return isTerminated;
437    }
438
439    /**
440     * Checks if a given synchronized block terminated by return, throw or,
441     * if allowed break, continue.
442     *
443     * @param synchronizedAst synchronized block to check.
444     * @param useBreak should we consider break as terminator.
445     * @param useContinue should we consider continue as terminator.
446     * @return true if synchronized block is terminated.
447     */
448    private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
449                                      boolean useContinue) {
450        return isTerminated(
451            synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
452    }
453
454    /**
455     * Determines if the fall through case between {@code currentCase} and
456     * {@code nextCase} is relieved by a appropriate comment.
457     *
458     * @param currentCase AST of the case that falls through to the next case.
459     * @param nextCase AST of the next case.
460     * @return True if a relief comment was found
461     */
462    private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) {
463        boolean allThroughComment = false;
464        final int endLineNo = nextCase.getLineNo();
465        final int endColNo = nextCase.getColumnNo();
466
467        // Remember: The lines number returned from the AST is 1-based, but
468        // the lines number in this array are 0-based. So you will often
469        // see a "lineNo-1" etc.
470        final String[] lines = getLines();
471
472        // Handle:
473        //    case 1:
474        //    /+ FALLTHRU +/ case 2:
475        //    ....
476        // and
477        //    switch(i) {
478        //    default:
479        //    /+ FALLTHRU +/}
480        //
481        final String linePart = lines[endLineNo - 1].substring(0, endColNo);
482        if (matchesComment(reliefPattern, linePart, endLineNo)) {
483            allThroughComment = true;
484        }
485        else {
486            // Handle:
487            //    case 1:
488            //    .....
489            //    // FALLTHRU
490            //    case 2:
491            //    ....
492            // and
493            //    switch(i) {
494            //    default:
495            //    // FALLTHRU
496            //    }
497            final int startLineNo = currentCase.getLineNo();
498            for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
499                if (!CommonUtil.isBlank(lines[i])) {
500                    allThroughComment = matchesComment(reliefPattern, lines[i], i + 1);
501                    break;
502                }
503            }
504        }
505        return allThroughComment;
506    }
507
508    /**
509     * Does a regular expression match on the given line and checks that a
510     * possible match is within a comment.
511     *
512     * @param pattern The regular expression pattern to use.
513     * @param line The line of test to do the match on.
514     * @param lineNo The line number in the file.
515     * @return True if a match was found inside a comment.
516     */
517    private boolean matchesComment(Pattern pattern, String line, int lineNo) {
518        final Matcher matcher = pattern.matcher(line);
519        boolean matches = false;
520
521        if (matcher.find()) {
522            matches = getFileContents().hasIntersectionWithComment(lineNo, matcher.start(),
523                    lineNo, matcher.end());
524        }
525        return matches;
526    }
527
528}