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