001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2022 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.CodePointUtil;
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 yield}, {@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, yield, 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 * public int bar() {
101 *   int i = 0;
102 *   return switch (i) {
103 *     case 1:
104 *       i++;
105 *     case 2: // violation, previous case contains code but lacks
106 *             // break, return, yield, throw or continue statement
107 *     case 3: // OK, case does not contain code
108 *       i++;
109 *       yield 11;
110 *     default: // OK
111 *       yield -1;
112 *   };
113 * }
114 * </pre>
115 * <p>
116 * Example how to suppress violations by comment:
117 * </p>
118 * <pre>
119 * switch (i) {
120 *   case 1:
121 *     i++; // fall through
122 *
123 *   case 2: // OK
124 *     i++;
125 *     // fallthru
126 *   case 3: { // OK
127 *     i++;
128 *   }
129 *   &#47;* fall-thru *&#47;
130 *   case 4: // OK
131 *     i++;
132 *     // Fallthru
133 *   case 5: // violation, "Fallthru" in case 4 should be "fallthru"
134 *     i++;
135 *     // fall through
136 *     i++;
137 *   case 6: // violation, the comment must be on the last non-empty line before 'case'
138 *     i++;
139 *   &#47;* fall through *&#47;case 7: // OK, comment can appear on the same line but before 'case'
140 *     i++;
141 * }
142 * </pre>
143 * <p>
144 * To configure the check to enable check for last case group:
145 * </p>
146 * <pre>
147 * &lt;module name=&quot;FallThrough&quot;&gt;
148 *    &lt;property name=&quot;checkLastCaseGroup&quot; value=&quot;true&quot;/&gt;
149 * &lt;/module&gt;
150 * </pre>
151 * <p>
152 * Example:
153 * </p>
154 * <pre>
155 * switch (i) {
156 *   case 1:
157 *     break;
158 *   case 2: // Previous case: OK
159 *           // This case: violation, last case must have statement that transfer control
160 *     i++;
161 * }
162 * </pre>
163 * <p>
164 * To configure the check with custom relief pattern:
165 * </p>
166 * <pre>
167 * &lt;module name=&quot;FallThrough&quot;&gt;
168 *    &lt;property name=&quot;reliefPattern&quot; value=&quot;FALL?[ -]?THROUGH&quot;/&gt;
169 * &lt;/module&gt;
170 * </pre>
171 * <p>
172 * Example:
173 * </p>
174 * <pre>
175 * switch (i) {
176 *   case 1:
177 *     i++;
178 *     // FALL-THROUGH
179 *   case 2: // OK, "FALL-THROUGH" matches the regular expression "FALL?[ -]?THROUGH"
180 *     i++;
181 *     // fall-through
182 *   case 3: // violation, "fall-through" doesn't match
183 *     break;
184 * }
185 * </pre>
186 * <p>
187 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
188 * </p>
189 * <p>
190 * Violation Message Keys:
191 * </p>
192 * <ul>
193 * <li>
194 * {@code fall.through}
195 * </li>
196 * <li>
197 * {@code fall.through.last}
198 * </li>
199 * </ul>
200 *
201 * @since 3.4
202 */
203@StatelessCheck
204public class FallThroughCheck extends AbstractCheck {
205
206    /**
207     * A key is pointing to the warning message text in "messages.properties"
208     * file.
209     */
210    public static final String MSG_FALL_THROUGH = "fall.through";
211
212    /**
213     * A key is pointing to the warning message text in "messages.properties"
214     * file.
215     */
216    public static final String MSG_FALL_THROUGH_LAST = "fall.through.last";
217
218    /** Control whether the last case group must be checked. */
219    private boolean checkLastCaseGroup;
220
221    /**
222     * Define the RegExp to match the relief comment that suppresses
223     * the warning about a fall through.
224     */
225    private Pattern reliefPattern = Pattern.compile("falls?[ -]?thr(u|ough)");
226
227    @Override
228    public int[] getDefaultTokens() {
229        return getRequiredTokens();
230    }
231
232    @Override
233    public int[] getRequiredTokens() {
234        return new int[] {TokenTypes.CASE_GROUP};
235    }
236
237    @Override
238    public int[] getAcceptableTokens() {
239        return getRequiredTokens();
240    }
241
242    /**
243     * Setter to define the RegExp to match the relief comment that suppresses
244     * the warning about a fall through.
245     *
246     * @param pattern
247     *            The regular expression pattern.
248     */
249    public void setReliefPattern(Pattern pattern) {
250        reliefPattern = pattern;
251    }
252
253    /**
254     * Setter to control whether the last case group must be checked.
255     *
256     * @param value new value of the property.
257     */
258    public void setCheckLastCaseGroup(boolean value) {
259        checkLastCaseGroup = value;
260    }
261
262    @Override
263    public void visitToken(DetailAST ast) {
264        final DetailAST nextGroup = ast.getNextSibling();
265        final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
266        if (!isLastGroup || checkLastCaseGroup) {
267            final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
268
269            if (slist != null && !isTerminated(slist, true, true)
270                && !hasFallThroughComment(ast, nextGroup)) {
271                if (isLastGroup) {
272                    log(ast, MSG_FALL_THROUGH_LAST);
273                }
274                else {
275                    log(nextGroup, MSG_FALL_THROUGH);
276                }
277            }
278        }
279    }
280
281    /**
282     * Checks if a given subtree terminated by return, throw or,
283     * if allowed break, continue.
284     *
285     * @param ast root of given subtree
286     * @param useBreak should we consider break as terminator
287     * @param useContinue should we consider continue as terminator
288     * @return true if the subtree is terminated.
289     */
290    private boolean isTerminated(final DetailAST ast, boolean useBreak,
291                                 boolean useContinue) {
292        final boolean terminated;
293
294        switch (ast.getType()) {
295            case TokenTypes.LITERAL_RETURN:
296            case TokenTypes.LITERAL_YIELD:
297            case TokenTypes.LITERAL_THROW:
298                terminated = true;
299                break;
300            case TokenTypes.LITERAL_BREAK:
301                terminated = useBreak;
302                break;
303            case TokenTypes.LITERAL_CONTINUE:
304                terminated = useContinue;
305                break;
306            case TokenTypes.SLIST:
307                terminated = checkSlist(ast, useBreak, useContinue);
308                break;
309            case TokenTypes.LITERAL_IF:
310                terminated = checkIf(ast, useBreak, useContinue);
311                break;
312            case TokenTypes.LITERAL_FOR:
313            case TokenTypes.LITERAL_WHILE:
314            case TokenTypes.LITERAL_DO:
315                terminated = checkLoop(ast);
316                break;
317            case TokenTypes.LITERAL_TRY:
318                terminated = checkTry(ast, useBreak, useContinue);
319                break;
320            case TokenTypes.LITERAL_SWITCH:
321                terminated = checkSwitch(ast, useContinue);
322                break;
323            case TokenTypes.LITERAL_SYNCHRONIZED:
324                terminated = checkSynchronized(ast, useBreak, useContinue);
325                break;
326            default:
327                terminated = false;
328        }
329        return terminated;
330    }
331
332    /**
333     * Checks if a given SLIST terminated by return, throw or,
334     * if allowed break, continue.
335     *
336     * @param slistAst SLIST to check
337     * @param useBreak should we consider break as terminator
338     * @param useContinue should we consider continue as terminator
339     * @return true if SLIST is terminated.
340     */
341    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
342                               boolean useContinue) {
343        DetailAST lastStmt = slistAst.getLastChild();
344
345        if (lastStmt.getType() == TokenTypes.RCURLY) {
346            lastStmt = lastStmt.getPreviousSibling();
347        }
348
349        return lastStmt != null
350            && isTerminated(lastStmt, useBreak, useContinue);
351    }
352
353    /**
354     * Checks if a given IF terminated by return, throw or,
355     * if allowed break, continue.
356     *
357     * @param ast IF to check
358     * @param useBreak should we consider break as terminator
359     * @param useContinue should we consider continue as terminator
360     * @return true if IF is terminated.
361     */
362    private boolean checkIf(final DetailAST ast, boolean useBreak,
363                            boolean useContinue) {
364        final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN)
365                .getNextSibling();
366        final DetailAST elseStmt = thenStmt.getNextSibling();
367
368        return elseStmt != null
369                && isTerminated(thenStmt, useBreak, useContinue)
370                && isTerminated(elseStmt.getFirstChild(), useBreak, useContinue);
371    }
372
373    /**
374     * Checks if a given loop terminated by return, throw or,
375     * if allowed break, continue.
376     *
377     * @param ast loop to check
378     * @return true if loop is terminated.
379     */
380    private boolean checkLoop(final DetailAST ast) {
381        final DetailAST loopBody;
382        if (ast.getType() == TokenTypes.LITERAL_DO) {
383            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
384            loopBody = lparen.getPreviousSibling();
385        }
386        else {
387            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
388            loopBody = rparen.getNextSibling();
389        }
390        return isTerminated(loopBody, false, false);
391    }
392
393    /**
394     * Checks if a given try/catch/finally block terminated by return, throw or,
395     * if allowed break, continue.
396     *
397     * @param ast loop to check
398     * @param useBreak should we consider break as terminator
399     * @param useContinue should we consider continue as terminator
400     * @return true if try/catch/finally block is terminated
401     */
402    private boolean checkTry(final DetailAST ast, boolean useBreak,
403                             boolean useContinue) {
404        final DetailAST finalStmt = ast.getLastChild();
405        boolean isTerminated = finalStmt.getType() == TokenTypes.LITERAL_FINALLY
406                && isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
407
408        if (!isTerminated) {
409            DetailAST firstChild = ast.getFirstChild();
410
411            if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
412                firstChild = firstChild.getNextSibling();
413            }
414
415            isTerminated = isTerminated(firstChild,
416                    useBreak, useContinue);
417
418            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
419            while (catchStmt != null
420                    && isTerminated
421                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
422                final DetailAST catchBody =
423                        catchStmt.findFirstToken(TokenTypes.SLIST);
424                isTerminated = isTerminated(catchBody, useBreak, useContinue);
425                catchStmt = catchStmt.getNextSibling();
426            }
427        }
428        return isTerminated;
429    }
430
431    /**
432     * Checks if a given switch terminated by return, throw or,
433     * if allowed break, continue.
434     *
435     * @param literalSwitchAst loop to check
436     * @param useContinue should we consider continue as terminator
437     * @return true if switch is terminated
438     */
439    private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) {
440        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
441        boolean isTerminated = caseGroup != null;
442        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
443            final DetailAST caseBody =
444                caseGroup.findFirstToken(TokenTypes.SLIST);
445            isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
446            caseGroup = caseGroup.getNextSibling();
447        }
448        return isTerminated;
449    }
450
451    /**
452     * Checks if a given synchronized block terminated by return, throw or,
453     * if allowed break, continue.
454     *
455     * @param synchronizedAst synchronized block to check.
456     * @param useBreak should we consider break as terminator
457     * @param useContinue should we consider continue as terminator
458     * @return true if synchronized block is terminated
459     */
460    private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
461                                      boolean useContinue) {
462        return isTerminated(
463            synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
464    }
465
466    /**
467     * Determines if the fall through case between {@code currentCase} and
468     * {@code nextCase} is relieved by an appropriate comment.
469     *
470     * <p>Handles</p>
471     * <pre>
472     * case 1:
473     * /&#42; FALLTHRU &#42;/ case 2:
474     *
475     * switch(i) {
476     * default:
477     * /&#42; FALLTHRU &#42;/}
478     *
479     * case 1:
480     * // FALLTHRU
481     * case 2:
482     *
483     * switch(i) {
484     * default:
485     * // FALLTHRU
486     * </pre>
487     *
488     * @param currentCase AST of the case that falls through to the next case.
489     * @param nextCase AST of the next case.
490     * @return True if a relief comment was found
491     */
492    private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) {
493        boolean allThroughComment = false;
494        final int endLineNo = nextCase.getLineNo();
495
496        if (matchesComment(reliefPattern, endLineNo)) {
497            allThroughComment = true;
498        }
499        else {
500            final int startLineNo = currentCase.getLineNo();
501            for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
502                final int[] line = getLineCodePoints(i);
503                if (!CodePointUtil.isBlank(line)) {
504                    allThroughComment = matchesComment(reliefPattern, i + 1);
505                    break;
506                }
507            }
508        }
509        return allThroughComment;
510    }
511
512    /**
513     * Does a regular expression match on the given line and checks that a
514     * possible match is within a comment.
515     *
516     * @param pattern The regular expression pattern to use.
517     * @param lineNo The line number in the file.
518     * @return True if a match was found inside a comment.
519     */
520    // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
521    @SuppressWarnings("deprecation")
522    private boolean matchesComment(Pattern pattern, int lineNo) {
523        final String line = getLine(lineNo - 1);
524
525        final Matcher matcher = pattern.matcher(line);
526        return matcher.find()
527                && getFileContents().hasIntersectionWithComment(
528                        lineNo, matcher.start(), lineNo, matcher.end());
529    }
530
531}