UnusedLambdaParameterShouldBeUnnamedCheck.java
///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2024 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
///////////////////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.checks.coding;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Optional;
import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
/**
* <p>
* Ensures that lambda parameters that are not used are declared as an unnamed variable.
* </p>
* <p>
* Rationale:
* </p>
* <ul>
* <li>
* Improves code readability by clearly indicating which parameters are unused.
* </li>
* <li>
* Follows Java conventions for denoting unused parameters with an underscore ({@code _}).
* </li>
* </ul>
* <p>
* See the <a href="https://docs.oracle.com/en/java/javase/21/docs/specs/unnamed-jls.html">
* Java Language Specification</a> for more information about unnamed variables.
* </p>
* <p>
* <b>Attention</b>: Unnamed variables are available as a preview feature in Java 21,
* and became an official part of the language in Java 22.
* This check should be activated only on source code which meets those requirements.
* </p>
* <p>
* Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
* </p>
* <p>
* Violation Message Keys:
* </p>
* <ul>
* <li>
* {@code unused.lambda.parameter}
* </li>
* </ul>
*
* @since 10.18.0
*/
@FileStatefulCheck
public class UnusedLambdaParameterShouldBeUnnamedCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text in "messages.properties"
* file.
*/
public static final String MSG_UNUSED_LAMBDA_PARAMETER = "unused.lambda.parameter";
/**
* Invalid parents of the lambda parameter identifier.
* These are tokens that can not be parents for a lambda
* parameter identifier.
*/
private static final int[] INVALID_LAMBDA_PARAM_IDENT_PARENTS = {
TokenTypes.DOT,
TokenTypes.LITERAL_NEW,
TokenTypes.METHOD_CALL,
TokenTypes.TYPE,
};
/**
* Keeps track of the lambda parameters in a block.
*/
private final Deque<LambdaParameterDetails> lambdaParameters = new ArrayDeque<>();
@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}
@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return new int[] {
TokenTypes.LAMBDA,
TokenTypes.IDENT,
};
}
@Override
public void beginTree(DetailAST rootAST) {
lambdaParameters.clear();
}
@Override
public void visitToken(DetailAST ast) {
if (ast.getType() == TokenTypes.LAMBDA) {
final DetailAST parameters = ast.findFirstToken(TokenTypes.PARAMETERS);
if (parameters != null) {
// we have multiple lambda parameters
TokenUtil.forEachChild(parameters, TokenTypes.PARAMETER_DEF, parameter -> {
final DetailAST identifierAst = parameter.findFirstToken(TokenTypes.IDENT);
final LambdaParameterDetails lambdaParameter =
new LambdaParameterDetails(ast, identifierAst);
lambdaParameters.push(lambdaParameter);
});
}
else if (ast.getChildCount() != 0) {
// we are not switch rule and have a single parameter
final LambdaParameterDetails lambdaParameter =
new LambdaParameterDetails(ast, ast.findFirstToken(TokenTypes.IDENT));
lambdaParameters.push(lambdaParameter);
}
}
else if (isLambdaParameterIdentifierCandidate(ast) && !isLeftHandOfAssignment(ast)) {
// we do not count reassignment as usage
lambdaParameters.stream()
.filter(parameter -> parameter.getName().equals(ast.getText()))
.findFirst()
.ifPresent(LambdaParameterDetails::registerAsUsed);
}
}
@Override
public void leaveToken(DetailAST ast) {
while (lambdaParameters.peek() != null
&& ast.equals(lambdaParameters.peek().enclosingLambda)) {
final Optional<LambdaParameterDetails> unusedLambdaParameter =
Optional.ofNullable(lambdaParameters.peek())
.filter(parameter -> !parameter.isUsed())
.filter(parameter -> !"_".equals(parameter.getName()));
unusedLambdaParameter.ifPresent(parameter -> {
log(parameter.getIdentifierAst(),
MSG_UNUSED_LAMBDA_PARAMETER,
parameter.getName());
});
lambdaParameters.pop();
}
}
/**
* Visit ast of type {@link TokenTypes#IDENT}
* and check if it is a candidate for a lambda parameter identifier.
*
* @param identifierAst token representing {@link TokenTypes#IDENT}
* @return true if the given {@link TokenTypes#IDENT} could be a lambda parameter identifier
*/
private static boolean isLambdaParameterIdentifierCandidate(DetailAST identifierAst) {
// we should ignore the ident if it is in the lambda parameters declaration
final boolean isLambdaParameterDeclaration =
identifierAst.getParent().getType() == TokenTypes.LAMBDA
|| identifierAst.getParent().getType() == TokenTypes.PARAMETER_DEF;
return !isLambdaParameterDeclaration
&& (hasValidParentToken(identifierAst) || isMethodInvocation(identifierAst));
}
/**
* Check if the given {@link TokenTypes#IDENT} has a valid parent token.
* A valid parent token is a token that can be a parent for a lambda parameter identifier.
*
* @param identifierAst token representing {@link TokenTypes#IDENT}
* @return true if the given {@link TokenTypes#IDENT} has a valid parent token
*/
private static boolean hasValidParentToken(DetailAST identifierAst) {
return !TokenUtil.isOfType(identifierAst.getParent(), INVALID_LAMBDA_PARAM_IDENT_PARENTS);
}
/**
* Check if the given {@link TokenTypes#IDENT} is a child of a dot operator
* and is a candidate for lambda parameter.
*
* @param identAst token representing {@link TokenTypes#IDENT}
* @return true if the given {@link TokenTypes#IDENT} is a child of a dot operator
* and a candidate for lambda parameter.
*/
private static boolean isMethodInvocation(DetailAST identAst) {
final DetailAST parent = identAst.getParent();
return parent.getType() == TokenTypes.DOT
&& identAst.equals(parent.getFirstChild());
}
/**
* Check if the given {@link TokenTypes#IDENT} is a left hand side value.
*
* @param identAst token representing {@link TokenTypes#IDENT}
* @return true if the given {@link TokenTypes#IDENT} is a left hand side value.
*/
private static boolean isLeftHandOfAssignment(DetailAST identAst) {
final DetailAST parent = identAst.getParent();
return parent.getType() == TokenTypes.ASSIGN
&& !identAst.equals(parent.getLastChild());
}
/**
* Maintains information about the lambda parameter.
*/
private static final class LambdaParameterDetails {
/**
* Ast of type {@link TokenTypes#LAMBDA} enclosing the lambda
* parameter.
*/
private final DetailAST enclosingLambda;
/**
* Ast of type {@link TokenTypes#IDENT} of the given
* lambda parameter.
*/
private final DetailAST identifierAst;
/**
* Is the variable used.
*/
private boolean used;
/**
* Create a new lambda parameter instance.
*
* @param enclosingLambda ast of type {@link TokenTypes#LAMBDA}
* @param identifierAst ast of type {@link TokenTypes#IDENT}
*/
private LambdaParameterDetails(DetailAST enclosingLambda, DetailAST identifierAst) {
this.enclosingLambda = enclosingLambda;
this.identifierAst = identifierAst;
}
/**
* Register the lambda parameter as used.
*/
private void registerAsUsed() {
used = true;
}
/**
* Get the name of the lambda parameter.
*
* @return the name of the lambda parameter
*/
private String getName() {
return identifierAst.getText();
}
/**
* Get ast of type {@link TokenTypes#IDENT} of the given
* lambda parameter.
*
* @return ast of type {@link TokenTypes#IDENT} of the given lambda parameter
*/
private DetailAST getIdentifierAst() {
return identifierAst;
}
/**
* Check if the lambda parameter is used.
*
* @return true if the lambda parameter is used
*/
private boolean isUsed() {
return used;
}
}
}