View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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   * <p>
34   * Ensures that lambda parameters that are not used are declared as an unnamed variable.
35   * </p>
36   * <p>
37   * Rationale:
38   * </p>
39   * <ul>
40   *     <li>
41   *         Improves code readability by clearly indicating which parameters are unused.
42   *     </li>
43   *     <li>
44   *         Follows Java conventions for denoting unused parameters with an underscore ({@code _}).
45   *     </li>
46   * </ul>
47   * <p>
48   * See the <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
49   * Java Language Specification</a> for more information about unnamed variables.
50   * </p>
51   * <p>
52   * <b>Attention</b>: Unnamed variables are available as a preview feature in Java 21,
53   * and became an official part of the language in Java 22.
54   * This check should be activated only on source code which meets those requirements.
55   * </p>
56   * <p>
57   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
58   * </p>
59   * <p>
60   * Violation Message Keys:
61   * </p>
62   * <ul>
63   * <li>
64   * {@code unused.lambda.parameter}
65   * </li>
66   * </ul>
67   *
68   * @since 10.18.0
69   */
70  @FileStatefulCheck
71  public class UnusedLambdaParameterShouldBeUnnamedCheck extends AbstractCheck {
72  
73      /**
74       * A key is pointing to the warning message text in "messages.properties"
75       * file.
76       */
77      public static final String MSG_UNUSED_LAMBDA_PARAMETER = "unused.lambda.parameter";
78  
79      /**
80       * Invalid parents of the lambda parameter identifier.
81       * These are tokens that can not be parents for a lambda
82       * parameter identifier.
83       */
84      private static final int[] INVALID_LAMBDA_PARAM_IDENT_PARENTS = {
85          TokenTypes.DOT,
86          TokenTypes.LITERAL_NEW,
87          TokenTypes.METHOD_CALL,
88          TokenTypes.TYPE,
89      };
90  
91      /**
92       * Keeps track of the lambda parameters in a block.
93       */
94      private final Deque<LambdaParameterDetails> lambdaParameters = new ArrayDeque<>();
95  
96      @Override
97      public int[] getDefaultTokens() {
98          return getRequiredTokens();
99      }
100 
101     @Override
102     public int[] getAcceptableTokens() {
103         return getRequiredTokens();
104     }
105 
106     @Override
107     public int[] getRequiredTokens() {
108         return new int[] {
109             TokenTypes.LAMBDA,
110             TokenTypes.IDENT,
111         };
112     }
113 
114     @Override
115     public void beginTree(DetailAST rootAST) {
116         lambdaParameters.clear();
117     }
118 
119     @Override
120     public void visitToken(DetailAST ast) {
121         if (ast.getType() == TokenTypes.LAMBDA) {
122             final DetailAST parameters = ast.findFirstToken(TokenTypes.PARAMETERS);
123             if (parameters != null) {
124                 // we have multiple lambda parameters
125                 TokenUtil.forEachChild(parameters, TokenTypes.PARAMETER_DEF, parameter -> {
126                     final DetailAST identifierAst = parameter.findFirstToken(TokenTypes.IDENT);
127                     final LambdaParameterDetails lambdaParameter =
128                             new LambdaParameterDetails(ast, identifierAst);
129                     lambdaParameters.push(lambdaParameter);
130                 });
131             }
132             else if (ast.getChildCount() != 0) {
133                 // we are not switch rule and have a single parameter
134                 final LambdaParameterDetails lambdaParameter =
135                             new LambdaParameterDetails(ast, ast.findFirstToken(TokenTypes.IDENT));
136                 lambdaParameters.push(lambdaParameter);
137             }
138         }
139         else if (isLambdaParameterIdentifierCandidate(ast) && !isLeftHandOfAssignment(ast)) {
140             // we do not count reassignment as usage
141             lambdaParameters.stream()
142                     .filter(parameter -> parameter.getName().equals(ast.getText()))
143                     .findFirst()
144                     .ifPresent(LambdaParameterDetails::registerAsUsed);
145         }
146     }
147 
148     @Override
149     public void leaveToken(DetailAST ast) {
150         while (lambdaParameters.peek() != null
151                     && ast.equals(lambdaParameters.peek().enclosingLambda)) {
152 
153             final Optional<LambdaParameterDetails> unusedLambdaParameter =
154                     Optional.ofNullable(lambdaParameters.peek())
155                             .filter(parameter -> !parameter.isUsed())
156                             .filter(parameter -> !"_".equals(parameter.getName()));
157 
158             unusedLambdaParameter.ifPresent(parameter -> {
159                 log(parameter.getIdentifierAst(),
160                         MSG_UNUSED_LAMBDA_PARAMETER,
161                         parameter.getName());
162             });
163             lambdaParameters.pop();
164         }
165     }
166 
167     /**
168      * Visit ast of type {@link TokenTypes#IDENT}
169      * and check if it is a candidate for a lambda parameter identifier.
170      *
171      * @param identifierAst token representing {@link TokenTypes#IDENT}
172      * @return true if the given {@link TokenTypes#IDENT} could be a lambda parameter identifier
173      */
174     private static boolean isLambdaParameterIdentifierCandidate(DetailAST identifierAst) {
175         // we should ignore the ident if it is in the lambda parameters declaration
176         final boolean isLambdaParameterDeclaration =
177                 identifierAst.getParent().getType() == TokenTypes.LAMBDA
178                     || identifierAst.getParent().getType() == TokenTypes.PARAMETER_DEF;
179 
180         return !isLambdaParameterDeclaration
181                  && (hasValidParentToken(identifierAst) || isMethodInvocation(identifierAst));
182     }
183 
184     /**
185      * Check if the given {@link TokenTypes#IDENT} has a valid parent token.
186      * A valid parent token is a token that can be a parent for a lambda parameter identifier.
187      *
188      * @param identifierAst token representing {@link TokenTypes#IDENT}
189      * @return true if the given {@link TokenTypes#IDENT} has a valid parent token
190      */
191     private static boolean hasValidParentToken(DetailAST identifierAst) {
192         return !TokenUtil.isOfType(identifierAst.getParent(), INVALID_LAMBDA_PARAM_IDENT_PARENTS);
193     }
194 
195     /**
196      * Check if the given {@link TokenTypes#IDENT} is a child of a dot operator
197      * and is a candidate for lambda parameter.
198      *
199      * @param identAst token representing {@link TokenTypes#IDENT}
200      * @return true if the given {@link TokenTypes#IDENT} is a child of a dot operator
201      *     and a candidate for lambda parameter.
202      */
203     private static boolean isMethodInvocation(DetailAST identAst) {
204         final DetailAST parent = identAst.getParent();
205         return parent.getType() == TokenTypes.DOT
206                 && identAst.equals(parent.getFirstChild());
207     }
208 
209     /**
210      * Check if the given {@link TokenTypes#IDENT} is a left hand side value.
211      *
212      * @param identAst token representing {@link TokenTypes#IDENT}
213      * @return true if the given {@link TokenTypes#IDENT} is a left hand side value.
214      */
215     private static boolean isLeftHandOfAssignment(DetailAST identAst) {
216         final DetailAST parent = identAst.getParent();
217         return parent.getType() == TokenTypes.ASSIGN
218                 && !identAst.equals(parent.getLastChild());
219     }
220 
221     /**
222      * Maintains information about the lambda parameter.
223      */
224     private static final class LambdaParameterDetails {
225 
226         /**
227          * Ast of type {@link TokenTypes#LAMBDA} enclosing the lambda
228          * parameter.
229          */
230         private final DetailAST enclosingLambda;
231 
232         /**
233          * Ast of type {@link TokenTypes#IDENT} of the given
234          * lambda parameter.
235          */
236         private final DetailAST identifierAst;
237 
238         /**
239          * Is the variable used.
240          */
241         private boolean used;
242 
243         /**
244          * Create a new lambda parameter instance.
245          *
246          * @param enclosingLambda ast of type {@link TokenTypes#LAMBDA}
247          * @param identifierAst ast of type {@link TokenTypes#IDENT}
248          */
249         private LambdaParameterDetails(DetailAST enclosingLambda, DetailAST identifierAst) {
250             this.enclosingLambda = enclosingLambda;
251             this.identifierAst = identifierAst;
252         }
253 
254         /**
255          * Register the lambda parameter as used.
256          */
257         private void registerAsUsed() {
258             used = true;
259         }
260 
261         /**
262          * Get the name of the lambda parameter.
263          *
264          * @return the name of the lambda parameter
265          */
266         private String getName() {
267             return identifierAst.getText();
268         }
269 
270         /**
271          * Get ast of type {@link TokenTypes#IDENT} of the given
272          * lambda parameter.
273          *
274          * @return ast of type {@link TokenTypes#IDENT} of the given lambda parameter
275          */
276         private DetailAST getIdentifierAst() {
277             return identifierAst;
278         }
279 
280         /**
281          * Check if the lambda parameter is used.
282          *
283          * @return true if the lambda parameter is used
284          */
285         private boolean isUsed() {
286             return used;
287         }
288     }
289 }