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    /**
066     * Creates a new {@code PatternVariableAssignmentCheck} instance.
067     */
068    public PatternVariableAssignmentCheck() {
069        // no code by default
070    }
071
072    @Override
073    public int[] getRequiredTokens() {
074        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
075    }
076
077    @Override
078    public int[] getDefaultTokens() {
079        return getRequiredTokens();
080    }
081
082    @Override
083    public int[] getAcceptableTokens() {
084        return getRequiredTokens();
085    }
086
087    @Override
088    public void visitToken(DetailAST ast) {
089
090        final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
091        final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);
092
093        for (DetailAST patternVariableIdent : patternVariableIdents) {
094            checkForReassignment(patternVariableIdent, reassignedVariableIdents);
095        }
096    }
097
098    /**
099     * Gets the list of all pattern variable idents in instanceof expression.
100     *
101     * @param ast ast tree of instanceof to get the list from.
102     * @return list of pattern variables.
103     */
104    private static List<DetailAST> getPatternVariableIdents(DetailAST ast) {
105
106        final DetailAST outermostPatternVariable =
107            ast.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF);
108
109        final DetailAST recordPatternDef;
110        if (ast.getType() == TokenTypes.LITERAL_INSTANCEOF) {
111            recordPatternDef = ast.findFirstToken(TokenTypes.RECORD_PATTERN_DEF);
112        }
113        else {
114            recordPatternDef = ast;
115        }
116
117        final List<DetailAST> patternVariableIdentsArray = new ArrayList<>();
118
119        if (outermostPatternVariable != null) {
120            patternVariableIdentsArray.add(
121                outermostPatternVariable.findFirstToken(TokenTypes.IDENT));
122        }
123        else if (recordPatternDef != null) {
124            final DetailAST recordPatternComponents = recordPatternDef
125                .findFirstToken(TokenTypes.RECORD_PATTERN_COMPONENTS);
126
127            if (recordPatternComponents != null) {
128                for (DetailAST innerPatternVariable = recordPatternComponents.getFirstChild();
129                     innerPatternVariable != null;
130                     innerPatternVariable = innerPatternVariable.getNextSibling()) {
131
132                    if (innerPatternVariable.getType() == TokenTypes.PATTERN_VARIABLE_DEF) {
133                        patternVariableIdentsArray.add(
134                            innerPatternVariable.findFirstToken(TokenTypes.IDENT));
135                    }
136                    else {
137                        patternVariableIdentsArray.addAll(
138                            getPatternVariableIdents(innerPatternVariable));
139                    }
140
141                }
142            }
143
144        }
145        return patternVariableIdentsArray;
146    }
147
148    /**
149     * Gets the list of AST branches of reassigned variable identifiers.
150     *
151     * @param ast ast tree of checked instanceof statement
152     * @return list of AST identifiers that represent reassigned variables
153     */
154    private static List<DetailAST> getReassignedVariableIdents(DetailAST ast) {
155
156        final List<DetailAST> reassignedVariableIdents = new ArrayList<>();
157        final DetailAST scopeRoot = findReassignmentScopeRoot(ast);
158
159        if (scopeRoot != null) {
160
161            final List<DetailAST> branches =
162                    expandReassignmentScopes(scopeRoot);
163
164            for (DetailAST branch : branches) {
165                for (DetailAST expressionBranch = branch;
166                     expressionBranch != null;
167                     expressionBranch = shiftToNextTraversedBranch(
168                             expressionBranch, branch)) {
169
170                    final DetailAST assignToken =
171                            getMatchedAssignToken(expressionBranch);
172
173                    if (assignToken != null) {
174                        reassignedVariableIdents.add(assignToken.getFirstChild());
175                    }
176                }
177            }
178        }
179
180        return reassignedVariableIdents;
181    }
182
183    /**
184     * Gets statements that follow the conditional where pattern variable scope extends.
185     * Only returns top-level statements that are siblings of the conditional, excluding
186     * statements nested in control structures like while loops.
187     *
188     * @param conditionalStatement The if statement.
189     * @return List of statement nodes in the extended scope.
190     */
191    private static List<DetailAST> getStatementsInExtendedScope(DetailAST conditionalStatement) {
192        final List<DetailAST> statements = new ArrayList<>();
193
194        DetailAST nextSibling = conditionalStatement.getNextSibling();
195
196        while (nextSibling != null) {
197            final int type = nextSibling.getType();
198            if (type == TokenTypes.EXPR || type == TokenTypes.LITERAL_RETURN
199                    || type == TokenTypes.LITERAL_IF) {
200                statements.add(nextSibling);
201            }
202            else if (type != TokenTypes.SEMI) {
203                break;
204            }
205            nextSibling = nextSibling.getNextSibling();
206        }
207
208        return statements;
209    }
210
211    /**
212     * Shifts once to the next possible branch within traverse trajectory.
213     *
214     * @param ast AST branch to shift from.
215     * @param boundAst AST Branch that the traverse cannot further extend to.
216     * @return the AST tree of next possible branch within traverse trajectory.
217     */
218    @Nullable
219    private static DetailAST shiftToNextTraversedBranch(DetailAST ast, DetailAST boundAst) {
220        DetailAST newAst = ast;
221
222        if (ast.getFirstChild() != null) {
223            newAst = ast.getFirstChild();
224        }
225        else {
226            while (newAst.getNextSibling() == null && !newAst.equals(boundAst)) {
227                newAst = newAst.getParent();
228            }
229            if (newAst.equals(boundAst)) {
230                newAst = null;
231            }
232            else {
233                newAst = newAst.getNextSibling();
234            }
235        }
236
237        return newAst;
238    }
239
240    /**
241     * Gets the type of ASSIGN tokens that particularly matches with what follows the preceding
242     * branch.
243     *
244     * @param preAssignBranch branch that precedes the branch of ASSIGN token types.
245     * @return type of ASSIGN token.
246     */
247    @Nullable
248    private static DetailAST getMatchedAssignToken(DetailAST preAssignBranch) {
249        DetailAST matchedAssignToken = null;
250
251        for (int assignType : ASSIGN_TOKEN_TYPES) {
252            matchedAssignToken = preAssignBranch.findFirstToken(assignType);
253            if (matchedAssignToken != null) {
254                break;
255            }
256        }
257
258        return matchedAssignToken;
259    }
260
261    /**
262     * Checks whether a pattern variable is reassigned and logs a violation if so.
263     *
264     * @param patternVariableIdent AST ident of the pattern variable
265     * @param reassignedVariableIdents list of AST idents that represent reassigned variables
266     */
267    private void checkForReassignment(
268            DetailAST patternVariableIdent,
269            Iterable<DetailAST> reassignedVariableIdents) {
270
271        for (DetailAST assignTokenIdent : reassignedVariableIdents) {
272            if (patternVariableIdent.getText().equals(assignTokenIdent.getText())) {
273                log(assignTokenIdent, MSG_KEY, assignTokenIdent.getText());
274            }
275        }
276    }
277
278    /**
279     * Finds the nearest AST node that defines the scope in which reassignment
280     * of a pattern variable may occur.
281     *
282     * <p>
283     * The scope is determined by locating the closest enclosing conditional
284     * structure such as {@code if}, {@code else}, or ternary operator.
285     * </p>
286     *
287     * @param ast the AST node to start searching from
288     * @return the AST node representing the reassignment scope root,
289     *         or {@code null} if none is found
290     */
291    @Nullable
292    private static DetailAST findReassignmentScopeRoot(DetailAST ast) {
293
294        DetailAST result = null;
295
296        for (DetailAST node = ast; node != null && result == null;
297             node = node.getParent()) {
298
299            final int type = node.getType();
300
301            if (type == TokenTypes.LITERAL_IF
302                    || type == TokenTypes.LITERAL_ELSE
303                    || type == TokenTypes.QUESTION) {
304                result = node;
305            }
306        }
307
308        return result;
309    }
310
311    /**
312     * Expands the reassignment scope root into concrete AST branches
313     * that may contain reassigned pattern variables.
314     *
315     * <p>
316     * For ternary expressions, the conditional expression itself is
317     * treated as the reassignment scope. For {@code if} / {@code else}
318     * statements, the method includes the statement list and any
319     * subsequent statements where the pattern variable remains in scope.
320     * </p>
321     *
322     * @param scopeRoot the root AST node of the reassignment scope
323     * @return list of AST branches that may contain reassignments
324     */
325    private static List<DetailAST> expandReassignmentScopes(
326            DetailAST scopeRoot) {
327
328        final List<DetailAST> branches = new ArrayList<>();
329
330        addBodyBranch(branches, scopeRoot);
331        branches.addAll(getStatementsInExtendedScope(scopeRoot));
332
333        return branches;
334    }
335
336    /**
337     * Adds the body branch of a conditional (if/else) to the list.
338     * For braced blocks, adds the SLIST. For unbraced statements,
339     * adds the single statement directly.
340     *
341     * @param branches collection to add the body branch to
342     * @param scopeRoot the if/else AST node
343     */
344    private static void addBodyBranch(Collection<DetailAST> branches,
345            DetailAST scopeRoot) {
346        if (scopeRoot.getType() == TokenTypes.LITERAL_IF) {
347            final DetailAST body = TokenUtil.findFirstTokenByPredicate(scopeRoot,
348                    node -> node.getType() == TokenTypes.RPAREN)
349                    .map(DetailAST::getNextSibling)
350                    .orElse(scopeRoot);
351            branches.add(body);
352        }
353        else {
354            branches.add(scopeRoot);
355        }
356    }
357
358}