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     * Creates a new {@code EmptyCatchBlockCheck} instance.
075     */
076    public EmptyCatchBlockCheck() {
077        // no code by default
078    }
079
080    /**
081     * Setter to specify the RegExp for the name of the variable associated with exception.
082     * If check meets variable name matching specified value - empty block is suppressed.
083     *
084     * @param exceptionVariablePattern
085     *        pattern of exception's variable name.
086     * @since 6.4
087     */
088    public void setExceptionVariableName(Pattern exceptionVariablePattern) {
089        exceptionVariableName = exceptionVariablePattern;
090    }
091
092    /**
093     * Setter to 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     * @param commentPattern
098     *        pattern of comment.
099     * @since 6.4
100     */
101    public void setCommentFormat(Pattern commentPattern) {
102        commentFormat = commentPattern;
103    }
104
105    @Override
106    public int[] getDefaultTokens() {
107        return getRequiredTokens();
108    }
109
110    @Override
111    public int[] getAcceptableTokens() {
112        return getRequiredTokens();
113    }
114
115    @Override
116    public int[] getRequiredTokens() {
117        return new int[] {
118            TokenTypes.LITERAL_CATCH,
119        };
120    }
121
122    @Override
123    public boolean isCommentNodesRequired() {
124        return true;
125    }
126
127    @Override
128    public void visitToken(DetailAST ast) {
129        visitCatchBlock(ast);
130    }
131
132    /**
133     * Visits catch ast node, if it is empty catch block - checks it according to
134     *  Check's options. If exception's variable name or comment inside block are matching
135     *   specified regexp - skips from consideration, else - puts violation.
136     *
137     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
138     */
139    private void visitCatchBlock(DetailAST catchAst) {
140        if (isEmptyCatchBlock(catchAst)) {
141            final String commentContent = getCommentFirstLine(catchAst);
142            if (isVerifiable(catchAst, commentContent)) {
143                log(catchAst.findFirstToken(TokenTypes.SLIST), MSG_KEY_CATCH_BLOCK_EMPTY);
144            }
145        }
146    }
147
148    /**
149     * Gets the first line of comment in catch block. If comment is single-line -
150     *  returns it fully, else if comment is multi-line - returns the first line.
151     *
152     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
153     * @return the first line of comment in catch block, "" if no comment was found.
154     */
155    private static String getCommentFirstLine(DetailAST catchAst) {
156        final DetailAST slistToken = catchAst.getLastChild();
157        final DetailAST firstElementInBlock = slistToken.getFirstChild();
158        String commentContent = "";
159        if (firstElementInBlock.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
160            commentContent = firstElementInBlock.getFirstChild().getText();
161        }
162        else if (firstElementInBlock.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
163            commentContent = firstElementInBlock.getFirstChild().getText();
164            final String[] lines = LINE_END_PATTERN.split(commentContent, -1);
165            for (String line : lines) {
166                if (!line.isEmpty()) {
167                    commentContent = line;
168                    break;
169                }
170            }
171        }
172        return commentContent;
173    }
174
175    /**
176     * Checks if current empty catch block is verifiable according to Check's options
177     *  (exception's variable name and comment format are both in consideration).
178     *
179     * @param emptyCatchAst empty catch {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH} block.
180     * @param commentContent text of comment.
181     * @return true if empty catch block is verifiable by Check.
182     */
183    private boolean isVerifiable(DetailAST emptyCatchAst, String commentContent) {
184        final String variableName = getExceptionVariableName(emptyCatchAst);
185        final boolean isMatchingVariableName = exceptionVariableName
186                .matcher(variableName).find();
187        final boolean isMatchingCommentContent = !commentContent.isEmpty()
188                 && commentFormat.matcher(commentContent).find();
189        return !isMatchingVariableName && !isMatchingCommentContent;
190    }
191
192    /**
193     * Checks if catch block is empty or contains only comments.
194     *
195     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
196     * @return true if catch block is empty.
197     */
198    private static boolean isEmptyCatchBlock(DetailAST catchAst) {
199        boolean result = true;
200        final DetailAST slistToken = catchAst.findFirstToken(TokenTypes.SLIST);
201        DetailAST catchBlockStmt = slistToken.getFirstChild();
202        while (catchBlockStmt.getType() != TokenTypes.RCURLY) {
203            if (catchBlockStmt.getType() != TokenTypes.SINGLE_LINE_COMMENT
204                 && catchBlockStmt.getType() != TokenTypes.BLOCK_COMMENT_BEGIN) {
205                result = false;
206                break;
207            }
208            catchBlockStmt = catchBlockStmt.getNextSibling();
209        }
210        return result;
211    }
212
213    /**
214     * Gets variable's name associated with exception.
215     *
216     * @param catchAst {@link TokenTypes#LITERAL_CATCH LITERAL_CATCH}
217     * @return Variable's name associated with exception.
218     */
219    private static String getExceptionVariableName(DetailAST catchAst) {
220        final DetailAST parameterDef = NullUtil.notNull(
221                catchAst.findFirstToken(TokenTypes.PARAMETER_DEF));
222        final DetailAST variableName = TokenUtil.getIdent(parameterDef);
223        return variableName.getText();
224    }
225
226}