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