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