001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.Collection;
024import java.util.List;
025import java.util.Set;
026
027import javax.annotation.Nullable;
028
029import com.puppycrawl.tools.checkstyle.StatelessCheck;
030import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
031import com.puppycrawl.tools.checkstyle.api.DetailAST;
032import com.puppycrawl.tools.checkstyle.api.TokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
034
035/**
036 * <div>
037 * Checks for assignment of pattern variables.
038 * </div>
039 *
040 * <p>
041 * Pattern variable assignment is considered bad programming practice. The pattern variable
042 * is meant to be a direct reference to the object being matched. Reassigning it can break this
043 * connection and mislead readers.
044 * </p>
045 *
046 * @since 10.26.0
047 */
048@StatelessCheck
049public class PatternVariableAssignmentCheck extends AbstractCheck {
050
051    /**
052     * A key is pointing to the warning message in "messages.properties" file.
053     */
054    public static final String MSG_KEY = "pattern.variable.assignment";
055
056    /**
057     * The set of all valid types of ASSIGN token for this check.
058     */
059    private static final Set<Integer> ASSIGN_TOKEN_TYPES = Set.of(
060        TokenTypes.ASSIGN, TokenTypes.PLUS_ASSIGN, TokenTypes.MINUS_ASSIGN, TokenTypes.STAR_ASSIGN,
061        TokenTypes.DIV_ASSIGN, TokenTypes.MOD_ASSIGN, TokenTypes.SR_ASSIGN, TokenTypes.BSR_ASSIGN,
062        TokenTypes.SL_ASSIGN, TokenTypes.BAND_ASSIGN, TokenTypes.BXOR_ASSIGN,
063        TokenTypes.BOR_ASSIGN);
064
065    @Override
066    public int[] getRequiredTokens() {
067        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
068    }
069
070    @Override
071    public int[] getDefaultTokens() {
072        return getRequiredTokens();
073    }
074
075    @Override
076    public int[] getAcceptableTokens() {
077        return getRequiredTokens();
078    }
079
080    @Override
081    public void visitToken(DetailAST ast) {
082
083        final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
084        final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);
085
086        for (DetailAST patternVariableIdent : patternVariableIdents) {
087            checkForReassignment(patternVariableIdent, reassignedVariableIdents);
088        }
089    }
090
091    /**
092     * Gets the list of all pattern variable idents in instanceof expression.
093     *
094     * @param ast ast tree of instanceof to get the list from.
095     * @return list of pattern variables.
096     */
097    private static List<DetailAST> getPatternVariableIdents(DetailAST ast) {
098
099        final DetailAST outermostPatternVariable =
100            ast.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF);
101
102        final DetailAST recordPatternDef;
103        if (ast.getType() == TokenTypes.LITERAL_INSTANCEOF) {
104            recordPatternDef = ast.findFirstToken(TokenTypes.RECORD_PATTERN_DEF);
105        }
106        else {
107            recordPatternDef = ast;
108        }
109
110        final List<DetailAST> patternVariableIdentsArray = new ArrayList<>();
111
112        if (outermostPatternVariable != null) {
113            patternVariableIdentsArray.add(
114                outermostPatternVariable.findFirstToken(TokenTypes.IDENT));
115        }
116        else if (recordPatternDef != null) {
117            final DetailAST recordPatternComponents = recordPatternDef
118                .findFirstToken(TokenTypes.RECORD_PATTERN_COMPONENTS);
119
120            if (recordPatternComponents != null) {
121                for (DetailAST innerPatternVariable = recordPatternComponents.getFirstChild();
122                     innerPatternVariable != null;
123                     innerPatternVariable = innerPatternVariable.getNextSibling()) {
124
125                    if (innerPatternVariable.getType() == TokenTypes.PATTERN_VARIABLE_DEF) {
126                        patternVariableIdentsArray.add(
127                            innerPatternVariable.findFirstToken(TokenTypes.IDENT));
128                    }
129                    else {
130                        patternVariableIdentsArray.addAll(
131                            getPatternVariableIdents(innerPatternVariable));
132                    }
133
134                }
135            }
136
137        }
138        return patternVariableIdentsArray;
139    }
140
141    /**
142     * Gets the list of AST branches of reassigned variable identifiers.
143     *
144     * @param ast ast tree of checked instanceof statement
145     * @return list of AST identifiers that represent reassigned variables
146     */
147    private static List<DetailAST> getReassignedVariableIdents(DetailAST ast) {
148
149        final List<DetailAST> reassignedVariableIdents = new ArrayList<>();
150        final DetailAST scopeRoot = findReassignmentScopeRoot(ast);
151
152        if (scopeRoot != null) {
153
154            final List<DetailAST> branches =
155                    expandReassignmentScopes(scopeRoot);
156
157            for (DetailAST branch : branches) {
158                for (DetailAST expressionBranch = branch;
159                     expressionBranch != null;
160                     expressionBranch = traverseUntilNeededBranchType(
161                             expressionBranch, branch, TokenTypes.EXPR)) {
162
163                    final DetailAST assignToken =
164                            getMatchedAssignToken(expressionBranch);
165
166                    if (assignToken != null) {
167                        final DetailAST neededAssignIdent =
168                                getNeededAssignIdent(assignToken);
169
170                        if (neededAssignIdent.getPreviousSibling() == null) {
171                            reassignedVariableIdents.add(neededAssignIdent);
172                        }
173                    }
174                }
175            }
176        }
177
178        return reassignedVariableIdents;
179    }
180
181    /**
182     * Gets statements that follow the conditional where pattern variable scope extends.
183     * Only returns top-level statements that are siblings of the conditional, excluding
184     * statements nested in control structures like while loops.
185     *
186     * @param conditionalStatement The if statement.
187     * @return List of statement nodes in the extended scope.
188     */
189    private static List<DetailAST> getStatementsInExtendedScope(DetailAST conditionalStatement) {
190        final List<DetailAST> statements = new ArrayList<>();
191
192        DetailAST nextSibling = conditionalStatement.getNextSibling();
193
194        while (nextSibling != null) {
195            final int type = nextSibling.getType();
196            if (type == TokenTypes.EXPR || type == TokenTypes.LITERAL_RETURN
197                    || type == TokenTypes.LITERAL_IF) {
198                statements.add(nextSibling);
199            }
200            else if (type != TokenTypes.SEMI) {
201                break;
202            }
203            nextSibling = nextSibling.getNextSibling();
204        }
205
206        return statements;
207    }
208
209    /**
210     * Traverses along the AST tree to locate the first branch of certain token type.
211     *
212     * @param startingBranch AST branch to start the traverse from, but not check.
213     * @param bound AST Branch that the traverse cannot further extend to.
214     * @param neededTokenType Token type whose first encountered branch is to look for.
215     * @return the AST tree of first encountered branch of needed token type.
216     */
217    @Nullable
218    private static DetailAST traverseUntilNeededBranchType(DetailAST startingBranch,
219                              DetailAST bound, int neededTokenType) {
220
221        DetailAST match = null;
222
223        DetailAST iteratedBranch = shiftToNextTraversedBranch(startingBranch, bound);
224
225        while (iteratedBranch != null) {
226            if (iteratedBranch.getType() == neededTokenType) {
227                match = iteratedBranch;
228                break;
229            }
230
231            iteratedBranch = shiftToNextTraversedBranch(iteratedBranch, bound);
232        }
233
234        return match;
235    }
236
237    /**
238     * Shifts once to the next possible branch within traverse trajectory.
239     *
240     * @param ast AST branch to shift from.
241     * @param boundAst AST Branch that the traverse cannot further extend to.
242     * @return the AST tree of next possible branch within traverse trajectory.
243     */
244    @Nullable
245    private static DetailAST shiftToNextTraversedBranch(DetailAST ast, DetailAST boundAst) {
246        DetailAST newAst = ast;
247
248        if (ast.getFirstChild() != null) {
249            newAst = ast.getFirstChild();
250        }
251        else {
252            while (newAst.getNextSibling() == null && !newAst.equals(boundAst)) {
253                newAst = newAst.getParent();
254            }
255            if (newAst.equals(boundAst)) {
256                newAst = null;
257            }
258            else {
259                newAst = newAst.getNextSibling();
260            }
261        }
262
263        return newAst;
264    }
265
266    /**
267     * Gets the type of ASSIGN tokens that particularly matches with what follows the preceding
268     * branch.
269     *
270     * @param preAssignBranch branch that precedes the branch of ASSIGN token types.
271     * @return type of ASSIGN token.
272     */
273    @Nullable
274    private static DetailAST getMatchedAssignToken(DetailAST preAssignBranch) {
275        DetailAST matchedAssignToken = null;
276
277        for (int assignType : ASSIGN_TOKEN_TYPES) {
278            matchedAssignToken = preAssignBranch.findFirstToken(assignType);
279            if (matchedAssignToken != null) {
280                break;
281            }
282        }
283
284        return matchedAssignToken;
285    }
286
287    /**
288     * Gets the needed AST Ident of reassigned variable for check to compare.
289     *
290     * @param assignToken The AST branch of reassigned variable's ASSIGN token.
291     * @return needed AST Ident.
292     */
293    private static DetailAST getNeededAssignIdent(DetailAST assignToken) {
294        DetailAST assignIdent = assignToken;
295
296        while (traverseUntilNeededBranchType(
297            assignIdent, assignToken.getFirstChild(), TokenTypes.IDENT) != null) {
298
299            assignIdent =
300                traverseUntilNeededBranchType(assignIdent, assignToken, TokenTypes.IDENT);
301        }
302
303        return assignIdent;
304    }
305
306    /**
307     * Checks whether a pattern variable is reassigned and logs a violation if so.
308     *
309     * @param patternVariableIdent AST ident of the pattern variable
310     * @param reassignedVariableIdents list of AST idents that represent reassigned variables
311     */
312    private void checkForReassignment(
313            DetailAST patternVariableIdent,
314            Iterable<DetailAST> reassignedVariableIdents) {
315
316        for (DetailAST assignTokenIdent : reassignedVariableIdents) {
317            if (patternVariableIdent.getText().equals(assignTokenIdent.getText())) {
318                log(assignTokenIdent, MSG_KEY, assignTokenIdent.getText());
319            }
320        }
321    }
322
323    /**
324     * Finds the nearest AST node that defines the scope in which reassignment
325     * of a pattern variable may occur.
326     *
327     * <p>
328     * The scope is determined by locating the closest enclosing conditional
329     * structure such as {@code if}, {@code else}, or ternary operator.
330     * </p>
331     *
332     * @param ast the AST node to start searching from
333     * @return the AST node representing the reassignment scope root,
334     *         or {@code null} if none is found
335     */
336    @Nullable
337    private static DetailAST findReassignmentScopeRoot(DetailAST ast) {
338
339        DetailAST result = null;
340
341        for (DetailAST node = ast; node != null && result == null;
342             node = node.getParent()) {
343
344            final int type = node.getType();
345
346            if (type == TokenTypes.LITERAL_IF
347                    || type == TokenTypes.LITERAL_ELSE
348                    || type == TokenTypes.QUESTION) {
349                result = node;
350            }
351        }
352
353        return result;
354    }
355
356    /**
357     * Expands the reassignment scope root into concrete AST branches
358     * that may contain reassigned pattern variables.
359     *
360     * <p>
361     * For ternary expressions, the conditional expression itself is
362     * treated as the reassignment scope. For {@code if} / {@code else}
363     * statements, the method includes the statement list and any
364     * subsequent statements where the pattern variable remains in scope.
365     * </p>
366     *
367     * @param scopeRoot the root AST node of the reassignment scope
368     * @return list of AST branches that may contain reassignments
369     */
370    private static List<DetailAST> expandReassignmentScopes(
371            DetailAST scopeRoot) {
372
373        final List<DetailAST> branches = new ArrayList<>();
374
375        addBodyBranch(branches, scopeRoot);
376        branches.addAll(getStatementsInExtendedScope(scopeRoot));
377
378        return branches;
379    }
380
381    /**
382     * Adds the body branch of a conditional (if/else) to the list.
383     * For braced blocks, adds the SLIST. For unbraced statements,
384     * adds the single statement directly.
385     *
386     * @param branches collection to add the body branch to
387     * @param scopeRoot the if/else AST node
388     */
389    private static void addBodyBranch(Collection<DetailAST> branches,
390            DetailAST scopeRoot) {
391        if (scopeRoot.getType() == TokenTypes.LITERAL_IF) {
392            final DetailAST body = TokenUtil.findFirstTokenByPredicate(scopeRoot,
393                    node -> node.getType() == TokenTypes.RPAREN)
394                    .map(DetailAST::getNextSibling)
395                    .orElse(scopeRoot);
396            branches.add(body);
397        }
398        else {
399            branches.add(scopeRoot);
400        }
401    }
402
403}