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