001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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 lambda 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>: Unnamed variables are available as a preview feature in Java 21,
056 * and became an official part of the language in Java 22.
057 * This check should be activated only on source code which meets those requirements.
058 * </p>
059 *
060 * @since 10.18.0
061 */
062@FileStatefulCheck
063public class UnusedLambdaParameterShouldBeUnnamedCheck extends AbstractCheck {
064
065    /**
066     * A key is pointing to the warning message text in "messages.properties"
067     * file.
068     */
069    public static final String MSG_UNUSED_LAMBDA_PARAMETER = "unused.lambda.parameter";
070
071    /**
072     * Invalid parents of the lambda parameter identifier.
073     * These are tokens that can not be parents for a lambda
074     * parameter identifier.
075     */
076    private static final int[] INVALID_LAMBDA_PARAM_IDENT_PARENTS = {
077        TokenTypes.DOT,
078        TokenTypes.LITERAL_NEW,
079        TokenTypes.METHOD_CALL,
080        TokenTypes.TYPE,
081    };
082
083    /**
084     * Keeps track of the lambda parameters in a block.
085     */
086    private final Deque<LambdaParameterDetails> lambdaParameters = new ArrayDeque<>();
087
088    @Override
089    public int[] getDefaultTokens() {
090        return getRequiredTokens();
091    }
092
093    @Override
094    public int[] getAcceptableTokens() {
095        return getRequiredTokens();
096    }
097
098    @Override
099    public int[] getRequiredTokens() {
100        return new int[] {
101            TokenTypes.LAMBDA,
102            TokenTypes.IDENT,
103        };
104    }
105
106    @Override
107    public void beginTree(DetailAST rootAST) {
108        lambdaParameters.clear();
109    }
110
111    @Override
112    public void visitToken(DetailAST ast) {
113        if (ast.getType() == TokenTypes.LAMBDA) {
114            final DetailAST parameters = ast.findFirstToken(TokenTypes.PARAMETERS);
115            if (parameters != null) {
116                // we have multiple lambda parameters
117                TokenUtil.forEachChild(parameters, TokenTypes.PARAMETER_DEF, parameter -> {
118                    final DetailAST identifierAst = parameter.findFirstToken(TokenTypes.IDENT);
119                    final LambdaParameterDetails lambdaParameter =
120                            new LambdaParameterDetails(ast, identifierAst);
121                    lambdaParameters.push(lambdaParameter);
122                });
123            }
124            else if (ast.getChildCount() != 0) {
125                // we are not switch rule and have a single parameter
126                final LambdaParameterDetails lambdaParameter =
127                            new LambdaParameterDetails(ast, ast.findFirstToken(TokenTypes.IDENT));
128                lambdaParameters.push(lambdaParameter);
129            }
130        }
131        else if (isLambdaParameterIdentifierCandidate(ast) && !isLeftHandOfAssignment(ast)) {
132            // we do not count reassignment as usage
133            lambdaParameters.stream()
134                    .filter(parameter -> parameter.getName().equals(ast.getText()))
135                    .findFirst()
136                    .ifPresent(LambdaParameterDetails::registerAsUsed);
137        }
138    }
139
140    @Override
141    public void leaveToken(DetailAST ast) {
142        while (lambdaParameters.peek() != null
143                    && ast.equals(lambdaParameters.peek().enclosingLambda)) {
144
145            final Optional<LambdaParameterDetails> unusedLambdaParameter =
146                    Optional.ofNullable(lambdaParameters.peek())
147                            .filter(parameter -> !parameter.isUsed())
148                            .filter(parameter -> !"_".equals(parameter.getName()));
149
150            unusedLambdaParameter.ifPresent(parameter -> {
151                log(parameter.getIdentifierAst(),
152                        MSG_UNUSED_LAMBDA_PARAMETER,
153                        parameter.getName());
154            });
155            lambdaParameters.pop();
156        }
157    }
158
159    /**
160     * Visit ast of type {@link TokenTypes#IDENT}
161     * and check if it is a candidate for a lambda parameter identifier.
162     *
163     * @param identifierAst token representing {@link TokenTypes#IDENT}
164     * @return true if the given {@link TokenTypes#IDENT} could be a lambda parameter identifier
165     */
166    private static boolean isLambdaParameterIdentifierCandidate(DetailAST identifierAst) {
167        // we should ignore the ident if it is in the lambda parameters declaration
168        final boolean isLambdaParameterDeclaration =
169                identifierAst.getParent().getType() == TokenTypes.LAMBDA
170                    || identifierAst.getParent().getType() == TokenTypes.PARAMETER_DEF;
171
172        return !isLambdaParameterDeclaration
173                 && (hasValidParentToken(identifierAst) || isMethodInvocation(identifierAst));
174    }
175
176    /**
177     * Check if the given {@link TokenTypes#IDENT} has a valid parent token.
178     * A valid parent token is a token that can be a parent for a lambda parameter identifier.
179     *
180     * @param identifierAst token representing {@link TokenTypes#IDENT}
181     * @return true if the given {@link TokenTypes#IDENT} has a valid parent token
182     */
183    private static boolean hasValidParentToken(DetailAST identifierAst) {
184        return !TokenUtil.isOfType(identifierAst.getParent(), INVALID_LAMBDA_PARAM_IDENT_PARENTS);
185    }
186
187    /**
188     * Check if the given {@link TokenTypes#IDENT} is a child of a dot operator
189     * and is a candidate for lambda parameter.
190     *
191     * @param identAst token representing {@link TokenTypes#IDENT}
192     * @return true if the given {@link TokenTypes#IDENT} is a child of a dot operator
193     *     and a candidate for lambda parameter.
194     */
195    private static boolean isMethodInvocation(DetailAST identAst) {
196        final DetailAST parent = identAst.getParent();
197        return parent.getType() == TokenTypes.DOT
198                && identAst.equals(parent.getFirstChild());
199    }
200
201    /**
202     * Check if the given {@link TokenTypes#IDENT} is a left hand side value.
203     *
204     * @param identAst token representing {@link TokenTypes#IDENT}
205     * @return true if the given {@link TokenTypes#IDENT} is a left hand side value.
206     */
207    private static boolean isLeftHandOfAssignment(DetailAST identAst) {
208        final DetailAST parent = identAst.getParent();
209        return parent.getType() == TokenTypes.ASSIGN
210                && !identAst.equals(parent.getLastChild());
211    }
212
213    /**
214     * Maintains information about the lambda parameter.
215     */
216    private static final class LambdaParameterDetails {
217
218        /**
219         * Ast of type {@link TokenTypes#LAMBDA} enclosing the lambda
220         * parameter.
221         */
222        private final DetailAST enclosingLambda;
223
224        /**
225         * Ast of type {@link TokenTypes#IDENT} of the given
226         * lambda parameter.
227         */
228        private final DetailAST identifierAst;
229
230        /**
231         * Is the variable used.
232         */
233        private boolean used;
234
235        /**
236         * Create a new lambda parameter instance.
237         *
238         * @param enclosingLambda ast of type {@link TokenTypes#LAMBDA}
239         * @param identifierAst ast of type {@link TokenTypes#IDENT}
240         */
241        private LambdaParameterDetails(DetailAST enclosingLambda, DetailAST identifierAst) {
242            this.enclosingLambda = enclosingLambda;
243            this.identifierAst = identifierAst;
244        }
245
246        /**
247         * Register the lambda parameter as used.
248         */
249        private void registerAsUsed() {
250            used = true;
251        }
252
253        /**
254         * Get the name of the lambda parameter.
255         *
256         * @return the name of the lambda parameter
257         */
258        private String getName() {
259            return identifierAst.getText();
260        }
261
262        /**
263         * Get ast of type {@link TokenTypes#IDENT} of the given
264         * lambda parameter.
265         *
266         * @return ast of type {@link TokenTypes#IDENT} of the given lambda parameter
267         */
268        private DetailAST getIdentifierAst() {
269            return identifierAst;
270        }
271
272        /**
273         * Check if the lambda parameter is used.
274         *
275         * @return true if the lambda parameter is used
276         */
277        private boolean isUsed() {
278            return used;
279        }
280    }
281}