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