View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.List;
24  import java.util.Set;
25  
26  import javax.annotation.Nullable;
27  
28  import com.puppycrawl.tools.checkstyle.StatelessCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
32  
33  /**
34   * <div>
35   * Checks for assignment of pattern variables.
36   * </div>
37   *
38   * <p>
39   * Pattern variable assignment is considered bad programming practice. The pattern variable
40   * is meant to be a direct reference to the object being matched. Reassigning it can break this
41   * connection and mislead readers.
42   * </p>
43   *
44   * @since 10.26.0
45   */
46  @StatelessCheck
47  public class PatternVariableAssignmentCheck extends AbstractCheck {
48  
49      /**
50       * A key is pointing to the warning message in "messages.properties" file.
51       */
52      public static final String MSG_KEY = "pattern.variable.assignment";
53  
54      /**
55       * The set of all valid types of ASSIGN token for this check.
56       */
57      private static final Set<Integer> ASSIGN_TOKEN_TYPES = Set.of(
58          TokenTypes.ASSIGN, TokenTypes.PLUS_ASSIGN, TokenTypes.MINUS_ASSIGN, TokenTypes.STAR_ASSIGN,
59          TokenTypes.DIV_ASSIGN, TokenTypes.MOD_ASSIGN, TokenTypes.SR_ASSIGN, TokenTypes.BSR_ASSIGN,
60          TokenTypes.SL_ASSIGN, TokenTypes.BAND_ASSIGN, TokenTypes.BXOR_ASSIGN,
61          TokenTypes.BOR_ASSIGN);
62  
63      @Override
64      public int[] getRequiredTokens() {
65          return new int[] {TokenTypes.LITERAL_INSTANCEOF};
66      }
67  
68      @Override
69      public int[] getDefaultTokens() {
70          return getRequiredTokens();
71      }
72  
73      @Override
74      public int[] getAcceptableTokens() {
75          return getRequiredTokens();
76      }
77  
78      @Override
79      public void visitToken(DetailAST ast) {
80  
81          final List<DetailAST> patternVariableIdents = getPatternVariableIdents(ast);
82          final List<DetailAST> reassignedVariableIdents = getReassignedVariableIdents(ast);
83  
84          for (DetailAST patternVariableIdent : patternVariableIdents) {
85              for (DetailAST assignTokenIdent : reassignedVariableIdents) {
86                  if (patternVariableIdent.getText().equals(assignTokenIdent.getText())) {
87  
88                      log(assignTokenIdent, MSG_KEY, assignTokenIdent.getText());
89                      break;
90                  }
91  
92              }
93          }
94      }
95  
96      /**
97       * Gets the list of all pattern variable idents in instanceof expression.
98       *
99       * @param ast ast tree of instanceof to get the list from.
100      * @return list of pattern variables.
101      */
102     private static List<DetailAST> getPatternVariableIdents(DetailAST ast) {
103 
104         final DetailAST outermostPatternVariable =
105             ast.findFirstToken(TokenTypes.PATTERN_VARIABLE_DEF);
106 
107         final DetailAST recordPatternDef;
108         if (ast.getType() == TokenTypes.LITERAL_INSTANCEOF) {
109             recordPatternDef = ast.findFirstToken(TokenTypes.RECORD_PATTERN_DEF);
110         }
111         else {
112             recordPatternDef = ast;
113         }
114 
115         final List<DetailAST> patternVariableIdentsArray = new ArrayList<>();
116 
117         if (outermostPatternVariable != null) {
118             patternVariableIdentsArray.add(
119                 outermostPatternVariable.findFirstToken(TokenTypes.IDENT));
120         }
121         else if (recordPatternDef != null) {
122             final DetailAST recordPatternComponents = recordPatternDef
123                 .findFirstToken(TokenTypes.RECORD_PATTERN_COMPONENTS);
124 
125             if (recordPatternComponents != null) {
126                 for (DetailAST innerPatternVariable = recordPatternComponents.getFirstChild();
127                      innerPatternVariable != null;
128                      innerPatternVariable = innerPatternVariable.getNextSibling()) {
129 
130                     if (innerPatternVariable.getType() == TokenTypes.PATTERN_VARIABLE_DEF) {
131                         patternVariableIdentsArray.add(
132                             innerPatternVariable.findFirstToken(TokenTypes.IDENT));
133                     }
134                     else {
135                         patternVariableIdentsArray.addAll(
136                             getPatternVariableIdents(innerPatternVariable));
137                     }
138 
139                 }
140             }
141 
142         }
143         return patternVariableIdentsArray;
144     }
145 
146     /**
147      * Gets the array list made out of AST branches of reassigned variable idents.
148      *
149      * @param ast ast tree of checked instanceof statement.
150      * @return the list of AST branches of reassigned variable idents.
151      */
152     private static List<DetailAST> getReassignedVariableIdents(DetailAST ast) {
153 
154         final DetailAST branchLeadingToReassignedVar = getBranchLeadingToReassignedVars(ast);
155         final List<DetailAST> reassignedVariableIdents = new ArrayList<>();
156 
157         for (DetailAST expressionBranch = branchLeadingToReassignedVar;
158              expressionBranch != null;
159              expressionBranch = traverseUntilNeededBranchType(expressionBranch,
160                  branchLeadingToReassignedVar, TokenTypes.EXPR)) {
161 
162             final DetailAST assignToken = getMatchedAssignToken(expressionBranch);
163 
164             if (assignToken != null) {
165                 final DetailAST neededAssignIdent = getNeededAssignIdent(assignToken);
166                 if (neededAssignIdent.getPreviousSibling() == null) {
167                     reassignedVariableIdents.add(getNeededAssignIdent(assignToken));
168                 }
169             }
170         }
171 
172         return reassignedVariableIdents;
173 
174     }
175 
176     /**
177      * Gets the closest consistent AST branch that leads to reassigned variable's ident.
178      *
179      * @param ast ast tree of checked instanceof statement.
180      * @return the closest consistent AST branch that leads to reassigned variable's ident.
181      */
182     @Nullable
183     private static DetailAST getBranchLeadingToReassignedVars(DetailAST ast) {
184         DetailAST leadingToReassignedVarBranch = null;
185 
186         for (DetailAST conditionalStatement = ast;
187              conditionalStatement != null && leadingToReassignedVarBranch == null;
188              conditionalStatement = conditionalStatement.getParent()) {
189 
190             if (conditionalStatement.getType() == TokenTypes.LITERAL_IF
191                 || conditionalStatement.getType() == TokenTypes.LITERAL_ELSE) {
192 
193                 leadingToReassignedVarBranch =
194                     conditionalStatement.findFirstToken(TokenTypes.SLIST);
195 
196             }
197             else if (conditionalStatement.getType() == TokenTypes.QUESTION) {
198                 leadingToReassignedVarBranch = conditionalStatement;
199             }
200         }
201 
202         return leadingToReassignedVarBranch;
203 
204     }
205 
206     /**
207      * Traverses along the AST tree to locate the first branch of certain token type.
208      *
209      * @param startingBranch AST branch to start the traverse from, but not check.
210      * @param bound AST Branch that the traverse cannot further extend to.
211      * @param neededTokenType Token type whose first encountered branch is to look for.
212      * @return the AST tree of first encountered branch of needed token type.
213      */
214     @Nullable
215     private static DetailAST traverseUntilNeededBranchType(DetailAST startingBranch,
216                               DetailAST bound, int neededTokenType) {
217 
218         DetailAST match = null;
219 
220         DetailAST iteratedBranch = shiftToNextTraversedBranch(startingBranch, bound);
221 
222         while (iteratedBranch != null) {
223             if (iteratedBranch.getType() == neededTokenType) {
224                 match = iteratedBranch;
225                 break;
226             }
227 
228             iteratedBranch = shiftToNextTraversedBranch(iteratedBranch, bound);
229         }
230 
231         return match;
232     }
233 
234     /**
235      * Shifts once to the next possible branch within traverse trajectory.
236      *
237      * @param ast AST branch to shift from.
238      * @param boundAst AST Branch that the traverse cannot further extend to.
239      * @return the AST tree of next possible branch within traverse trajectory.
240      */
241     @Nullable
242     private static DetailAST shiftToNextTraversedBranch(DetailAST ast, DetailAST boundAst) {
243         DetailAST newAst = ast;
244 
245         if (ast.getFirstChild() != null) {
246             newAst = ast.getFirstChild();
247         }
248         else {
249             while (newAst.getNextSibling() == null && !newAst.equals(boundAst)) {
250                 newAst = newAst.getParent();
251             }
252             if (newAst.equals(boundAst)) {
253                 newAst = null;
254             }
255             else {
256                 newAst = newAst.getNextSibling();
257             }
258         }
259 
260         return newAst;
261     }
262 
263     /**
264      * Gets the type of ASSIGN tokens that particularly matches with what follows the preceding
265      * branch.
266      *
267      * @param preAssignBranch branch that precedes the branch of ASSIGN token types.
268      * @return type of ASSIGN token.
269      */
270     @Nullable
271     private static DetailAST getMatchedAssignToken(DetailAST preAssignBranch) {
272         DetailAST matchedAssignToken = null;
273 
274         for (int assignType : ASSIGN_TOKEN_TYPES) {
275             matchedAssignToken = preAssignBranch.findFirstToken(assignType);
276             if (matchedAssignToken != null) {
277                 break;
278             }
279         }
280 
281         return matchedAssignToken;
282     }
283 
284     /**
285      * Gets the needed AST Ident of reassigned variable for check to compare.
286      *
287      * @param assignToken The AST branch of reassigned variable's ASSIGN token.
288      * @return needed AST Ident.
289      */
290     private static DetailAST getNeededAssignIdent(DetailAST assignToken) {
291         DetailAST assignIdent = assignToken;
292 
293         while (traverseUntilNeededBranchType(
294             assignIdent, assignToken.getFirstChild(), TokenTypes.IDENT) != null) {
295 
296             assignIdent =
297                 traverseUntilNeededBranchType(assignIdent, assignToken, TokenTypes.IDENT);
298         }
299 
300         return assignIdent;
301     }
302 }