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.coding;
021
022import java.util.ArrayDeque;
023import java.util.Deque;
024import java.util.Optional;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
031
032/**
033 * <div>
034 * Ensures that catch parameters that are not used are declared as an unnamed variable.
035 * </div>
036 *
037 * <p>
038 * Rationale:
039 * </p>
040 * <ul>
041 *     <li>
042 *         Improves code readability by clearly indicating which parameters are unused.
043 *     </li>
044 *     <li>
045 *         Follows Java conventions for denoting unused parameters with an underscore ({@code _}).
046 *     </li>
047 * </ul>
048 *
049 * <p>
050 * See the <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
051 * Java Language Specification</a> for more information about unnamed variables.
052 * </p>
053 *
054 * <p>
055 * <b>Attention</b>: This check should be activated only on source code
056 * that is compiled by jdk21 or higher;
057 * unnamed catch parameters came out as the first preview in Java 21.
058 * </p>
059 *
060 * <p>
061 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
062 * </p>
063 *
064 * <p>
065 * Violation Message Keys:
066 * </p>
067 * <ul>
068 * <li>
069 * {@code unused.catch.parameter}
070 * </li>
071 * </ul>
072 *
073 * @since 10.18.0
074 *
075 */
076
077@FileStatefulCheck
078public class UnusedCatchParameterShouldBeUnnamedCheck extends AbstractCheck {
079
080    /**
081     * A key is pointing to the warning message text in "messages.properties"
082     * file.
083     */
084    public static final String MSG_UNUSED_CATCH_PARAMETER = "unused.catch.parameter";
085
086    /**
087     * Invalid parents of the catch parameter identifier.
088     */
089    private static final int[] INVALID_CATCH_PARAM_IDENT_PARENTS = {
090        TokenTypes.DOT,
091        TokenTypes.LITERAL_NEW,
092        TokenTypes.METHOD_CALL,
093        TokenTypes.TYPE,
094    };
095
096    /**
097     * Keeps track of the catch parameters in a block.
098     */
099    private final Deque<CatchParameterDetails> catchParameters = new ArrayDeque<>();
100
101    @Override
102    public int[] getDefaultTokens() {
103        return getRequiredTokens();
104    }
105
106    @Override
107    public int[] getAcceptableTokens() {
108        return getRequiredTokens();
109    }
110
111    @Override
112    public int[] getRequiredTokens() {
113        return new int[] {
114            TokenTypes.LITERAL_CATCH,
115            TokenTypes.IDENT,
116        };
117    }
118
119    @Override
120    public void beginTree(DetailAST rootAST) {
121        catchParameters.clear();
122    }
123
124    @Override
125    public void visitToken(DetailAST ast) {
126        if (ast.getType() == TokenTypes.LITERAL_CATCH) {
127            final CatchParameterDetails catchParameter = new CatchParameterDetails(ast);
128            catchParameters.push(catchParameter);
129        }
130        else if (isCatchParameterIdentifierCandidate(ast) && !isLeftHandOfAssignment(ast)) {
131            // we do not count reassignment as usage
132            catchParameters.stream()
133                    .filter(parameter -> parameter.getName().equals(ast.getText()))
134                    .findFirst()
135                    .ifPresent(CatchParameterDetails::registerAsUsed);
136        }
137    }
138
139    @Override
140    public void leaveToken(DetailAST ast) {
141        if (ast.getType() == TokenTypes.LITERAL_CATCH) {
142            final Optional<CatchParameterDetails> unusedCatchParameter =
143                    Optional.ofNullable(catchParameters.peek())
144                            .filter(parameter -> !parameter.isUsed())
145                            .filter(parameter -> !"_".equals(parameter.getName()));
146
147            unusedCatchParameter.ifPresent(parameter -> {
148                log(parameter.getParameterDefinition(),
149                        MSG_UNUSED_CATCH_PARAMETER,
150                        parameter.getName());
151            });
152            catchParameters.pop();
153        }
154    }
155
156    /**
157     * Visit ast of type {@link TokenTypes#IDENT}
158     * and check if it is a candidate for a catch parameter identifier.
159     *
160     * @param identifierAst token representing {@link TokenTypes#IDENT}
161     * @return true if the given {@link TokenTypes#IDENT} could be a catch parameter identifier
162     */
163    private static boolean isCatchParameterIdentifierCandidate(DetailAST identifierAst) {
164        // we should ignore the ident if it is in the exception declaration
165        return identifierAst.getParent().getParent().getType() != TokenTypes.LITERAL_CATCH
166            && (!TokenUtil.isOfType(identifierAst.getParent(), INVALID_CATCH_PARAM_IDENT_PARENTS)
167                 || isMethodInvocation(identifierAst));
168    }
169
170    /**
171     * Check if the given {@link TokenTypes#IDENT} is a child of a dot operator
172     * and is a candidate for catch parameter.
173     *
174     * @param identAst token representing {@link TokenTypes#IDENT}
175     * @return true if the given {@link TokenTypes#IDENT} is a child of a dot operator
176     *     and a candidate for catch parameter.
177     */
178    private static boolean isMethodInvocation(DetailAST identAst) {
179        final DetailAST parent = identAst.getParent();
180        return parent.getType() == TokenTypes.DOT
181                && identAst.equals(parent.getFirstChild());
182    }
183
184    /**
185     * Check if the given {@link TokenTypes#IDENT} is a left hand side value.
186     *
187     * @param identAst token representing {@link TokenTypes#IDENT}
188     * @return true if the given {@link TokenTypes#IDENT} is a left hand side value.
189     */
190    private static boolean isLeftHandOfAssignment(DetailAST identAst) {
191        final DetailAST parent = identAst.getParent();
192        return parent.getType() == TokenTypes.ASSIGN
193                && !identAst.equals(parent.getLastChild());
194    }
195
196    /**
197     * Maintains information about the catch parameter.
198     */
199    private static final class CatchParameterDetails {
200
201        /**
202         * The name of the catch parameter.
203         */
204        private final String name;
205
206        /**
207         * Ast of type {@link TokenTypes#PARAMETER_DEF} to use it when logging.
208         */
209        private final DetailAST parameterDefinition;
210
211        /**
212         * Is the variable used.
213         */
214        private boolean used;
215
216        /**
217         * Create a new catch parameter instance.
218         *
219         * @param enclosingCatchClause ast of type {@link TokenTypes#LITERAL_CATCH}
220         */
221        private CatchParameterDetails(DetailAST enclosingCatchClause) {
222            parameterDefinition =
223                    enclosingCatchClause.findFirstToken(TokenTypes.PARAMETER_DEF);
224            name = parameterDefinition.findFirstToken(TokenTypes.IDENT).getText();
225        }
226
227        /**
228         * Register the catch parameter as used.
229         */
230        private void registerAsUsed() {
231            used = true;
232        }
233
234        /**
235         * Get the name of the catch parameter.
236         *
237         * @return the name of the catch parameter
238         */
239        private String getName() {
240            return name;
241        }
242
243        /**
244         * Check if the catch parameter is used.
245         *
246         * @return true if the catch parameter is used
247         */
248        private boolean isUsed() {
249            return used;
250        }
251
252        /**
253         * Get the parameter definition token of the catch parameter
254         * represented by ast of type {@link TokenTypes#PARAMETER_DEF}.
255         *
256         * @return the ast of type {@link TokenTypes#PARAMETER_DEF}
257         */
258        private DetailAST getParameterDefinition() {
259            return parameterDefinition;
260        }
261    }
262}