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 * <p>
045 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
046 * </p>
047 *
048 * <p>
049 * Violation Message Keys:
050 * </p>
051 * <ul>
052 * <li>
053 * {@code pattern.variable.assignment}
054 * </li>
055 * </ul>
056 *
057 * @since 10.26.0
058 */
059@StatelessCheck
060public class PatternVariableAssignmentCheck extends AbstractCheck {
061
062    /**
063     * A key is pointing to the warning message in "messages.properties" file.
064     */
065    public static final String MSG_KEY = "pattern.variable.assignment";
066
067    /**
068     * The set of all valid types of ASSIGN token for this check.
069     */
070    private static final Set<Integer> ASSIGN_TOKEN_TYPES = Set.of(
071        TokenTypes.ASSIGN, TokenTypes.PLUS_ASSIGN, TokenTypes.MINUS_ASSIGN, TokenTypes.STAR_ASSIGN,
072        TokenTypes.DIV_ASSIGN, TokenTypes.MOD_ASSIGN, TokenTypes.SR_ASSIGN, TokenTypes.BSR_ASSIGN,
073        TokenTypes.SL_ASSIGN, TokenTypes.BAND_ASSIGN, TokenTypes.BXOR_ASSIGN,
074        TokenTypes.BOR_ASSIGN);
075
076    @Override
077    public int[] getRequiredTokens() {
078        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
079    }
080
081    @Override
082    public int[] getDefaultTokens() {
083        return getRequiredTokens();
084    }
085
086    @Override
087    public int[] getAcceptableTokens() {
088        return getRequiredTokens();
089    }
090
091    @Override
092    public void visitToken(DetailAST ast) {
093
094        final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
095        final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);
096
097        for (DetailAST patternVariableIdent : patternVariableIdents) {
098            for (DetailAST assignTokenIdent : reassignedVariableIdents) {
099                if (patternVariableIdent.getText().equals(assignTokenIdent.getText())) {
100
101                    log(assignTokenIdent, MSG_KEY, assignTokenIdent.getText());
102                    break;
103                }
104
105            }
106        }
107    }
108
109    /**
110     * Gets the list of all pattern variable idents in instanceof expression.
111     *
112     * @param ast ast tree of instanceof to get the list from.
113     * @return list of pattern variables.
114     */
115    private static List<DetailAST> getPatternVariableIdents(DetailAST ast) {
116
117        final DetailAST outermostPatternVariable =
118            ast.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF);
119
120        final DetailAST recordPatternDef;
121        if (ast.getType() == TokenTypes.LITERAL_INSTANCEOF) {
122            recordPatternDef = ast.findFirstToken(TokenTypes.RECORD_PATTERN_DEF);
123        }
124        else {
125            recordPatternDef = ast;
126        }
127
128        final List<DetailAST> patternVariableIdentsArray = new ArrayList<>();
129
130        if (outermostPatternVariable != null) {
131            patternVariableIdentsArray.add(
132                outermostPatternVariable.findFirstToken(TokenTypes.IDENT));
133        }
134        else if (recordPatternDef != null) {
135            final DetailAST recordPatternComponents = recordPatternDef
136                .findFirstToken(TokenTypes.RECORD_PATTERN_COMPONENTS);
137
138            if (recordPatternComponents != null) {
139                for (DetailAST innerPatternVariable = recordPatternComponents.getFirstChild();
140                     innerPatternVariable != null;
141                     innerPatternVariable = innerPatternVariable.getNextSibling()) {
142
143                    if (innerPatternVariable.getType() == TokenTypes.PATTERN_VARIABLE_DEF) {
144                        patternVariableIdentsArray.add(
145                            innerPatternVariable.findFirstToken(TokenTypes.IDENT));
146                    }
147                    else {
148                        patternVariableIdentsArray.addAll(
149                            getPatternVariableIdents(innerPatternVariable));
150                    }
151
152                }
153            }
154
155        }
156        return patternVariableIdentsArray;
157    }
158
159    /**
160     * Gets the array list made out of AST branches of reassigned variable idents.
161     *
162     * @param ast ast tree of checked instanceof statement.
163     * @return the list of AST branches of reassigned variable idents.
164     */
165    private static List<DetailAST> getReassignedVariableIdents(DetailAST ast) {
166
167        final DetailAST branchLeadingToReassignedVar = getBranchLeadingToReassignedVars(ast);
168        final List<DetailAST> reassignedVariableIdents = new ArrayList<>();
169
170        for (DetailAST expressionBranch = branchLeadingToReassignedVar;
171             expressionBranch != null;
172             expressionBranch = traverseUntilNeededBranchType(expressionBranch,
173                 branchLeadingToReassignedVar, TokenTypes.EXPR)) {
174
175            final DetailAST assignToken = getMatchedAssignToken(expressionBranch);
176
177            if (assignToken != null) {
178                reassignedVariableIdents.add(getNeededAssignIdent(assignToken));
179            }
180
181        }
182
183        return reassignedVariableIdents;
184
185    }
186
187    /**
188     * Gets the closest consistent AST branch that leads to reassigned variable's ident.
189     *
190     * @param ast ast tree of checked instanceof statement.
191     * @return the closest consistent AST branch that leads to reassigned variable's ident.
192     */
193    @Nullable
194    private static DetailAST getBranchLeadingToReassignedVars(DetailAST ast) {
195        DetailAST leadingToReassignedVarBranch = null;
196
197        for (DetailAST conditionalStatement = ast;
198             conditionalStatement != null && leadingToReassignedVarBranch == null;
199             conditionalStatement = conditionalStatement.getParent()) {
200
201            if (conditionalStatement.getType() == TokenTypes.LITERAL_IF
202                || conditionalStatement.getType() == TokenTypes.LITERAL_ELSE) {
203
204                leadingToReassignedVarBranch =
205                    conditionalStatement.findFirstToken(TokenTypes.SLIST);
206
207            }
208            else if (conditionalStatement.getType() == TokenTypes.QUESTION) {
209                leadingToReassignedVarBranch = conditionalStatement;
210            }
211        }
212
213        return leadingToReassignedVarBranch;
214
215    }
216
217    /**
218     * Traverses along the AST tree to locate the first branch of certain token type.
219     *
220     * @param startingBranch AST branch to start the traverse from, but not check.
221     * @param bound AST Branch that the traverse cannot further extend to.
222     * @param neededTokenType Token type whose first encountered branch is to look for.
223     * @return the AST tree of first encountered branch of needed token type.
224     */
225    @Nullable
226    private static DetailAST traverseUntilNeededBranchType(DetailAST startingBranch,
227                              DetailAST bound, int neededTokenType) {
228
229        DetailAST match = null;
230
231        DetailAST iteratedBranch = shiftToNextTraversedBranch(startingBranch, bound);
232
233        while (iteratedBranch != null) {
234            if (iteratedBranch.getType() == neededTokenType) {
235                match = iteratedBranch;
236                break;
237            }
238
239            iteratedBranch = shiftToNextTraversedBranch(iteratedBranch, bound);
240        }
241
242        return match;
243    }
244
245    /**
246     * Shifts once to the next possible branch within traverse trajectory.
247     *
248     * @param ast AST branch to shift from.
249     * @param boundAst AST Branch that the traverse cannot further extend to.
250     * @return the AST tree of next possible branch within traverse trajectory.
251     */
252    @Nullable
253    private static DetailAST shiftToNextTraversedBranch(DetailAST ast, DetailAST boundAst) {
254        DetailAST newAst = ast;
255
256        if (ast.getFirstChild() != null) {
257            newAst = ast.getFirstChild();
258        }
259        else {
260            while (newAst.getNextSibling() == null && !newAst.equals(boundAst)) {
261                newAst = newAst.getParent();
262            }
263            if (newAst.equals(boundAst)) {
264                newAst = null;
265            }
266            else {
267                newAst = newAst.getNextSibling();
268            }
269        }
270
271        return newAst;
272    }
273
274    /**
275     * Gets the type of ASSIGN tokens that particularly matches with what follows the preceding
276     * branch.
277     *
278     * @param preAssignBranch branch that precedes the branch of ASSIGN token types.
279     * @return type of ASSIGN token.
280     */
281    @Nullable
282    private static DetailAST getMatchedAssignToken(DetailAST preAssignBranch) {
283        DetailAST matchedAssignToken = null;
284
285        for (int assignType : ASSIGN_TOKEN_TYPES) {
286            matchedAssignToken = preAssignBranch.findFirstToken(assignType);
287            if (matchedAssignToken != null) {
288                break;
289            }
290        }
291
292        return matchedAssignToken;
293    }
294
295    /**
296     * Gets the needed AST Ident of reassigned variable for check to compare.
297     *
298     * @param assignToken The AST branch of reassigned variable's ASSIGN token.
299     * @return needed AST Ident.
300     */
301    private static DetailAST getNeededAssignIdent(DetailAST assignToken) {
302        DetailAST assignIdent = assignToken;
303
304        while (traverseUntilNeededBranchType(
305            assignIdent, assignToken.getFirstChild(), TokenTypes.IDENT) != null) {
306
307            assignIdent =
308                traverseUntilNeededBranchType(assignIdent, assignToken, TokenTypes.IDENT);
309        }
310
311        return assignIdent;
312    }
313}