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   * @since 10.18.0
61   */
62  @FileStatefulCheck
63  public class UnusedLambdaParameterShouldBeUnnamedCheck extends AbstractCheck {
64  
65      /**
66       * A key is pointing to the warning message text in "messages.properties"
67       * file.
68       */
69      public static final String MSG_UNUSED_LAMBDA_PARAMETER = "unused.lambda.parameter";
70  
71      /**
72       * Invalid parents of the lambda parameter identifier.
73       * These are tokens that can not be parents for a lambda
74       * parameter identifier.
75       */
76      private static final int[] INVALID_LAMBDA_PARAM_IDENT_PARENTS = {
77          TokenTypes.DOT,
78          TokenTypes.LITERAL_NEW,
79          TokenTypes.METHOD_CALL,
80          TokenTypes.TYPE,
81      };
82  
83      /**
84       * Keeps track of the lambda parameters in a block.
85       */
86      private final Deque<LambdaParameterDetails> lambdaParameters = new ArrayDeque<>();
87  
88      @Override
89      public int[] getDefaultTokens() {
90          return getRequiredTokens();
91      }
92  
93      @Override
94      public int[] getAcceptableTokens() {
95          return getRequiredTokens();
96      }
97  
98      @Override
99      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 }