001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.ArrayList;
023import java.util.List;
024import java.util.Set;
025
026import javax.annotation.Nullable;
027
028import com.puppycrawl.tools.checkstyle.StatelessCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032
033/**
034 * <div>
035 * Checks for assignment of pattern variables.
036 * </div>
037 *
038 * <p>
039 * Pattern variable assignment is considered bad programming practice. The pattern variable
040 * is meant to be a direct reference to the object being matched. Reassigning it can break this
041 * connection and mislead readers.
042 * </p>
043 *
044 * @since 10.26.0
045 */
046@StatelessCheck
047public class PatternVariableAssignmentCheck extends AbstractCheck {
048
049    /**
050     * A key is pointing to the warning message in "messages.properties" file.
051     */
052    public static final String MSG_KEY = "pattern.variable.assignment";
053
054    /**
055     * The set of all valid types of ASSIGN token for this check.
056     */
057    private static final Set<Integer> ASSIGN_TOKEN_TYPES = Set.of(
058        TokenTypes.ASSIGN, TokenTypes.PLUS_ASSIGN, TokenTypes.MINUS_ASSIGN, TokenTypes.STAR_ASSIGN,
059        TokenTypes.DIV_ASSIGN, TokenTypes.MOD_ASSIGN, TokenTypes.SR_ASSIGN, TokenTypes.BSR_ASSIGN,
060        TokenTypes.SL_ASSIGN, TokenTypes.BAND_ASSIGN, TokenTypes.BXOR_ASSIGN,
061        TokenTypes.BOR_ASSIGN);
062
063    @Override
064    public int[] getRequiredTokens() {
065        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
066    }
067
068    @Override
069    public int[] getDefaultTokens() {
070        return getRequiredTokens();
071    }
072
073    @Override
074    public int[] getAcceptableTokens() {
075        return getRequiredTokens();
076    }
077
078    @Override
079    public void visitToken(DetailAST ast) {
080
081        final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
082        final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);
083
084        for (DetailAST patternVariableIdent : patternVariableIdents) {
085            for (DetailAST assignTokenIdent : reassignedVariableIdents) {
086                if (patternVariableIdent.getText().equals(assignTokenIdent.getText())) {
087
088                    log(assignTokenIdent, MSG_KEY, assignTokenIdent.getText());
089                    break;
090                }
091
092            }
093        }
094    }
095
096    /**
097     * Gets the list of all pattern variable idents in instanceof expression.
098     *
099     * @param ast ast tree of instanceof to get the list from.
100     * @return list of pattern variables.
101     */
102    private static List<DetailAST> getPatternVariableIdents(DetailAST ast) {
103
104        final DetailAST outermostPatternVariable =
105            ast.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF);
106
107        final DetailAST recordPatternDef;
108        if (ast.getType() == TokenTypes.LITERAL_INSTANCEOF) {
109            recordPatternDef = ast.findFirstToken(TokenTypes.RECORD_PATTERN_DEF);
110        }
111        else {
112            recordPatternDef = ast;
113        }
114
115        final List<DetailAST> patternVariableIdentsArray = new ArrayList<>();
116
117        if (outermostPatternVariable != null) {
118            patternVariableIdentsArray.add(
119                outermostPatternVariable.findFirstToken(TokenTypes.IDENT));
120        }
121        else if (recordPatternDef != null) {
122            final DetailAST recordPatternComponents = recordPatternDef
123                .findFirstToken(TokenTypes.RECORD_PATTERN_COMPONENTS);
124
125            if (recordPatternComponents != null) {
126                for (DetailAST innerPatternVariable = recordPatternComponents.getFirstChild();
127                     innerPatternVariable != null;
128                     innerPatternVariable = innerPatternVariable.getNextSibling()) {
129
130                    if (innerPatternVariable.getType() == TokenTypes.PATTERN_VARIABLE_DEF) {
131                        patternVariableIdentsArray.add(
132                            innerPatternVariable.findFirstToken(TokenTypes.IDENT));
133                    }
134                    else {
135                        patternVariableIdentsArray.addAll(
136                            getPatternVariableIdents(innerPatternVariable));
137                    }
138
139                }
140            }
141
142        }
143        return patternVariableIdentsArray;
144    }
145
146    /**
147     * Gets the array list made out of AST branches of reassigned variable idents.
148     *
149     * @param ast ast tree of checked instanceof statement.
150     * @return the list of AST branches of reassigned variable idents.
151     */
152    private static List<DetailAST> getReassignedVariableIdents(DetailAST ast) {
153
154        final DetailAST branchLeadingToReassignedVar = getBranchLeadingToReassignedVars(ast);
155        final List<DetailAST> reassignedVariableIdents = new ArrayList<>();
156
157        for (DetailAST expressionBranch = branchLeadingToReassignedVar;
158             expressionBranch != null;
159             expressionBranch = traverseUntilNeededBranchType(expressionBranch,
160                 branchLeadingToReassignedVar, TokenTypes.EXPR)) {
161
162            final DetailAST assignToken = getMatchedAssignToken(expressionBranch);
163
164            if (assignToken != null) {
165                reassignedVariableIdents.add(getNeededAssignIdent(assignToken));
166            }
167
168        }
169
170        return reassignedVariableIdents;
171
172    }
173
174    /**
175     * Gets the closest consistent AST branch that leads to reassigned variable's ident.
176     *
177     * @param ast ast tree of checked instanceof statement.
178     * @return the closest consistent AST branch that leads to reassigned variable's ident.
179     */
180    @Nullable
181    private static DetailAST getBranchLeadingToReassignedVars(DetailAST ast) {
182        DetailAST leadingToReassignedVarBranch = null;
183
184        for (DetailAST conditionalStatement = ast;
185             conditionalStatement != null && leadingToReassignedVarBranch == null;
186             conditionalStatement = conditionalStatement.getParent()) {
187
188            if (conditionalStatement.getType() == TokenTypes.LITERAL_IF
189                || conditionalStatement.getType() == TokenTypes.LITERAL_ELSE) {
190
191                leadingToReassignedVarBranch =
192                    conditionalStatement.findFirstToken(TokenTypes.SLIST);
193
194            }
195            else if (conditionalStatement.getType() == TokenTypes.QUESTION) {
196                leadingToReassignedVarBranch = conditionalStatement;
197            }
198        }
199
200        return leadingToReassignedVarBranch;
201
202    }
203
204    /**
205     * Traverses along the AST tree to locate the first branch of certain token type.
206     *
207     * @param startingBranch AST branch to start the traverse from, but not check.
208     * @param bound AST Branch that the traverse cannot further extend to.
209     * @param neededTokenType Token type whose first encountered branch is to look for.
210     * @return the AST tree of first encountered branch of needed token type.
211     */
212    @Nullable
213    private static DetailAST traverseUntilNeededBranchType(DetailAST startingBranch,
214                              DetailAST bound, int neededTokenType) {
215
216        DetailAST match = null;
217
218        DetailAST iteratedBranch = shiftToNextTraversedBranch(startingBranch, bound);
219
220        while (iteratedBranch != null) {
221            if (iteratedBranch.getType() == neededTokenType) {
222                match = iteratedBranch;
223                break;
224            }
225
226            iteratedBranch = shiftToNextTraversedBranch(iteratedBranch, bound);
227        }
228
229        return match;
230    }
231
232    /**
233     * Shifts once to the next possible branch within traverse trajectory.
234     *
235     * @param ast AST branch to shift from.
236     * @param boundAst AST Branch that the traverse cannot further extend to.
237     * @return the AST tree of next possible branch within traverse trajectory.
238     */
239    @Nullable
240    private static DetailAST shiftToNextTraversedBranch(DetailAST ast, DetailAST boundAst) {
241        DetailAST newAst = ast;
242
243        if (ast.getFirstChild() != null) {
244            newAst = ast.getFirstChild();
245        }
246        else {
247            while (newAst.getNextSibling() == null && !newAst.equals(boundAst)) {
248                newAst = newAst.getParent();
249            }
250            if (newAst.equals(boundAst)) {
251                newAst = null;
252            }
253            else {
254                newAst = newAst.getNextSibling();
255            }
256        }
257
258        return newAst;
259    }
260
261    /**
262     * Gets the type of ASSIGN tokens that particularly matches with what follows the preceding
263     * branch.
264     *
265     * @param preAssignBranch branch that precedes the branch of ASSIGN token types.
266     * @return type of ASSIGN token.
267     */
268    @Nullable
269    private static DetailAST getMatchedAssignToken(DetailAST preAssignBranch) {
270        DetailAST matchedAssignToken = null;
271
272        for (int assignType : ASSIGN_TOKEN_TYPES) {
273            matchedAssignToken = preAssignBranch.findFirstToken(assignType);
274            if (matchedAssignToken != null) {
275                break;
276            }
277        }
278
279        return matchedAssignToken;
280    }
281
282    /**
283     * Gets the needed AST Ident of reassigned variable for check to compare.
284     *
285     * @param assignToken The AST branch of reassigned variable's ASSIGN token.
286     * @return needed AST Ident.
287     */
288    private static DetailAST getNeededAssignIdent(DetailAST assignToken) {
289        DetailAST assignIdent = assignToken;
290
291        while (traverseUntilNeededBranchType(
292            assignIdent, assignToken.getFirstChild(), TokenTypes.IDENT) != null) {
293
294            assignIdent =
295                traverseUntilNeededBranchType(assignIdent, assignToken, TokenTypes.IDENT);
296        }
297
298        return assignIdent;
299    }
300}