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