001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.Check;
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 Check {
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 pattern to allow fall through to the next case branch. */
088    private String reliefPattern = "fallthru|falls? ?through";
089
090    /** Relief regexp. */
091    private Pattern regExp;
092
093    @Override
094    public int[] getDefaultTokens() {
095        return new int[]{TokenTypes.CASE_GROUP};
096    }
097
098    @Override
099    public int[] getRequiredTokens() {
100        return getDefaultTokens();
101    }
102
103    @Override
104    public int[] getAcceptableTokens() {
105        return new int[]{TokenTypes.CASE_GROUP};
106    }
107
108    /**
109     * Set the relief pattern.
110     *
111     * @param pattern
112     *            The regular expression pattern.
113     */
114    public void setReliefPattern(String pattern) {
115        reliefPattern = pattern;
116    }
117
118    /**
119     * Configures whether we need to check last case group or not.
120     * @param value new value of the property.
121     */
122    public void setCheckLastCaseGroup(boolean value) {
123        checkLastCaseGroup = value;
124    }
125
126    @Override
127    public void init() {
128        super.init();
129        regExp = Pattern.compile(reliefPattern);
130    }
131
132    @Override
133    public void visitToken(DetailAST ast) {
134        final DetailAST nextGroup = ast.getNextSibling();
135        final boolean isLastGroup = nextGroup.getType() != TokenTypes.CASE_GROUP;
136        if (isLastGroup && !checkLastCaseGroup) {
137            // we do not need to check last group
138            return;
139        }
140
141        final DetailAST slist = ast.findFirstToken(TokenTypes.SLIST);
142
143        if (slist != null && !isTerminated(slist, true, true)
144            && !hasFallThroughComment(ast, nextGroup)) {
145            if (isLastGroup) {
146                log(ast, MSG_FALL_THROUGH_LAST);
147            }
148            else {
149                log(nextGroup, MSG_FALL_THROUGH);
150            }
151        }
152    }
153
154    /**
155     * Checks if a given subtree terminated by return, throw or,
156     * if allowed break, continue.
157     * @param ast root of given subtree
158     * @param useBreak should we consider break as terminator.
159     * @param useContinue should we consider continue as terminator.
160     * @return true if the subtree is terminated.
161     */
162    private boolean isTerminated(final DetailAST ast, boolean useBreak,
163                                 boolean useContinue) {
164        boolean terminated;
165
166        switch (ast.getType()) {
167            case TokenTypes.LITERAL_RETURN:
168            case TokenTypes.LITERAL_THROW:
169                terminated = true;
170                break;
171            case TokenTypes.LITERAL_BREAK:
172                terminated = useBreak;
173                break;
174            case TokenTypes.LITERAL_CONTINUE:
175                terminated = useContinue;
176                break;
177            case TokenTypes.SLIST:
178                terminated = checkSlist(ast, useBreak, useContinue);
179                break;
180            case TokenTypes.LITERAL_IF:
181                terminated = checkIf(ast, useBreak, useContinue);
182                break;
183            case TokenTypes.LITERAL_FOR:
184            case TokenTypes.LITERAL_WHILE:
185            case TokenTypes.LITERAL_DO:
186                terminated = checkLoop(ast);
187                break;
188            case TokenTypes.LITERAL_TRY:
189                terminated = checkTry(ast, useBreak, useContinue);
190                break;
191            case TokenTypes.LITERAL_SWITCH:
192                terminated = checkSwitch(ast, useContinue);
193                break;
194            default:
195                terminated = false;
196        }
197        return terminated;
198    }
199
200    /**
201     * Checks if a given SLIST terminated by return, throw or,
202     * if allowed break, continue.
203     * @param slistAst SLIST to check
204     * @param useBreak should we consider break as terminator.
205     * @param useContinue should we consider continue as terminator.
206     * @return true if SLIST is terminated.
207     */
208    private boolean checkSlist(final DetailAST slistAst, boolean useBreak,
209                               boolean useContinue) {
210        DetailAST lastStmt = slistAst.getLastChild();
211
212        if (lastStmt.getType() == TokenTypes.RCURLY) {
213            lastStmt = lastStmt.getPreviousSibling();
214        }
215
216        return lastStmt != null
217            && isTerminated(lastStmt, useBreak, useContinue);
218    }
219
220    /**
221     * Checks if a given IF terminated by return, throw or,
222     * if allowed break, continue.
223     * @param ast IF to check
224     * @param useBreak should we consider break as terminator.
225     * @param useContinue should we consider continue as terminator.
226     * @return true if IF is terminated.
227     */
228    private boolean checkIf(final DetailAST ast, boolean useBreak,
229                            boolean useContinue) {
230        final DetailAST thenStmt = ast.findFirstToken(TokenTypes.RPAREN)
231                .getNextSibling();
232        final DetailAST elseStmt = thenStmt.getNextSibling();
233        boolean isTerminated = isTerminated(thenStmt, useBreak, useContinue);
234
235        if (isTerminated && elseStmt != null) {
236            isTerminated = isTerminated(elseStmt.getFirstChild(),
237                useBreak, useContinue);
238        }
239        else if (elseStmt == null) {
240            isTerminated = false;
241        }
242        return isTerminated;
243    }
244
245    /**
246     * Checks if a given loop terminated by return, throw or,
247     * if allowed break, continue.
248     * @param ast loop to check
249     * @return true if loop is terminated.
250     */
251    private boolean checkLoop(final DetailAST ast) {
252        DetailAST loopBody;
253        if (ast.getType() == TokenTypes.LITERAL_DO) {
254            final DetailAST lparen = ast.findFirstToken(TokenTypes.DO_WHILE);
255            loopBody = lparen.getPreviousSibling();
256        }
257        else {
258            final DetailAST rparen = ast.findFirstToken(TokenTypes.RPAREN);
259            loopBody = rparen.getNextSibling();
260        }
261        return isTerminated(loopBody, false, false);
262    }
263
264    /**
265     * Checks if a given try/catch/finally block terminated by return, throw or,
266     * if allowed break, continue.
267     * @param ast loop to check
268     * @param useBreak should we consider break as terminator.
269     * @param useContinue should we consider continue as terminator.
270     * @return true if try/catch/finally block is terminated.
271     */
272    private boolean checkTry(final DetailAST ast, boolean useBreak,
273                             boolean useContinue) {
274        final DetailAST finalStmt = ast.getLastChild();
275        boolean isTerminated = false;
276        if (finalStmt.getType() == TokenTypes.LITERAL_FINALLY) {
277            isTerminated = isTerminated(finalStmt.findFirstToken(TokenTypes.SLIST),
278                                useBreak, useContinue);
279        }
280
281        if (!isTerminated) {
282            isTerminated = isTerminated(ast.getFirstChild(),
283                    useBreak, useContinue);
284
285            DetailAST catchStmt = ast.findFirstToken(TokenTypes.LITERAL_CATCH);
286            while (catchStmt != null
287                    && isTerminated
288                    && catchStmt.getType() == TokenTypes.LITERAL_CATCH) {
289                final DetailAST catchBody =
290                        catchStmt.findFirstToken(TokenTypes.SLIST);
291                isTerminated = isTerminated(catchBody, useBreak, useContinue);
292                catchStmt = catchStmt.getNextSibling();
293            }
294        }
295        return isTerminated;
296    }
297
298    /**
299     * Checks if a given switch terminated by return, throw or,
300     * if allowed break, continue.
301     * @param literalSwitchAst loop to check
302     * @param useContinue should we consider continue as terminator.
303     * @return true if switch is terminated.
304     */
305    private boolean checkSwitch(final DetailAST literalSwitchAst, boolean useContinue) {
306        DetailAST caseGroup = literalSwitchAst.findFirstToken(TokenTypes.CASE_GROUP);
307        boolean isTerminated = caseGroup != null;
308        while (isTerminated && caseGroup.getType() != TokenTypes.RCURLY) {
309            final DetailAST caseBody =
310                caseGroup.findFirstToken(TokenTypes.SLIST);
311            isTerminated = caseBody != null && isTerminated(caseBody, false, useContinue);
312            caseGroup = caseGroup.getNextSibling();
313        }
314        return isTerminated;
315    }
316
317    /**
318     * Determines if the fall through case between {@code currentCase} and
319     * {@code nextCase} is relieved by a appropriate comment.
320     *
321     * @param currentCase AST of the case that falls through to the next case.
322     * @param nextCase AST of the next case.
323     * @return True if a relief comment was found
324     */
325    private boolean hasFallThroughComment(DetailAST currentCase, DetailAST nextCase) {
326        boolean allThroughComment = false;
327        final int endLineNo = nextCase.getLineNo();
328        final int endColNo = nextCase.getColumnNo();
329
330        /*
331         * Remember: The lines number returned from the AST is 1-based, but
332         * the lines number in this array are 0-based. So you will often
333         * see a "lineNo-1" etc.
334         */
335        final String[] lines = getLines();
336
337        /*
338         * Handle:
339         *    case 1:
340         *    /+ FALLTHRU +/ case 2:
341         *    ....
342         * and
343         *    switch(i) {
344         *    default:
345         *    /+ FALLTHRU +/}
346         */
347        final String linePart = lines[endLineNo - 1].substring(0, endColNo);
348        if (matchesComment(regExp, linePart, endLineNo)) {
349            allThroughComment = true;
350        }
351        else {
352            /*
353             * Handle:
354             *    case 1:
355             *    .....
356             *    // FALLTHRU
357             *    case 2:
358             *    ....
359             * and
360             *    switch(i) {
361             *    default:
362             *    // FALLTHRU
363             *    }
364             */
365            final int startLineNo = currentCase.getLineNo();
366            for (int i = endLineNo - 2; i > startLineNo - 1; i--) {
367                if (!lines[i].trim().isEmpty()) {
368                    allThroughComment = matchesComment(regExp, lines[i], i + 1);
369                    break;
370                }
371            }
372        }
373        return allThroughComment;
374    }
375
376    /**
377     * Does a regular expression match on the given line and checks that a
378     * possible match is within a comment.
379     * @param pattern The regular expression pattern to use.
380     * @param line The line of test to do the match on.
381     * @param lineNo The line number in the file.
382     * @return True if a match was found inside a comment.
383     */
384    private boolean matchesComment(Pattern pattern, String line, int lineNo
385    ) {
386        final Matcher matcher = pattern.matcher(line);
387
388        final boolean hit = matcher.find();
389
390        if (hit) {
391            final int startMatch = matcher.start();
392            // -1 because it returns the char position beyond the match
393            final int endMatch = matcher.end() - 1;
394            return getFileContents().hasIntersectionWithComment(lineNo,
395                    startMatch, lineNo, endMatch);
396        }
397        return false;
398    }
399}