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