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      /**
66       * Creates a new {@code PatternVariableAssignmentCheck} instance.
67       */
68      public PatternVariableAssignmentCheck() {
69          // no code by default
70      }
71  
72      @Override
73      public int[] getRequiredTokens() {
74          return new int[] {TokenTypes.LITERAL_INSTANCEOF};
75      }
76  
77      @Override
78      public int[] getDefaultTokens() {
79          return getRequiredTokens();
80      }
81  
82      @Override
83      public int[] getAcceptableTokens() {
84          return getRequiredTokens();
85      }
86  
87      @Override
88      public void visitToken(DetailAST ast) {
89  
90          final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
91          final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);
92  
93          for (DetailAST patternVariableIdent : patternVariableIdents) {
94              checkForReassignment(patternVariableIdent, reassignedVariableIdents);
95          }
96      }
97  
98      /**
99       * 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 }