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