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     * @param value new value of the property.
169     */
170    public void setCheckLastCaseGroup(boolean value) {
171        checkLastCaseGroup = value;
172    }
173
174    @Override
175    public void visitToken(DetailAST ast) {
176        final DetailAST nextGroup = ast.getNextSibling();
177        final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
178        if (!isLastGroup || checkLastCaseGroup) {
179            final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
180
181            if (slist != null && !isTerminated(slist, true, true)
182                && !hasFallThroughComment(ast, nextGroup)) {
183                if (isLastGroup) {
184                    log(ast, MSG_FALL_THROUGH_LAST);
185                }
186                else {
187                    log(nextGroup, MSG_FALL_THROUGH);
188                }
189            }
190        }
191    }
192
193    /**
194     * Checks if a given subtree terminated by return, throw or,
195     * if allowed break, continue.
196     * @param ast root of given subtree
197     * @param useBreak should we consider break as terminator.
198     * @param useContinue should we consider continue as terminator.
199     * @return true if the subtree is terminated.
200     */
201    private boolean isTerminated(final DetailAST ast, boolean useBreak,
202                                 boolean useContinue) {
203        final boolean terminated;
204
205        switch (ast.getType()) {
206            case TokenTypes.LITERAL_RETURN:
207            case TokenTypes.LITERAL_THROW:
208                terminated = true;
209                break;
210            case TokenTypes.LITERAL_BREAK:
211                terminated = useBreak;
212                break;
213            case TokenTypes.LITERAL_CONTINUE:
214                terminated = useContinue;
215                break;
216            case TokenTypes.SLIST:
217                terminated = checkSlist(ast, useBreak, useContinue);
218                break;
219            case TokenTypes.LITERAL_IF:
220                terminated = checkIf(ast, useBreak, useContinue);
221                break;
222            case TokenTypes.LITERAL_FOR:
223            case TokenTypes.LITERAL_WHILE:
224            case TokenTypes.LITERAL_DO:
225                terminated = checkLoop(ast);
226                break;
227            case TokenTypes.LITERAL_TRY:
228                terminated = checkTry(ast, useBreak, useContinue);
229                break;
230            case TokenTypes.LITERAL_SWITCH:
231                terminated = checkSwitch(ast, useContinue);
232                break;
233            case TokenTypes.LITERAL_SYNCHRONIZED:
234                terminated = checkSynchronized(ast, useBreak, useContinue);
235                break;
236            default:
237                terminated = false;
238        }
239        return terminated;
240    }
241
242    /**
243     * Checks if a given SLIST terminated by return, throw or,
244     * if allowed break, continue.
245     * @param slistAst SLIST to check
246     * @param useBreak should we consider break as terminator.
247     * @param useContinue should we consider continue as terminator.
248     * @return true if SLIST is terminated.
249     */
250    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
251                               boolean useContinue) {
252        DetailAST lastStmt = slistAst.getLastChild();
253
254        if (lastStmt.getType() == TokenTypes.RCURLY) {
255            lastStmt = lastStmt.getPreviousSibling();
256        }
257
258        return lastStmt != null
259            && isTerminated(lastStmt, useBreak, useContinue);
260    }
261
262    /**
263     * Checks if a given IF terminated by return, throw or,
264     * if allowed break, continue.
265     * @param ast IF to check
266     * @param useBreak should we consider break as terminator.
267     * @param useContinue should we consider continue as terminator.
268     * @return true if IF is terminated.
269     */
270    private boolean checkIf(final DetailAST ast, boolean useBreak,
271                            boolean useContinue) {
272        final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN)
273                .getNextSibling();
274        final DetailAST elseStmt = thenStmt.getNextSibling();
275
276        return elseStmt != null
277                && isTerminated(thenStmt, useBreak, useContinue)
278                && isTerminated(elseStmt.getFirstChild(), useBreak, useContinue);
279    }
280
281    /**
282     * Checks if a given loop terminated by return, throw or,
283     * if allowed break, continue.
284     * @param ast loop to check
285     * @return true if loop is terminated.
286     */
287    private boolean checkLoop(final DetailAST ast) {
288        final DetailAST loopBody;
289        if (ast.getType() == TokenTypes.LITERAL_DO) {
290            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
291            loopBody = lparen.getPreviousSibling();
292        }
293        else {
294            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
295            loopBody = rparen.getNextSibling();
296        }
297        return isTerminated(loopBody, false, false);
298    }
299
300    /**
301     * Checks if a given try/catch/finally block terminated by return, throw or,
302     * if allowed break, continue.
303     * @param ast loop to check
304     * @param useBreak should we consider break as terminator.
305     * @param useContinue should we consider continue as terminator.
306     * @return true if try/catch/finally block is terminated.
307     */
308    private boolean checkTry(final DetailAST ast, boolean useBreak,
309                             boolean useContinue) {
310        final DetailAST finalStmt = ast.getLastChild();
311        boolean isTerminated = false;
312        if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) {
313            isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
314                                useBreak, useContinue);
315        }
316
317        if (!isTerminated) {
318            DetailAST firstChild = ast.getFirstChild();
319
320            if (firstChild.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
321                firstChild = firstChild.getNextSibling();
322            }
323
324            isTerminated = isTerminated(firstChild,
325                    useBreak, useContinue);
326
327            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
328            while (catchStmt != null
329                    && isTerminated
330                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
331                final DetailAST catchBody =
332                        catchStmt.findFirstToken(TokenTypes.SLIST);
333                isTerminated = isTerminated(catchBody, useBreak, useContinue);
334                catchStmt = catchStmt.getNextSibling();
335            }
336        }
337        return isTerminated;
338    }
339
340    /**
341     * Checks if a given switch terminated by return, throw or,
342     * if allowed break, continue.
343     * @param literalSwitchAst loop to check
344     * @param useContinue should we consider continue as terminator.
345     * @return true if switch is terminated.
346     */
347    private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) {
348        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
349        boolean isTerminated = caseGroup != null;
350        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
351            final DetailAST caseBody =
352                caseGroup.findFirstToken(TokenTypes.SLIST);
353            isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
354            caseGroup = caseGroup.getNextSibling();
355        }
356        return isTerminated;
357    }
358
359    /**
360     * Checks if a given synchronized block terminated by return, throw or,
361     * if allowed break, continue.
362     * @param synchronizedAst synchronized block to check.
363     * @param useBreak should we consider break as terminator.
364     * @param useContinue should we consider continue as terminator.
365     * @return true if synchronized block is terminated.
366     */
367    private boolean checkSynchronized(final DetailAST synchronizedAst, boolean useBreak,
368                                      boolean useContinue) {
369        return isTerminated(
370            synchronizedAst.findFirstToken(TokenTypes.SLIST), useBreak, useContinue);
371    }
372
373    /**
374     * Determines if the fall through case between {@code currentCase} and
375     * {@code nextCase} is relieved by a appropriate comment.
376     *
377     * @param currentCase AST of the case that falls through to the next case.
378     * @param nextCase AST of the next case.
379     * @return True if a relief comment was found
380     */
381    private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) {
382        boolean allThroughComment = false;
383        final int endLineNo = nextCase.getLineNo();
384        final int endColNo = nextCase.getColumnNo();
385
386        // Remember: The lines number returned from the AST is 1-based, but
387        // the lines number in this array are 0-based. So you will often
388        // see a "lineNo-1" etc.
389        final String[] lines = getLines();
390
391        // Handle:
392        //    case 1:
393        //    /+ FALLTHRU +/ case 2:
394        //    ....
395        // and
396        //    switch(i) {
397        //    default:
398        //    /+ FALLTHRU +/}
399        //
400        final String linePart = lines[endLineNo - 1].substring(0, endColNo);
401        if (matchesComment(reliefPattern, linePart, endLineNo)) {
402            allThroughComment = true;
403        }
404        else {
405            // Handle:
406            //    case 1:
407            //    .....
408            //    // FALLTHRU
409            //    case 2:
410            //    ....
411            // and
412            //    switch(i) {
413            //    default:
414            //    // FALLTHRU
415            //    }
416            final int startLineNo = currentCase.getLineNo();
417            for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
418                if (!CommonUtil.isBlank(lines[i])) {
419                    allThroughComment = matchesComment(reliefPattern, lines[i], i + 1);
420                    break;
421                }
422            }
423        }
424        return allThroughComment;
425    }
426
427    /**
428     * Does a regular expression match on the given line and checks that a
429     * possible match is within a comment.
430     * @param pattern The regular expression pattern to use.
431     * @param line The line of test to do the match on.
432     * @param lineNo The line number in the file.
433     * @return True if a match was found inside a comment.
434     */
435    private boolean matchesComment(Pattern pattern, String line, int lineNo) {
436        final Matcher matcher = pattern.matcher(line);
437        boolean matches = false;
438
439        if (matcher.find()) {
440            matches = getFileContents().hasIntersectionWithComment(lineNo, matcher.start(),
441                    lineNo, matcher.end());
442        }
443        return matches;
444    }
445
446}