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