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