View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.coding;
21  
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.List;
25  import java.util.Set;
26  
27  import javax.annotation.Nullable;
28  
29  import com.puppycrawl.tools.checkstyle.StatelessCheck;
30  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31  import com.puppycrawl.tools.checkstyle.api.DetailAST;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
34  
35  /**
36   * <div>
37   * Checks for assignment of pattern variables.
38   * </div>
39   *
40   * <p>
41   * Pattern variable assignment is considered bad programming practice. The pattern variable
42   * is meant to be a direct reference to the object being matched. Reassigning it can break this
43   * connection and mislead readers.
44   * </p>
45   *
46   * @since 10.26.0
47   */
48  @StatelessCheck
49  public class PatternVariableAssignmentCheck extends AbstractCheck {
50  
51      /**
52       * A key is pointing to the warning message in "messages.properties" file.
53       */
54      public static final String MSG_KEY = "pattern.variable.assignment";
55  
56      /**
57       * The set of all valid types of ASSIGN token for this check.
58       */
59      private static final Set<Integer> ASSIGN_TOKEN_TYPES = Set.of(
60          TokenTypes.ASSIGN, TokenTypes.PLUS_ASSIGN, TokenTypes.MINUS_ASSIGN, TokenTypes.STAR_ASSIGN,
61          TokenTypes.DIV_ASSIGN, TokenTypes.MOD_ASSIGN, TokenTypes.SR_ASSIGN, TokenTypes.BSR_ASSIGN,
62          TokenTypes.SL_ASSIGN, TokenTypes.BAND_ASSIGN, TokenTypes.BXOR_ASSIGN,
63          TokenTypes.BOR_ASSIGN);
64  
65      @Override
66      public int[] getRequiredTokens() {
67          return new int[] {TokenTypes.LITERAL_INSTANCEOF};
68      }
69  
70      @Override
71      public int[] getDefaultTokens() {
72          return getRequiredTokens();
73      }
74  
75      @Override
76      public int[] getAcceptableTokens() {
77          return getRequiredTokens();
78      }
79  
80      @Override
81      public void visitToken(DetailAST ast) {
82  
83          final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
84          final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);
85  
86          for (DetailAST patternVariableIdent : patternVariableIdents) {
87              checkForReassignment(patternVariableIdent, reassignedVariableIdents);
88          }
89      }
90  
91      /**
92       * Gets the list of all pattern variable idents in instanceof expression.
93       *
94       * @param ast ast tree of instanceof to get the list from.
95       * @return list of pattern variables.
96       */
97      private static List<DetailAST> getPatternVariableIdents(DetailAST ast) {
98  
99          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 }