PatternVariableAssignmentCheck.java

///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2025 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.ArrayList;
import java.util.List;
import java.util.Set;

import javax.annotation.Nullable;

import com.puppycrawl.tools.checkstyle.StatelessCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

/**
 * <div>
 * Checks for assignment of pattern variables.
 * </div>
 *
 * <p>
 * Pattern variable assignment is considered bad programming practice. The pattern variable
 * is meant to be a direct reference to the object being matched. Reassigning it can break this
 * connection and mislead readers.
 * </p>
 *
 * <p>
 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
 * </p>
 *
 * <p>
 * Violation Message Keys:
 * </p>
 * <ul>
 * <li>
 * {@code pattern.variable.assignment}
 * </li>
 * </ul>
 *
 * @since 10.26.0
 */
@StatelessCheck
public class PatternVariableAssignmentCheck extends AbstractCheck {

    /**
     * A key is pointing to the warning message in "messages.properties" file.
     */
    public static final String MSG_KEY = "pattern.variable.assignment";

    /**
     * The set of all valid types of ASSIGN token for this check.
     */
    private static final Set<Integer> ASSIGN_TOKEN_TYPES = Set.of(
        TokenTypes.ASSIGN, TokenTypes.PLUS_ASSIGN, TokenTypes.MINUS_ASSIGN, TokenTypes.STAR_ASSIGN,
        TokenTypes.DIV_ASSIGN, TokenTypes.MOD_ASSIGN, TokenTypes.SR_ASSIGN, TokenTypes.BSR_ASSIGN,
        TokenTypes.SL_ASSIGN, TokenTypes.BAND_ASSIGN, TokenTypes.BXOR_ASSIGN,
        TokenTypes.BOR_ASSIGN);

    @Override
    public int[] getRequiredTokens() {
        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
    }

    @Override
    public int[] getDefaultTokens() {
        return getRequiredTokens();
    }

    @Override
    public int[] getAcceptableTokens() {
        return getRequiredTokens();
    }

    @Override
    public void visitToken(DetailAST ast) {

        final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
        final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);

        for (DetailAST patternVariableIdent : patternVariableIdents) {
            for (DetailAST assignTokenIdent : reassignedVariableIdents) {
                if (patternVariableIdent.getText().equals(assignTokenIdent.getText())) {

                    log(assignTokenIdent, MSG_KEY, assignTokenIdent.getText());
                    break;
                }

            }
        }
    }

    /**
     * Gets the list of all pattern variable idents in instanceof expression.
     *
     * @param ast ast tree of instanceof to get the list from.
     * @return list of pattern variables.
     */
    private static List<DetailAST> getPatternVariableIdents(DetailAST ast) {

        final DetailAST outermostPatternVariable =
            ast.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF);

        final DetailAST recordPatternDef;
        if (ast.getType() == TokenTypes.LITERAL_INSTANCEOF) {
            recordPatternDef = ast.findFirstToken(TokenTypes.RECORD_PATTERN_DEF);
        }
        else {
            recordPatternDef = ast;
        }

        final List<DetailAST> patternVariableIdentsArray = new ArrayList<>();

        if (outermostPatternVariable != null) {
            patternVariableIdentsArray.add(
                outermostPatternVariable.findFirstToken(TokenTypes.IDENT));
        }
        else if (recordPatternDef != null) {
            final DetailAST recordPatternComponents = recordPatternDef
                .findFirstToken(TokenTypes.RECORD_PATTERN_COMPONENTS);

            if (recordPatternComponents != null) {
                for (DetailAST innerPatternVariable = recordPatternComponents.getFirstChild();
                     innerPatternVariable != null;
                     innerPatternVariable = innerPatternVariable.getNextSibling()) {

                    if (innerPatternVariable.getType() == TokenTypes.PATTERN_VARIABLE_DEF) {
                        patternVariableIdentsArray.add(
                            innerPatternVariable.findFirstToken(TokenTypes.IDENT));
                    }
                    else {
                        patternVariableIdentsArray.addAll(
                            getPatternVariableIdents(innerPatternVariable));
                    }

                }
            }

        }
        return patternVariableIdentsArray;
    }

    /**
     * Gets the array list made out of AST branches of reassigned variable idents.
     *
     * @param ast ast tree of checked instanceof statement.
     * @return the list of AST branches of reassigned variable idents.
     */
    private static List<DetailAST> getReassignedVariableIdents(DetailAST ast) {

        final DetailAST branchLeadingToReassignedVar = getBranchLeadingToReassignedVars(ast);
        final List<DetailAST> reassignedVariableIdents = new ArrayList<>();

        for (DetailAST expressionBranch = branchLeadingToReassignedVar;
             expressionBranch != null;
             expressionBranch = traverseUntilNeededBranchType(expressionBranch,
                 branchLeadingToReassignedVar, TokenTypes.EXPR)) {

            final DetailAST assignToken = getMatchedAssignToken(expressionBranch);

            if (assignToken != null) {
                reassignedVariableIdents.add(getNeededAssignIdent(assignToken));
            }

        }

        return reassignedVariableIdents;

    }

    /**
     * Gets the closest consistent AST branch that leads to reassigned variable's ident.
     *
     * @param ast ast tree of checked instanceof statement.
     * @return the closest consistent AST branch that leads to reassigned variable's ident.
     */
    @Nullable
    private static DetailAST getBranchLeadingToReassignedVars(DetailAST ast) {
        DetailAST leadingToReassignedVarBranch = null;

        for (DetailAST conditionalStatement = ast;
             conditionalStatement != null && leadingToReassignedVarBranch == null;
             conditionalStatement = conditionalStatement.getParent()) {

            if (conditionalStatement.getType() == TokenTypes.LITERAL_IF
                || conditionalStatement.getType() == TokenTypes.LITERAL_ELSE) {

                leadingToReassignedVarBranch =
                    conditionalStatement.findFirstToken(TokenTypes.SLIST);

            }
            else if (conditionalStatement.getType() == TokenTypes.QUESTION) {
                leadingToReassignedVarBranch = conditionalStatement;
            }
        }

        return leadingToReassignedVarBranch;

    }

    /**
     * Traverses along the AST tree to locate the first branch of certain token type.
     *
     * @param startingBranch AST branch to start the traverse from, but not check.
     * @param bound AST Branch that the traverse cannot further extend to.
     * @param neededTokenType Token type whose first encountered branch is to look for.
     * @return the AST tree of first encountered branch of needed token type.
     */
    @Nullable
    private static DetailAST traverseUntilNeededBranchType(DetailAST startingBranch,
                              DetailAST bound, int neededTokenType) {

        DetailAST match = null;

        DetailAST iteratedBranch = shiftToNextTraversedBranch(startingBranch, bound);

        while (iteratedBranch != null) {
            if (iteratedBranch.getType() == neededTokenType) {
                match = iteratedBranch;
                break;
            }

            iteratedBranch = shiftToNextTraversedBranch(iteratedBranch, bound);
        }

        return match;
    }

    /**
     * Shifts once to the next possible branch within traverse trajectory.
     *
     * @param ast AST branch to shift from.
     * @param boundAst AST Branch that the traverse cannot further extend to.
     * @return the AST tree of next possible branch within traverse trajectory.
     */
    @Nullable
    private static DetailAST shiftToNextTraversedBranch(DetailAST ast, DetailAST boundAst) {
        DetailAST newAst = ast;

        if (ast.getFirstChild() != null) {
            newAst = ast.getFirstChild();
        }
        else {
            while (newAst.getNextSibling() == null && !newAst.equals(boundAst)) {
                newAst = newAst.getParent();
            }
            if (newAst.equals(boundAst)) {
                newAst = null;
            }
            else {
                newAst = newAst.getNextSibling();
            }
        }

        return newAst;
    }

    /**
     * Gets the type of ASSIGN tokens that particularly matches with what follows the preceding
     * branch.
     *
     * @param preAssignBranch branch that precedes the branch of ASSIGN token types.
     * @return type of ASSIGN token.
     */
    @Nullable
    private static DetailAST getMatchedAssignToken(DetailAST preAssignBranch) {
        DetailAST matchedAssignToken = null;

        for (int assignType : ASSIGN_TOKEN_TYPES) {
            matchedAssignToken = preAssignBranch.findFirstToken(assignType);
            if (matchedAssignToken != null) {
                break;
            }
        }

        return matchedAssignToken;
    }

    /**
     * Gets the needed AST Ident of reassigned variable for check to compare.
     *
     * @param assignToken The AST branch of reassigned variable's ASSIGN token.
     * @return needed AST Ident.
     */
    private static DetailAST getNeededAssignIdent(DetailAST assignToken) {
        DetailAST assignIdent = assignToken;

        while (traverseUntilNeededBranchType(
            assignIdent, assignToken.getFirstChild(), TokenTypes.IDENT) != null) {

            assignIdent =
                traverseUntilNeededBranchType(assignIdent, assignToken, TokenTypes.IDENT);
        }

        return assignIdent;
    }
}