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