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