001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.blocks;
021
022import java.util.regex.Pattern;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028
029/**
030 * <div>
031 * Checks for empty catch blocks.
032 * By default, check allows empty catch block with any comment inside.
033 * </div>
034 *
035 * <p>
036 * There are two options to make validation more precise: <b>exceptionVariableName</b> and
037 * <b>commentFormat</b>.
038 * If both options are specified - they are applied by <b>any of them is matching</b>.
039 * </p>
040 * <ul>
041 * <li>
042 * Property {@code commentFormat} - Specify the RegExp for the first comment inside empty
043 * catch block. If check meets comment inside empty catch block matching specified format
044 * - empty block is suppressed. If it is multi-line comment - only its first line is analyzed.
045 * Type is {@code java.util.regex.Pattern}.
046 * Default value is {@code ".*"}.
047 * </li>
048 * <li>
049 * Property {@code exceptionVariableName} - Specify the RegExp for the name of the variable
050 * associated with exception. If check meets variable name matching specified value - empty
051 * block is suppressed.
052 * Type is {@code java.util.regex.Pattern}.
053 * Default value is {@code "^$"}.
054 * </li>
055 * </ul>
056 *
057 * <p>
058 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
059 * </p>
060 *
061 * <p>
062 * Violation Message Keys:
063 * </p>
064 * <ul>
065 * <li>
066 * {@code catch.block.empty}
067 * </li>
068 * </ul>
069 *
070 * @since 6.4
071 */
072@StatelessCheck
073public class EmptyCatchBlockCheck extends AbstractCheck {
074
075    /**
076     * A key is pointing to the warning message text in "messages.properties"
077     * file.
078     */
079    public static final String MSG_KEY_CATCH_BLOCK_EMPTY = "catch.block.empty";
080
081    /**
082     * A pattern to split on line ends.
083     */
084    private static final Pattern LINE_END_PATTERN = Pattern.compile("\\r?+\\n|\\r");
085
086    /**
087     * Specify the RegExp for the name of the variable associated with exception.
088     * If check meets variable name matching specified value - empty block is suppressed.
089     */
090    private Pattern exceptionVariableName = Pattern.compile("^$");
091
092    /**
093     * Specify the RegExp for the first comment inside empty catch block.
094     * If check meets comment inside empty catch block matching specified format - empty
095     * block is suppressed. If it is multi-line comment - only its first line is analyzed.
096     */
097    private Pattern commentFormat = Pattern.compile(".*");
098
099    /**
100     * Setter to specify the RegExp for the name of the variable associated with exception.
101     * If check meets variable name matching specified value - empty block is suppressed.
102     *
103     * @param exceptionVariablePattern
104     *        pattern of exception's variable name.
105     * @since 6.4
106     */
107    public void setExceptionVariableName(Pattern exceptionVariablePattern) {
108        exceptionVariableName = exceptionVariablePattern;
109    }
110
111    /**
112     * Setter to specify the RegExp for the first comment inside empty catch block.
113     * If check meets comment inside empty catch block matching specified format - empty
114     * block is suppressed. If it is multi-line comment - only its first line is analyzed.
115     *
116     * @param commentPattern
117     *        pattern of comment.
118     * @since 6.4
119     */
120    public void setCommentFormat(Pattern commentPattern) {
121        commentFormat = commentPattern;
122    }
123
124    @Override
125    public int[] getDefaultTokens() {
126        return getRequiredTokens();
127    }
128
129    @Override
130    public int[] getAcceptableTokens() {
131        return getRequiredTokens();
132    }
133
134    @Override
135    public int[] getRequiredTokens() {
136        return new int[] {
137            TokenTypes.LITERAL_CATCH,
138        };
139    }
140
141    @Override
142    public boolean isCommentNodesRequired() {
143        return true;
144    }
145
146    @Override
147    public void visitToken(DetailAST ast) {
148        visitCatchBlock(ast);
149    }
150
151    /**
152     * Visits catch ast node, if it is empty catch block - checks it according to
153     *  Check's options. If exception's variable name or comment inside block are matching
154     *   specified regexp - skips from consideration, else - puts violation.
155     *
156     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
157     */
158    private void visitCatchBlock(DetailAST catchAst) {
159        if (isEmptyCatchBlock(catchAst)) {
160            final String commentContent = getCommentFirstLine(catchAst);
161            if (isVerifiable(catchAst, commentContent)) {
162                log(catchAst.findFirstToken(TokenTypes.SLIST), MSG_KEY_CATCH_BLOCK_EMPTY);
163            }
164        }
165    }
166
167    /**
168     * Gets the first line of comment in catch block. If comment is single-line -
169     *  returns it fully, else if comment is multi-line - returns the first line.
170     *
171     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
172     * @return the first line of comment in catch block, "" if no comment was found.
173     */
174    private static String getCommentFirstLine(DetailAST catchAst) {
175        final DetailAST slistToken = catchAst.getLastChild();
176        final DetailAST firstElementInBlock = slistToken.getFirstChild();
177        String commentContent = "";
178        if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
179            commentContent = firstElementInBlock.getFirstChild().getText();
180        }
181        else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
182            commentContent = firstElementInBlock.getFirstChild().getText();
183            final String[] lines = LINE_END_PATTERN.split(commentContent);
184            for (String line : lines) {
185                if (!line.isEmpty()) {
186                    commentContent = line;
187                    break;
188                }
189            }
190        }
191        return commentContent;
192    }
193
194    /**
195     * Checks if current empty catch block is verifiable according to Check's options
196     *  (exception's variable name and comment format are both in consideration).
197     *
198     * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block.
199     * @param commentContent text of comment.
200     * @return true if empty catch block is verifiable by Check.
201     */
202    private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) {
203        final String variableName = getExceptionVariableName(emptyCatchAst);
204        final boolean isMatchingVariableName = exceptionVariableName
205                .matcher(variableName).find();
206        final boolean isMatchingCommentContent = !commentContent.isEmpty()
207                 && commentFormat.matcher(commentContent).find();
208        return !isMatchingVariableName && !isMatchingCommentContent;
209    }
210
211    /**
212     * Checks if catch block is empty or contains only comments.
213     *
214     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
215     * @return true if catch block is empty.
216     */
217    private static boolean isEmptyCatchBlock(DetailAST catchAst) {
218        boolean result = true;
219        final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST);
220        DetailAST catchBlockStmt = slistToken.getFirstChild();
221        while (catchBlockStmt.getType() != TokenTypes.RCURLY) {
222            if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT
223                 && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) {
224                result = false;
225                break;
226            }
227            catchBlockStmt = catchBlockStmt.getNextSibling();
228        }
229        return result;
230    }
231
232    /**
233     * Gets variable's name associated with exception.
234     *
235     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
236     * @return Variable's name associated with exception.
237     */
238    private static String getExceptionVariableName(DetailAST catchAst) {
239        final DetailAST parameterDef = catchAst.findFirstToken(TokenTypes.PARAMETER_DEF);
240        final DetailAST variableName = parameterDef.findFirstToken(TokenTypes.IDENT);
241        return variableName.getText();
242    }
243
244}