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