View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.ArrayDeque;
23  import java.util.Deque;
24  import java.util.Optional;
25  
26  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
31  
32  /**
33   * <div>
34   * Ensures that lambda parameters that are not used are declared as an unnamed variable.
35   * </div>
36   *
37   * <p>
38   * Rationale:
39   * </p>
40   * <ul>
41   *     <li>
42   *         Improves code readability by clearly indicating which parameters are unused.
43   *     </li>
44   *     <li>
45   *         Follows Java conventions for denoting unused parameters with an underscore ({@code _}).
46   *     </li>
47   * </ul>
48   *
49   * <p>
50   * See the <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
51   * Java Language Specification</a> for more information about unnamed variables.
52   * </p>
53   *
54   * <p>
55   * <b>Attention</b>: Unnamed variables are available as a preview feature in Java 21,
56   * and became an official part of the language in Java 22.
57   * This check should be activated only on source code which meets those requirements.
58   * </p>
59   *
60   * <p>
61   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
62   * </p>
63   *
64   * <p>
65   * Violation Message Keys:
66   * </p>
67   * <ul>
68   * <li>
69   * {@code unused.lambda.parameter}
70   * </li>
71   * </ul>
72   *
73   * @since 10.18.0
74   */
75  @FileStatefulCheck
76  public class UnusedLambdaParameterShouldBeUnnamedCheck extends AbstractCheck {
77  
78      /**
79       * A key is pointing to the warning message text in "messages.properties"
80       * file.
81       */
82      public static final String MSG_UNUSED_LAMBDA_PARAMETER = "unused.lambda.parameter";
83  
84      /**
85       * Invalid parents of the lambda parameter identifier.
86       * These are tokens that can not be parents for a lambda
87       * parameter identifier.
88       */
89      private static final int[] INVALID_LAMBDA_PARAM_IDENT_PARENTS = {
90          TokenTypes.DOT,
91          TokenTypes.LITERAL_NEW,
92          TokenTypes.METHOD_CALL,
93          TokenTypes.TYPE,
94      };
95  
96      /**
97       * Keeps track of the lambda parameters in a block.
98       */
99      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 }