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