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