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                 reassignedVariableIdents.add(getNeededAssignIdent(assignToken));
166             }
167 
168         }
169 
170         return reassignedVariableIdents;
171 
172     }
173 
174     /**
175      * Gets the closest consistent AST branch that leads to reassigned variable's ident.
176      *
177      * @param ast ast tree of checked instanceof statement.
178      * @return the closest consistent AST branch that leads to reassigned variable's ident.
179      */
180     @Nullable
181     private static DetailAST getBranchLeadingToReassignedVars(DetailAST ast) {
182         DetailAST leadingToReassignedVarBranch = null;
183 
184         for (DetailAST conditionalStatement = ast;
185              conditionalStatement != null && leadingToReassignedVarBranch == null;
186              conditionalStatement = conditionalStatement.getParent()) {
187 
188             if (conditionalStatement.getType() == TokenTypes.LITERAL_IF
189                 || conditionalStatement.getType() == TokenTypes.LITERAL_ELSE) {
190 
191                 leadingToReassignedVarBranch =
192                     conditionalStatement.findFirstToken(TokenTypes.SLIST);
193 
194             }
195             else if (conditionalStatement.getType() == TokenTypes.QUESTION) {
196                 leadingToReassignedVarBranch = conditionalStatement;
197             }
198         }
199 
200         return leadingToReassignedVarBranch;
201 
202     }
203 
204     /**
205      * Traverses along the AST tree to locate the first branch of certain token type.
206      *
207      * @param startingBranch AST branch to start the traverse from, but not check.
208      * @param bound AST Branch that the traverse cannot further extend to.
209      * @param neededTokenType Token type whose first encountered branch is to look for.
210      * @return the AST tree of first encountered branch of needed token type.
211      */
212     @Nullable
213     private static DetailAST traverseUntilNeededBranchType(DetailAST startingBranch,
214                               DetailAST bound, int neededTokenType) {
215 
216         DetailAST match = null;
217 
218         DetailAST iteratedBranch = shiftToNextTraversedBranch(startingBranch, bound);
219 
220         while (iteratedBranch != null) {
221             if (iteratedBranch.getType() == neededTokenType) {
222                 match = iteratedBranch;
223                 break;
224             }
225 
226             iteratedBranch = shiftToNextTraversedBranch(iteratedBranch, bound);
227         }
228 
229         return match;
230     }
231 
232     /**
233      * Shifts once to the next possible branch within traverse trajectory.
234      *
235      * @param ast AST branch to shift from.
236      * @param boundAst AST Branch that the traverse cannot further extend to.
237      * @return the AST tree of next possible branch within traverse trajectory.
238      */
239     @Nullable
240     private static DetailAST shiftToNextTraversedBranch(DetailAST ast, DetailAST boundAst) {
241         DetailAST newAst = ast;
242 
243         if (ast.getFirstChild() != null) {
244             newAst = ast.getFirstChild();
245         }
246         else {
247             while (newAst.getNextSibling() == null && !newAst.equals(boundAst)) {
248                 newAst = newAst.getParent();
249             }
250             if (newAst.equals(boundAst)) {
251                 newAst = null;
252             }
253             else {
254                 newAst = newAst.getNextSibling();
255             }
256         }
257 
258         return newAst;
259     }
260 
261     /**
262      * Gets the type of ASSIGN tokens that particularly matches with what follows the preceding
263      * branch.
264      *
265      * @param preAssignBranch branch that precedes the branch of ASSIGN token types.
266      * @return type of ASSIGN token.
267      */
268     @Nullable
269     private static DetailAST getMatchedAssignToken(DetailAST preAssignBranch) {
270         DetailAST matchedAssignToken = null;
271 
272         for (int assignType : ASSIGN_TOKEN_TYPES) {
273             matchedAssignToken = preAssignBranch.findFirstToken(assignType);
274             if (matchedAssignToken != null) {
275                 break;
276             }
277         }
278 
279         return matchedAssignToken;
280     }
281 
282     /**
283      * Gets the needed AST Ident of reassigned variable for check to compare.
284      *
285      * @param assignToken The AST branch of reassigned variable's ASSIGN token.
286      * @return needed AST Ident.
287      */
288     private static DetailAST getNeededAssignIdent(DetailAST assignToken) {
289         DetailAST assignIdent = assignToken;
290 
291         while (traverseUntilNeededBranchType(
292             assignIdent, assignToken.getFirstChild(), TokenTypes.IDENT) != null) {
293 
294             assignIdent =
295                 traverseUntilNeededBranchType(assignIdent, assignToken, TokenTypes.IDENT);
296         }
297 
298         return assignIdent;
299     }
300 }