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 * <pre>
048 * switch (i) {
049 * case 0:
050 *   i++; // fall through
051 *
052 * case 1:
053 *   i++;
054 *   // falls through
055 * case 2:
056 * case 3:
057 * case 4: {
058 *   i++;
059 * }
060 * // fallthrough
061 * case 5:
062 *   i++;
063 * &#47;* fallthru *&#47;case 6:
064 *   i++;
065 * // fall-through
066 * case 7:
067 *   i++;
068 *   break;
069 * }
070 * </pre>
071 * <p>
072 * Note: The check assumes that there is no unreachable code in the {@code case}.
073 * </p>
074 * <p>
075 * The following fragment of code will NOT trigger the check,
076 * because of the comment "fallthru" or any Java code
077 * in case 5 are absent.
078 * </p>
079 * <pre>
080 * case 3:
081 *     x = 2;
082 *     // fallthru
083 * case 4:
084 * case 5: // violation
085 * case 6:
086 *     break;
087 * </pre>
088 * <ul>
089 * <li>
090 * Property {@code checkLastCaseGroup} - Control whether the last case group must be checked.
091 * Type is {@code boolean}.
092 * Default value is {@code false}.
093 * </li>
094 * <li>
095 * Property {@code reliefPattern} - Define the RegExp to match the relief comment that suppresses
096 * the warning about a fall through.
097 * Type is {@code java.util.regex.Pattern}.
098 * Default value is {@code "falls?[ -]?thr(u|ough)"}.
099 * </li>
100 * </ul>
101 * <p>
102 * To configure the check:
103 * </p>
104 * <pre>
105 * &lt;module name=&quot;FallThrough&quot;/&gt;
106 * </pre>
107 * <p>
108 * or
109 * </p>
110 * <pre>
111 * &lt;module name=&quot;FallThrough&quot;&gt;
112 *   &lt;property name=&quot;reliefPattern&quot; value=&quot;continue in next case&quot;/&gt;
113 * &lt;/module&gt;
114 * </pre>
115 * <p>
116 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
117 * </p>
118 * <p>
119 * Violation Message Keys:
120 * </p>
121 * <ul>
122 * <li>
123 * {@code fall.through}
124 * </li>
125 * <li>
126 * {@code fall.through.last}
127 * </li>
128 * </ul>
129 *
130 * @since 3.4
131 */
132@StatelessCheck
133public class FallThroughCheck extends AbstractCheck {
134
135    /**
136     * A key is pointing to the warning message text in "messages.properties"
137     * file.
138     */
139    public static final String MSG_FALL_THROUGH = "fall.through";
140
141    /**
142     * A key is pointing to the warning message text in "messages.properties"
143     * file.
144     */
145    public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
146
147    /** Control whether the last case group must be checked. */
148    private boolean checkLastCaseGroup;
149
150    /**
151     * Define the RegExp to match the relief comment that suppresses
152     * the warning about a fall through.
153     */
154    private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
155
156    @Override
157    public int[] getDefaultTokens() {
158        return getRequiredTokens();
159    }
160
161    @Override
162    public int[] getRequiredTokens() {
163        return new int[] {TokenTypes.CASE_GROUP};
164    }
165
166    @Override
167    public int[] getAcceptableTokens() {
168        return getRequiredTokens();
169    }
170
171    /**
172     * Setter to define the RegExp to match the relief comment that suppresses
173     * the warning about a fall through.
174     *
175     * @param pattern
176     *            The regular expression pattern.
177     */
178    public void setReliefPattern(Pattern pattern) {
179        reliefPattern = pattern;
180    }
181
182    /**
183     * Setter to control whether the last case group must be checked.
184     *
185     * @param value new value of the property.
186     */
187    public void setCheckLastCaseGroup(boolean value) {
188        checkLastCaseGroup = value;
189    }
190
191    @Override
192    public void visitToken(DetailAST ast) {
193        final DetailAST nextGroup = ast.getNextSibling();
194        final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
195        if (!isLastGroup || checkLastCaseGroup) {
196            final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
197
198            if (slist != null && !isTerminated(slist, true, true)
199                && !hasFallThroughComment(ast, nextGroup)) {
200                if (isLastGroup) {
201                    log(ast, MSG_FALL_THROUGH_LAST);
202                }
203                else {
204                    log(nextGroup, MSG_FALL_THROUGH);
205                }
206            }
207        }
208    }
209
210    /**
211     * Checks if a given subtree terminated by return, throw or,
212     * if allowed break, continue.
213     *
214     * @param ast root of given subtree
215     * @param useBreak should we consider break as terminator.
216     * @param useContinue should we consider continue as terminator.
217     * @return true if the subtree is terminated.
218     */
219    private boolean isTerminated(final DetailAST ast, boolean useBreak,
220                                 boolean useContinue) {
221        final boolean terminated;
222
223        switch (ast.getType()) {
224            case TokenTypes.LITERAL_RETURN:
225            case TokenTypes.LITERAL_THROW:
226                terminated = true;
227                break;
228            case TokenTypes.LITERAL_BREAK:
229                terminated = useBreak;
230                break;
231            case TokenTypes.LITERAL_CONTINUE:
232                terminated = useContinue;
233                break;
234            case TokenTypes.SLIST:
235                terminated = checkSlist(ast, useBreak, useContinue);
236                break;
237            case TokenTypes.LITERAL_IF:
238                terminated = checkIf(ast, useBreak, useContinue);
239                break;
240            case TokenTypes.LITERAL_FOR:
241            case TokenTypes.LITERAL_WHILE:
242            case TokenTypes.LITERAL_DO:
243                terminated = checkLoop(ast);
244                break;
245            case TokenTypes.LITERAL_TRY:
246                terminated = checkTry(ast, useBreak, useContinue);
247                break;
248            case TokenTypes.LITERAL_SWITCH:
249                terminated = checkSwitch(ast, useContinue);
250                break;
251            case TokenTypes.LITERAL_SYNCHRONIZED:
252                terminated = checkSynchronized(ast, useBreak, useContinue);
253                break;
254            default:
255                terminated = false;
256        }
257        return terminated;
258    }
259
260    /**
261     * Checks if a given SLIST terminated by return, throw or,
262     * if allowed break, continue.
263     *
264     * @param slistAst SLIST to check
265     * @param useBreak should we consider break as terminator.
266     * @param useContinue should we consider continue as terminator.
267     * @return true if SLIST is terminated.
268     */
269    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
270                               boolean useContinue) {
271        DetailAST lastStmt = slistAst.getLastChild();
272
273        if (lastStmt.getType() == TokenTypes.RCURLY) {
274            lastStmt = lastStmt.getPreviousSibling();
275        }
276
277        return lastStmt != null
278            && isTerminated(lastStmt, useBreak, useContinue);
279    }
280
281    /**
282     * Checks if a given IF terminated by return, throw or,
283     * if allowed break, continue.
284     *
285     * @param ast IF to check
286     * @param useBreak should we consider break as terminator.
287     * @param useContinue should we consider continue as terminator.
288     * @return true if IF is terminated.
289     */
290    private boolean checkIf(final DetailAST ast, boolean useBreak,
291                            boolean useContinue) {
292        final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN)
293                .getNextSibling();
294        final DetailAST elseStmt = thenStmt.getNextSibling();
295
296        return elseStmt != null
297                && isTerminated(thenStmt, useBreak, useContinue)
298                && isTerminated(elseStmt.getFirstChild(), useBreak, useContinue);
299    }
300
301    /**
302     * Checks if a given loop terminated by return, throw or,
303     * if allowed break, continue.
304     *
305     * @param ast loop to check
306     * @return true if loop is terminated.
307     */
308    private boolean checkLoop(final DetailAST ast) {
309        final DetailAST loopBody;
310        if (ast.getType() == TokenTypes.LITERAL_DO) {
311            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
312            loopBody = lparen.getPreviousSibling();
313        }
314        else {
315            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
316            loopBody = rparen.getNextSibling();
317        }
318        return isTerminated(loopBody, false, false);
319    }
320
321    /**
322     * Checks if a given try/catch/finally block terminated by return, throw or,
323     * if allowed break, continue.
324     *
325     * @param ast loop to check
326     * @param useBreak should we consider break as terminator.
327     * @param useContinue should we consider continue as terminator.
328     * @return true if try/catch/finally block is terminated.
329     */
330    private boolean checkTry(final DetailAST ast, boolean useBreak,
331                             boolean useContinue) {
332        final DetailAST finalStmt = ast.getLastChild();
333        boolean isTerminated = false;
334        if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) {
335            isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
336                                useBreak, useContinue);
337        }
338
339        if (!isTerminated) {
340            DetailAST firstChild = ast.getFirstChild();
341
342            if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
343                firstChild = firstChild.getNextSibling();
344            }
345
346            isTerminated = isTerminated(firstChild,
347                    useBreak, useContinue);
348
349            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
350            while (catchStmt != null
351                    && isTerminated
352                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
353                final DetailAST catchBody =
354                        catchStmt.findFirstToken(TokenTypes.SLIST);
355                isTerminated = isTerminated(catchBody, useBreak, useContinue);
356                catchStmt = catchStmt.getNextSibling();
357            }
358        }
359        return isTerminated;
360    }
361
362    /**
363     * Checks if a given switch terminated by return, throw or,
364     * if allowed break, continue.
365     *
366     * @param literalSwitchAst loop to check
367     * @param useContinue should we consider continue as terminator.
368     * @return true if switch is terminated.
369     */
370    private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) {
371        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
372        boolean isTerminated = caseGroup != null;
373        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
374            final DetailAST caseBody =
375                caseGroup.findFirstToken(TokenTypes.SLIST);
376            isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
377            caseGroup = caseGroup.getNextSibling();
378        }
379        return isTerminated;
380    }
381
382    /**
383     * Checks if a given synchronized block terminated by return, throw or,
384     * if allowed break, continue.
385     *
386     * @param synchronizedAst synchronized block to check.
387     * @param useBreak should we consider break as terminator.
388     * @param useContinue should we consider continue as terminator.
389     * @return true if synchronized block is terminated.
390     */
391    private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
392                                      boolean useContinue) {
393        return isTerminated(
394            synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
395    }
396
397    /**
398     * Determines if the fall through case between {@code currentCase} and
399     * {@code nextCase} is relieved by a appropriate comment.
400     *
401     * @param currentCase AST of the case that falls through to the next case.
402     * @param nextCase AST of the next case.
403     * @return True if a relief comment was found
404     */
405    private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) {
406        boolean allThroughComment = false;
407        final int endLineNo = nextCase.getLineNo();
408        final int endColNo = nextCase.getColumnNo();
409
410        // Remember: The lines number returned from the AST is 1-based, but
411        // the lines number in this array are 0-based. So you will often
412        // see a "lineNo-1" etc.
413        final String[] lines = getLines();
414
415        // Handle:
416        //    case 1:
417        //    /+ FALLTHRU +/ case 2:
418        //    ....
419        // and
420        //    switch(i) {
421        //    default:
422        //    /+ FALLTHRU +/}
423        //
424        final String linePart = lines[endLineNo - 1].substring(0, endColNo);
425        if (matchesComment(reliefPattern, linePart, endLineNo)) {
426            allThroughComment = true;
427        }
428        else {
429            // Handle:
430            //    case 1:
431            //    .....
432            //    // FALLTHRU
433            //    case 2:
434            //    ....
435            // and
436            //    switch(i) {
437            //    default:
438            //    // FALLTHRU
439            //    }
440            final int startLineNo = currentCase.getLineNo();
441            for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
442                if (!CommonUtil.isBlank(lines[i])) {
443                    allThroughComment = matchesComment(reliefPattern, lines[i], i + 1);
444                    break;
445                }
446            }
447        }
448        return allThroughComment;
449    }
450
451    /**
452     * Does a regular expression match on the given line and checks that a
453     * possible match is within a comment.
454     *
455     * @param pattern The regular expression pattern to use.
456     * @param line The line of test to do the match on.
457     * @param lineNo The line number in the file.
458     * @return True if a match was found inside a comment.
459     */
460    private boolean matchesComment(Pattern pattern, String line, int lineNo) {
461        final Matcher matcher = pattern.matcher(line);
462        boolean matches = false;
463
464        if (matcher.find()) {
465            matches = getFileContents().hasIntersectionWithComment(lineNo, matcher.start(),
466                    lineNo, matcher.end());
467        }
468        return matches;
469    }
470
471}